From 01c85670ec1f754d77c2d173e232a200dd90c35f Mon Sep 17 00:00:00 2001 From: FlightControl Date: Sat, 23 Apr 2016 08:17:57 +0200 Subject: [PATCH] Event functions replacement stable now ... (I think)... --- Embedded/Moose_Create_Embedded.bat | 1 + Embedded/Moose_Embedded.lua | 3447 +++++++++++------ Moose/CleanUp.lua | 19 +- Moose/Database.lua | 1488 ++++--- Moose/Event.lua | 137 +- Moose/Sead.lua | 2 +- Moose/Spawn.lua | 2 +- .../Moose_Test_CLEANUP/Moose_Test_CLEANUP.lua | 10 + .../Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz | Bin 0 -> 99751 bytes 9 files changed, 3031 insertions(+), 2075 deletions(-) create mode 100644 Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.lua create mode 100644 Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz diff --git a/Embedded/Moose_Create_Embedded.bat b/Embedded/Moose_Create_Embedded.bat index 7211a207d..7dfa51470 100644 --- a/Embedded/Moose_Create_Embedded.bat +++ b/Embedded/Moose_Create_Embedded.bat @@ -2,6 +2,7 @@ rem Generate Moose_Embedded.lua copy /b ..\Moose\Routines.lua ^ + ..\Moose\Base.lua ^ + + ..\Moose\Event.lua ^ + ..\Moose\Menu.lua ^ + ..\Moose\Group.lua ^ + ..\Moose\Unit.lua ^ diff --git a/Embedded/Moose_Embedded.lua b/Embedded/Moose_Embedded.lua index 5f2fa13cb..7ab06e4d4 100644 --- a/Embedded/Moose_Embedded.lua +++ b/Embedded/Moose_Embedded.lua @@ -2624,6 +2624,8 @@ FORMATION = { Cone = "Cone" } + + --- The base constructor. This is the top top class of all classed defined within the MOOSE. -- Any new class needs to be derived from this class for proper inheritance. -- @param #BASE self @@ -2718,6 +2720,18 @@ function BASE:AddEvent( Event, EventFunction ) return self end +--- Returns the event dispatcher +-- @param #BASE self +-- @return Event#EVENT +function BASE:Event() + + return _EventDispatcher +end + + + + + --- Enable the event listeners for the class. -- @param #BASE self -- @return #BASE @@ -2831,8 +2845,8 @@ end -- @param #BASE self -- @param DCSTypes#Event event function BASE:onEvent(event) + --self:F( { BaseEventCodes[event.id], event } ) - --env.info( 'onEvent Table self = ' .. tostring(self) ) if self then for EventID, EventObject in pairs( self.Events ) do if EventObject.EventEnabled then @@ -2847,8 +2861,8 @@ function BASE:onEvent(event) if event.target and event.target:isExist() then event.TgtUnitName = event.target:getName() end - self:T( { BaseEventCodes[event.id], event } ) - EventObject.EventFunction( self, event ) + --self:T( { BaseEventCodes[event.id], event } ) + --EventObject.EventFunction( self, event ) end end end @@ -2909,7 +2923,7 @@ function BASE:F( Arguments ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d\(%6d\)/%1s:%20s%05d.%s\(%s\)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -2957,7 +2971,7 @@ function BASE:T( Arguments ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d\(%6d\)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -3000,11 +3014,500 @@ function BASE:E( Arguments ) local LineCurrent = DebugInfoCurrent.currentline local LineFrom = DebugInfoFrom.currentline - env.info( string.format( "%6d\(%6d\)/%1s:%20s%05d.%s\(%s\)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) end +--- The EVENT class models an efficient event handling process between other classes and its units, weapons. +-- @module Event +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +local _EVENTCODES = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--- The Event structure +-- @type EVENTDATA +-- @field id +-- @field initiator +-- @field target +-- @field weapon +-- @field IniDCSUnit +-- @field IniDCSUnitName +-- @field IniDCSGroup +-- @field IniDCSGroupName +-- @field TgtDCSUnit +-- @field TgtDCSUnitName +-- @field TgtDCSGroup +-- @field TgtDCSGroupName +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +function EVENT:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F() + self.EventHandler = world.addEventHandler( self ) + return self +end + +function EVENT:EventText( EventID ) + + local EventText = _EVENTCODES[EventID] + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param DCSWorld#world.event EventID +-- @param #string EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTCODES[EventID], EventClass } ) + if not self.Events[EventID] then + self.Events[EventID] = {} + end + if not self.Events[EventID][EventClass] then + self.Events[EventID][EventClass] = {} + end + return self.Events[EventID][EventClass] +end + + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @param #function OnEventFunction +-- @return #EVENT +function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) + self:F2( EventTemplate.name ) + + for EventUnitID, EventUnit in pairs( EventTemplate.units ) do + OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) + end + return self +end + +--- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) + self:F2( { EventID } ) + + local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + Event.EventFunction = EventFunction + Event.EventSelf = EventSelf + return self +end + + +--- Set a new listener for an S_EVENT_X event +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) + self:F2( EventDCSUnitName ) + + local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + if not Event.IniUnit then + Event.IniUnit = {} + end + Event.IniUnit[EventDCSUnitName] = {} + Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction + Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf + return self +end + + +--- Create an OnBirth event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnBirth( EventFunction, EventSelf ) + self:F() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Set a new listener for an S_EVENT_BIRTH event. +-- @param #EVENT self +-- @param #string EventDCSUnitName The id of the unit for the event to be handled. +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Create an OnCrash event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_CRASH event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnCrash( EventFunction, EventSelf ) + self:F() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self +end + +--- Set a new listener for an S_EVENT_CRASH event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_DEAD event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnDead( EventFunction, EventSelf ) + self:F() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + + +--- Set a new listener for an S_EVENT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + +--- Set a new listener for an S_EVENT_PILOT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_LAND event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_TAKEOFF event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self +end + +--- Set a new listener for an S_EVENT_ENGINE_STARTUP event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) + + return self +end + +--- Set a new listener for an S_EVENT_SHOT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnShot( EventFunction, EventSelf ) + self:F() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self +end + +--- Set a new listener for an S_EVENT_SHOT event for a unit. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self +end + +--- Set a new listener for an S_EVENT_HIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnHit( EventFunction, EventSelf ) + self:F() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self +end + +--- Set a new listener for an S_EVENT_HIT event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self +end + + + +function EVENT:onEvent( Event ) + self:F( { _EVENTCODES[Event.id], Event } ) + + if self and self.Events and self.Events[Event.id] then + if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + end + end + if Event.target then + if Event.target and Event.target:getCategory() == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + end + end + end + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + for ClassName, EventData in pairs( self.Events[Event.id] ) do + if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then + self:T2( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) + EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) + else + if Event.IniDCSUnit and not EventData.IniUnit then + self:T2( { "Calling event function for class ", ClassName } ) + EventData.EventFunction( EventData.EventSelf, Event ) + end + end + end + end +end + + + +--- Declare the event dispatcher based on the EVENT class +_EventDispatcher = EVENT:New() -- #EVENT + --- Encapsulation of DCS World Menu system in a set of MENU classes. -- @module Menu @@ -4993,36 +5496,37 @@ Include.File( "Routines" ) Include.File( "Base" ) Include.File( "Menu" ) Include.File( "Group" ) +Include.File( "Event" ) --- The DATABASE class -- @type DATABASE -- @extends Base#BASE DATABASE = { - ClassName = "DATABASE", - Units = {}, - Groups = {}, - NavPoints = {}, - Statics = {}, - Players = {}, - ActivePlayers = {}, - ClientsByName = {}, - ClientsByID = {}, + ClassName = "DATABASE", + Units = {}, + Groups = {}, + NavPoints = {}, + Statics = {}, + Players = {}, + ActivePlayers = {}, + ClientsByName = {}, + ClientsByID = {}, } -DATABASECoalition = -{ - [1] = "Red", - [2] = "Blue", -} +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + } -DATABASECategory = -{ - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", -} +local _DATABASECategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } --- Creates a new DATABASE Object to administer the Groups defined and alive within the DCSRTE. @@ -5032,83 +5536,79 @@ DATABASECategory = -- DBObject = DATABASE:New() function DATABASE:New() - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.Navpoints = {} - self.Units = {} - --Build routines.db.units and self.Navpoints - for coa_name, coa_data in pairs(env.mission.coalition) do + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[coa_name] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) + self.Navpoints = {} + self.Units = {} + --Build routines.db.units and self.Navpoints + for coa_name, coa_data in pairs(env.mission.coalition) do - self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 - self.Navpoints[coa_name][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local countryName = string.lower(cntry_data.name) - self.Units[coa_name][countryName] = {} - self.Units[coa_name][countryName]["countryId"] = cntry_data.id + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + self.Units[coa_name] = {} - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local category = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterGroup( GroupTemplate ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do + ---------------------------------------------- + -- build nav points DB + self.Navpoints[coa_name] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do - --self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) - self:AddEvent( world.event.S_EVENT_DEAD, self.OnDeadOrCrash ) - self:AddEvent( world.event.S_EVENT_CRASH, self.OnDeadOrCrash ) - - self:AddEvent( world.event.S_EVENT_HIT, self.OnHit) + if type(nav_data) == 'table' then + self.Navpoints[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) - self:EnableEvents() + self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 + self.Navpoints[coa_name][nav_ind]['point']['z'] = nav_data.y + end + end + end + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do - self.SchedulerId = routines.scheduleFunction( DATABASE._FollowPlayers, { self }, 0, 5 ) - - self:ScoreMenu() - - - return self + local countryName = string.lower(cntry_data.name) + self.Units[coa_name][countryName] = {} + self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local category = obj_type_name + + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + + self.Units[coa_name][countryName][category] = {} + + for group_num, GroupTemplate in pairs(obj_type_data.group) do + + if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group + self:_RegisterGroup( GroupTemplate ) + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + --self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) + _EventDispatcher:OnDead( self._EventOnDeadOrCrash, self ) + _EventDispatcher:OnCrash( self._EventOnDeadOrCrash, self ) + _EventDispatcher:OnHit( self._EventOnHit, self ) + + self.SchedulerId = routines.scheduleFunction( DATABASE._FollowPlayers, { self }, 0, 5 ) + + self:ScoreMenu() + + return self end @@ -5118,49 +5618,49 @@ end -- This method is used by the SPAWN class. function DATABASE:Spawn( SpawnTemplate ) - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID, SpawnTemplate.name } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID - local SpawnCountryID = SpawnTemplate.SpawnCountryID - local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - -- Nullify - SpawnTemplate.SpawnCoalitionID = nil - SpawnTemplate.SpawnCountryID = nil - SpawnTemplate.SpawnCategoryID = nil - - self:_RegisterGroup( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID, SpawnTemplate.name } ) - -- Restore - SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID - SpawnTemplate.SpawnCountryID = SpawnCountryID - SpawnTemplate.SpawnCategoryID = SpawnCategoryID + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID + local SpawnCountryID = SpawnTemplate.SpawnCountryID + local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - local SpawnGroup = GROUP:New( Group.getByName( SpawnTemplate.name ) ) - return SpawnGroup + -- Nullify + SpawnTemplate.SpawnCoalitionID = nil + SpawnTemplate.SpawnCountryID = nil + SpawnTemplate.SpawnCategoryID = nil + + self:_RegisterGroup( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID + SpawnTemplate.SpawnCountryID = SpawnCountryID + SpawnTemplate.SpawnCategoryID = SpawnCategoryID + + + local SpawnGroup = GROUP:New( Group.getByName( SpawnTemplate.name ) ) + return SpawnGroup end --- Set a status to a Group within the Database, this to check crossing events for example. function DATABASE:SetStatusGroup( GroupName, Status ) - self:F( Status ) + self:F( Status ) - self.Groups[GroupName].Status = Status + self.Groups[GroupName].Status = Status end --- Get a status to a Group within the Database, this to check crossing events for example. function DATABASE:GetStatusGroup( GroupName ) - self:F( Status ) + self:F( Status ) - if self.Groups[GroupName] then - return self.Groups[GroupName].Status - else - return "" - end + if self.Groups[GroupName] then + return self.Groups[GroupName].Status + else + return "" + end end @@ -5171,35 +5671,35 @@ end --- Registers new Group Templates within the DATABASE Object. function DATABASE:_RegisterGroup( GroupTemplate ) - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) + local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - if not self.Groups[GroupTemplateName] then - self.Groups[GroupTemplateName] = {} - self.Groups[GroupTemplateName].Status = nil - end - self.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Groups[GroupTemplateName].Template = GroupTemplate - self.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Groups[GroupTemplateName].Units = GroupTemplate.units - - self:T( { "Group", self.Groups[GroupTemplateName].GroupName, self.Groups[GroupTemplateName].UnitCount } ) - - for unit_num, UnitTemplate in pairs(GroupTemplate.units) do - - local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) - self.Units[UnitTemplateName] = {} - self.Units[UnitTemplateName].UnitName = UnitTemplateName - self.Units[UnitTemplateName].Template = UnitTemplate - self.Units[UnitTemplateName].GroupName = GroupTemplateName - self.Units[UnitTemplateName].GroupTemplate = GroupTemplate - self.Units[UnitTemplateName].GroupId = GroupTemplate.groupId - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.ClientsByName[UnitTemplateName] = UnitTemplate - self.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - self:T( { "Unit", self.Units[UnitTemplateName].UnitName } ) - end + if not self.Groups[GroupTemplateName] then + self.Groups[GroupTemplateName] = {} + self.Groups[GroupTemplateName].Status = nil + end + self.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Groups[GroupTemplateName].Template = GroupTemplate + self.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Groups[GroupTemplateName].Units = GroupTemplate.units + + self:T( { "Group", self.Groups[GroupTemplateName].GroupName, self.Groups[GroupTemplateName].UnitCount } ) + + for unit_num, UnitTemplate in pairs(GroupTemplate.units) do + + local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) + self.Units[UnitTemplateName] = {} + self.Units[UnitTemplateName].UnitName = UnitTemplateName + self.Units[UnitTemplateName].Template = UnitTemplate + self.Units[UnitTemplateName].GroupName = GroupTemplateName + self.Units[UnitTemplateName].GroupTemplate = GroupTemplate + self.Units[UnitTemplateName].GroupId = GroupTemplate.groupId + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.ClientsByName[UnitTemplateName] = UnitTemplate + self.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + self:T( { "Unit", self.Units[UnitTemplateName].UnitName } ) + end end @@ -5208,96 +5708,94 @@ end --- Track DCSRTE DEAD or CRASH events for the internal scoring. -function DATABASE:OnDeadOrCrash( event ) - self:F( { event } ) +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - - TargetUnit = event.initiator - TargetGroup = Unit.getGroup( TargetUnit ) - TargetUnitDesc = TargetUnit:getDesc() - - TargetUnitName = TargetUnit:getName() - if TargetGroup and TargetGroup:isExist() then - TargetGroupName = TargetGroup:getName() - end - TargetPlayerName = TargetUnit:getPlayerName() + if Event.IniDCSUnit then - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnitDesc.category -- Workaround - TargetType = TargetUnit:getTypeName() + TargetUnit = Event.IniDCSUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() - TargetUnitCoalition = DATABASECoalition[TargetCoalition] - TargetUnitCategory = DATABASECategory[TargetCategory] - TargetUnitType = TargetType + TargetCoalition = TargetUnit:getCoalition() + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetType = TargetUnit:getTypeName() - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end + TargetUnitCoalition = _DATABASECoalition[TargetCoalition] + TargetUnitCategory = _DATABASECategory[T1argetCategory] + TargetUnitType = TargetType - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = DATABASECoalition[InitCoalition] - local InitUnitCategory = DATABASECategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got killed" ) - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end + -- Some variables + local InitUnitName = PlayerData.UnitName + local InitUnitType = PlayerData.UnitType + local InitCoalition = PlayerData.UnitCoalition + local InitCategory = PlayerData.UnitCategory + local InitUnitCoalition = _DATABASECoalition[InitCoalition] + local InitUnitCategory = _DATABASECategory[InitCategory] - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreAdd( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreAdd( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is he hitting? + if TargetCategory then + if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + if not PlayerData.Kill[TargetCategory] then + PlayerData.Kill[TargetCategory] = {} + end + if not PlayerData.Kill[TargetCategory][TargetType] then + PlayerData.Kill[TargetCategory][TargetType] = {} + PlayerData.Kill[TargetCategory][TargetType].Score = 0 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 + PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 + end + + if InitCoalition == TargetCoalition then + PlayerData.Penalty = PlayerData.Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreAdd( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + PlayerData.Score = PlayerData.Score + 10 + PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreAdd( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + end + end end @@ -5307,20 +5805,20 @@ end --- Follows new players entering Clients within the DCSRTE. function DATABASE:_FollowPlayers() - self:F3( "_FollowPlayers" ) + self:F3( "_FollowPlayers" ) - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayers", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end + local ClientUnit = 0 + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } + local unitId + local unitData + local AlivePlayerUnits = {} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "_FollowPlayers", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:_AddPlayerFromUnit( UnitData ) + end + end end @@ -5330,102 +5828,102 @@ end --- Add a new player entering a Unit. function DATABASE:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) + self:F( UnitData ) - if UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() + if UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + local UnitDesc = UnitData:getDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:getCoalition() + local UnitTypeName = UnitData:getTypeName() - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( DATABASECategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Kill = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( DATABASECategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Kill[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].HitUnits = {} self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. DATABASECoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. DATABASECoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - "", - 2, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - self:ScoreAdd( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, DATABASECoalition[self.Players[PlayerName].UnitCoalition], DATABASECategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, DATABASECoalition[UnitCoalition], DATABASECategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _DATABASECoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _DATABASECoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + "", + 2, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + self:ScoreAdd( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _DATABASECoalition[self.Players[PlayerName].UnitCoalition], _DATABASECategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _DATABASECoalition[UnitCoalition], _DATABASECategory[UnitCategory], UnitData:getTypeName() ) + end + end + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName if self.Players[PlayerName].Penalty > 100 then if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - "", - 30, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() + MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + "", + 30, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 end end - + if self.Players[PlayerName].Penalty > 150 then ClientGroup = GROUP:NewFromDCSUnit( UnitData ) ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - "", - 10, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + "", + 10, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() end - - end + + end end --- Registers Scores the players completing a Mission Task. function DATABASE:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) + self:F( { PlayerUnit, MissionName, Score } ) - local PlayerName = PlayerUnit:getPlayerName() - - if not self.Players[PlayerName].Mission[MissionName] then - self.Players[PlayerName].Mission[MissionName] = {} - self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 - self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( self.Players[PlayerName].Mission[MissionName] ) + local PlayerName = PlayerUnit:getPlayerName() - self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score - self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + if not self.Players[PlayerName].Mission[MissionName] then + self.Players[PlayerName].Mission[MissionName] = {} + self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 + self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 + end - MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", + self:T( PlayerName ) + self:T( self.Players[PlayerName].Mission[MissionName] ) + + self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score + self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", "", 20, "/SCORETASK" .. PlayerName ):ToAll() _Database:ScoreAdd( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) @@ -5434,19 +5932,19 @@ end --- Registers Mission Scores for possible multiple players that contributed in the Mission. function DATABASE:_AddMissionScore( MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) + self:F( { PlayerUnit, MissionName, Score } ) - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerData.Mission[MissionName] then - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() - _Database:ScoreAdd( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end + for PlayerName, PlayerData in pairs( self.Players ) do + + if PlayerData.Mission[MissionName] then + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", + "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() + _Database:ScoreAdd( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end end @@ -5454,373 +5952,367 @@ end -- @section Events -function DATABASE:OnHit( event ) - self:F( { event } ) +--- Handles the OnHit event for the scoring. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnHit( Event ) + self:F( { Event } ) - local InitUnit = nil - local InitUnitName = "" - local InitGroupName = "" - local InitPlayerName = "dummy" + local InitUnit = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = "dummy" - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" + local TargetUnit = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil - if event.initiator and event.initiator:getName() then - - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - - InitUnit = event.initiator - InitGroup = Unit.getGroup( InitUnit ) - InitUnitDesc = InitUnit:getDesc() - - InitUnitName = InitUnit:getName() - if InitGroup and InitGroup:isExist() then - InitGroupName = InitGroup:getName() - end - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnitDesc.category -- Workaround - InitType = InitUnit:getTypeName() + if Event.IniDCSUnit then - InitUnitCoalition = DATABASECoalition[InitCoalition] - InitUnitCategory = DATABASECategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - self:T( { InitUnitDesc } ) - end + InitUnit = Event.IniDCSUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = InitUnit:getPlayerName() - - if event.target and Object.getCategory(event.target) == Object.Category.UNIT then - - TargetUnit = event.target - TargetGroup = Unit.getGroup( TargetUnit ) - TargetUnitDesc = TargetUnit:getDesc() - - TargetUnitName = TargetUnit:getName() - if TargetGroup and TargetGroup:isExist() then - TargetGroupName = TargetGroup:getName() - end - TargetPlayerName = TargetUnit:getPlayerName() + InitCoalition = InitUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + InitCategory = InitUnit:getDesc().category + InitType = InitUnit:getTypeName() - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnitDesc.category -- Workaround - TargetType = TargetUnit:getTypeName() + InitUnitCoalition = _DATABASECoalition[InitCoalition] + InitUnitCategory = _DATABASECategory[InitCategory] + InitUnitType = InitType - TargetUnitCoalition = DATABASECoalition[TargetCoalition] - TargetUnitCategory = DATABASECategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - self:T( { TargetUnitDesc } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreAdd( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/SCORE" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreAdd( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end - end + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _DATABASECoalition[TargetCoalition] + TargetUnitCategory = _DATABASECategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUnit ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUnit ) + self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 + end + + self:T( "Hitting Something" ) + -- What is he hitting? + if TargetCategory then + if not self.Players[InitPlayerName].Hit[TargetCategory] then + self.Players[InitPlayerName].Hit[TargetCategory] = {} + end + if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 + end + local Score = 0 + if InitCoalition == TargetCoalition then + self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreAdd( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/SCORE" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreAdd( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end end function DATABASE:ReportScoreAll() -env.info( "Hello World " ) + env.info( "Hello World " ) - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) + local ScoreMessage = "" + local PlayerMessage = "" - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) + self:T( "Score Report" ) - -- Some variables - local InitUnitCoalition = DATABASECoalition[PlayerData.UnitCoalition] - local InitUnitCategory = DATABASECategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + -- Some variables + local InitUnitCoalition = _DATABASECoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _DATABASECategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end + local PlayerScore = 0 + local PlayerPenalty = 0 - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask + ScoreMessage = ":\n" - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() end function DATABASE:ReportScorePlayer() -env.info( "Hello World " ) + env.info( "Hello World " ) - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) + local ScoreMessage = "" + local PlayerMessage = "" - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) + self:T( "Score Report" ) - -- Some variables - local InitUnitCoalition = DATABASECoalition[PlayerData.UnitCoalition] - local InitUnitCategory = DATABASECategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + -- Some variables + local InitUnitCoalition = _DATABASECoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _DATABASECategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end + local PlayerScore = 0 + local PlayerPenalty = 0 - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask + ScoreMessage = "" - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() end function DATABASE:ScoreMenu() - local ReportScore = SUBMENU:New( 'Scoring' ) - local ReportAllScores = COMMANDMENU:New( 'Score All Active Players', ReportScore, DATABASE.ReportScoreAll, self ) - local ReportPlayerScores = COMMANDMENU:New('Your Current Score', ReportScore, DATABASE.ReportScorePlayer, self ) + local ReportScore = SUBMENU:New( 'Scoring' ) + local ReportAllScores = COMMANDMENU:New( 'Score All Active Players', ReportScore, DATABASE.ReportScoreAll, self ) + local ReportPlayerScores = COMMANDMENU:New('Your Current Score', ReportScore, DATABASE.ReportScorePlayer, self ) end @@ -5829,102 +6321,116 @@ end -- File Logic for tracking the scores function DATABASE:SecondsToClock(sSeconds) -local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end end function DATABASE:ScoreOpen() - if lfs then - local fdir = lfs.writedir() .. [[Logs\]] .. "Player_Scores_" .. os.date( "%Y-%m-%d_%H-%M-%S" ) .. ".csv" - self.StatFile, self.err = io.open(fdir,"w+") - if not self.StatFile then - error( "Error: Cannot open 'Player Scores.csv' file in " .. lfs.writedir() ) - end - self.StatFile:write( '"RunID";"Time";"PlayerName";"ScoreType";"PlayerUnitCoaltion";"PlayerUnitCategory";"PlayerUnitType";"PlayerUnitName";"TargetUnitCoalition";"TargetUnitCategory";"TargetUnitType";"TargetUnitName";"Times";"Score"\n' ) - - self.RunID = os.date("%y-%m-%d_%H-%M-%S") - end + if lfs then + local fdir = lfs.writedir() .. [[Logs\]] .. "Player_Scores_" .. os.date( "%Y-%m-%d_%H-%M-%S" ) .. ".csv" + self.StatFile, self.err = io.open(fdir,"w+") + if not self.StatFile then + error( "Error: Cannot open 'Player Scores.csv' file in " .. lfs.writedir() ) + end + self.StatFile:write( '"RunID","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunID = os.date("%y-%m-%d_%H-%M-%S") + end end function DATABASE:ScoreAdd( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock(timer.getTime()) - PlayerName = PlayerName:gsub( '"', '_' ) + --write statistic information to file + local ScoreTime = self:SecondsToClock(timer.getTime()) + PlayerName = PlayerName:gsub( '"', '_' ) - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = DATABASECategory[PlayerUnit:getCategory()] - PlayerUnitCategory = DATABASECategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = DATABASECoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) - if lfs then - self.StatFile:write( '"' .. self.RunID .. '";' .. ScoreTime .. ';"' .. PlayerName .. '";"' .. ScoreType .. '";"' .. - PlayerUnitCoalition .. '";"' .. PlayerUnitCategory .. '";"' .. PlayerUnitType .. '";"' .. PlayerUnitName .. '";"' .. - TargetUnitCoalition .. '";"' .. TargetUnitCategory .. '";"' .. TargetUnitType .. '";"' .. TargetUnitName .. '";' .. - ScoreTimes .. ';' .. ScoreAmount ) - self.StatFile:write( "\n" ) - end + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = DATABASECategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _DATABASECategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _DATABASECoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + if not TargetUnitCoalition then + TargetUnitCoalition = '' + end + + if not TargetUnitCategory then + TargetUnitCategory = '' + end + + if not TargetUnitType then + TargetUnitType = '' + end + + if not TargetUnitName then + TargetUnitName = '' + end + + if lfs then + self.StatFile:write( + '"' .. self.RunID .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.StatFile:write( "\n" ) + end end - + function LogClose() - if lfs then - self.StatFile:close() - end + if lfs then + self.StatFile:close() + end end _Database = DATABASE:New() -- Database#DATABASE _Database:ScoreOpen() + --- CARGO Classes -- @module CARGO @@ -8579,7 +9085,7 @@ end --- Get progress of a TASK. -- @return string GoalsText function TASK:GetGoalProgress() - self:F() + self:F2() local GoalsText = "" for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do @@ -8603,7 +9109,7 @@ end -- @param MISSION Mission Group structure describing the Mission. -- @param CLIENT Client Group structure describing the Client. function TASK:ShowGoalProgress( Mission, Client ) - self:F() + self:F2() local GoalsText = "" for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do @@ -8625,14 +9131,14 @@ end --- Sets a TASK to status Done. function TASK:Done() - self:F() + self:F2() self.TaskDone = true end --- Returns if a TASK is done. -- @return bool function TASK:IsDone() - self:F( self.TaskDone ) + self:F2( self.TaskDone ) return self.TaskDone end @@ -8645,12 +9151,12 @@ end --- Returns if a TASk has failed. -- @return bool function TASK:IsFailed() - self:F( self.TaskFailed ) + self:F2( self.TaskFailed ) return self.TaskFailed end function TASK:Reset( Mission, Client ) - self:F() + self:F2() self.ExecuteStage = _TransportExecuteStage.NONE end @@ -8661,13 +9167,15 @@ function TASK:GetGoals() end --- Returns if a TASK has Goal(s). --- @param ?string GoalVerb is the name of the Goal of the TASK. +-- @param #TASK self +-- @param #string GoalVerb is the name of the Goal of the TASK. -- @return bool function TASK:Goal( GoalVerb ) - self:F() + self:F2( { GoalVerb } ) if not GoalVerb then GoalVerb = self.GoalVerb end + self:T2( {self.GoalTasks[GoalVerb] } ) if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then return true else @@ -8679,7 +9187,7 @@ end -- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. function TASK:SetGoalTotal( GoalTotal, GoalVerb ) - self:F( { GoalTotal, GoalVerb } ) + self:F2( { GoalTotal, GoalVerb } ) if not GoalVerb then GoalVerb = self.GoalVerb @@ -8694,7 +9202,7 @@ end --- Gets the total of Goals to be achieved within the TASK of the GoalVerb. -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. function TASK:GetGoalTotal( GoalVerb ) - self:F() + self:F2( { GoalVerb } ) if not GoalVerb then GoalVerb = self.GoalVerb end @@ -8710,7 +9218,7 @@ end -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -- @return TASK function TASK:SetGoalCount( GoalCount, GoalVerb ) - self:F() + self:F2() if not GoalVerb then GoalVerb = self.GoalVerb end @@ -8725,7 +9233,7 @@ end -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -- @return TASK function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) - self:F() + self:F2( { GoalCountIncrease, GoalVerb } ) if not GoalVerb then GoalVerb = self.GoalVerb end @@ -8739,7 +9247,7 @@ end -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -- @return TASK function TASK:GetGoalCount( GoalVerb ) - self:F() + self:F2() if not GoalVerb then GoalVerb = self.GoalVerb end @@ -8754,7 +9262,7 @@ end -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -- @return TASK function TASK:GetGoalPercentage( GoalVerb ) - self:F() + self:F2() if not GoalVerb then GoalVerb = self.GoalVerb end @@ -8767,15 +9275,16 @@ end --- Returns if all the Goals of the TASK were achieved. -- @return bool -function TASK:IsGoalReached( ) +function TASK:IsGoalReached() + self:F2() local GoalReached = true for GoalVerb, Goals in pairs( self.GoalTasks ) do - self:T( { "GoalVerb", GoalVerb } ) + self:T2( { "GoalVerb", GoalVerb } ) if self:Goal( GoalVerb ) then local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) - self:T( "GoalToDo = " .. GoalToDo ) + self:T2( "GoalToDo = " .. GoalToDo ) if GoalToDo <= 0 then else GoalReached = false @@ -8786,7 +9295,7 @@ function TASK:IsGoalReached( ) end end - self:T( GoalReached ) + self:T( { GoalReached, self.GoalTasks } ) return GoalReached end @@ -8795,7 +9304,7 @@ end -- @param string GoalTask is a text describing the Goal of the TASK to be achieved. -- @param number GoalIncrease is a number by which the Goal achievement is increasing. function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) - self:F( { GoalVerb, GoalTask, GoalIncrease } ) + self:F2( { GoalVerb, GoalTask, GoalIncrease } ) if self:Goal( GoalVerb ) then self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask @@ -8808,7 +9317,7 @@ end -- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -- @return string Goals function TASK:GetGoalCompletion( GoalVerb ) - self:F( { GoalVerb } ) + self:F2( { GoalVerb } ) if self:Goal( GoalVerb ) then local Goals = "" @@ -9001,10 +9510,11 @@ DESTROYBASETASK = { } --- Creates a new DESTROYBASETASK. --- @param string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". --- @param string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". --- @param table{string,...} DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. --- @param ?number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. +-- @param #DESTROYBASETASK self +-- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". +-- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". +-- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. +-- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. -- @return DESTROYBASETASK function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) local self = BASE:Inherit( self, TASK:New() ) @@ -9015,34 +9525,31 @@ function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPre self.DestroyGroupPrefixes = DestroyGroupPrefixes self.DestroyGroupType = DestroyGroupType self.DestroyUnitType = DestroyUnitType + if DestroyPercentage then + self.DestroyPercentage = DestroyPercentage + end self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } self.SetStage( self, 1 ) - --self.AddEvent( self, world.event.S_EVENT_DEAD, self.EventDead ) - - --env.info( 'New Table self = ' .. tostring(self) ) - --env.info( 'New Table self = ' .. tostring(self) ) - return self end --- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. --- @param event Event structure of DCS world. -function DESTROYBASETASK:EventDead( event ) - self:F( { 'EventDead', event } ) +-- @param #DESTROYBASETASK self +-- @param Event#EVENTDATA Event structure of MOOSE. +function DESTROYBASETASK:EventDead( Event ) + self:F( { Event } ) - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - local DestroyUnit = event.initiator - local DestroyUnitName = DestroyUnit:getName() - local DestroyGroup = Unit.getGroup( DestroyUnit ) - local DestroyGroupName = "" - if DestroyGroup and DestroyGroup:isExist() then - local DestroyGroupName = DestroyGroup:getName() - end + if Event.IniDCSUnit then + local DestroyUnit = Event.IniDCSUnit + local DestroyUnitName = Event.IniDCSUnitName + local DestroyGroup = Event.IniDCSGroup + local DestroyGroupName = Event.IniDCSGroupName + + --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! + --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... local UnitsDestroyed = 0 - self:T( DestroyGroupName ) - self:T( DestroyUnitName ) for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do self:T( DestroyGroupPrefix ) if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then @@ -9055,6 +9562,7 @@ function DESTROYBASETASK:EventDead( event ) self:T( { UnitsDestroyed } ) self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) end + end --- Validate task completeness of DESTROYBASETASK. @@ -9078,10 +9586,11 @@ DESTROYGROUPSTASK = { } --- Creates a new DESTROYGROUPSTASK. --- @param string DestroyGroupType String describing the group to be destroyed. --- @param string DestroyUnitType String describing the unit to be destroyed. --- @param table{string,...} DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. --- @param ?number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. +-- @param #DESTROYGROUPSTASK self +-- @param #string DestroyGroupType String describing the group to be destroyed. +-- @param #string DestroyUnitType String describing the unit to be destroyed. +-- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. +-- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. ---@return DESTROYGROUPSTASK function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) @@ -9090,35 +9599,35 @@ function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupN self.Name = 'Destroy Groups' self.GoalVerb = "Destroy " .. DestroyGroupType - self:AddEvent( world.event.S_EVENT_DEAD, self.EventDead ) - self:AddEvent( world.event.S_EVENT_CRASH, self.EventDead ) - --Child.AddEvent( Child, world.event.S_EVENT_PILOT_DEAD, Child.EventDead ) + _EventDispatcher:OnDead( self.EventDead , self ) + _EventDispatcher:OnCrash( self.EventDead , self ) return self end --- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. +-- @param #DESTROYGROUPSTASK self +-- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. +-- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. +-- @return #number The DestroyCount reflecting the amount of units destroyed within the group. function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - self:T( DestroyGroup:getSize() ) + self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) + + local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. + local DestroyGroupInitialSize = DestroyGroup:getInitialSize() + self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) local DestroyCount = 0 if DestroyGroup then - if ( ( DestroyGroup:getInitialSize() * self.DestroyPercentage ) / 100 ) - DestroyGroup:getSize() <= 0 then + if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then DestroyCount = 1 ---[[ else - if DestroyGroup:getSize() == 1 then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - DestroyCount = 1 - end - end - ]] end + end else DestroyCount = 1 end + self:T( DestroyCount ) + return DestroyCount end --- Task class to destroy radar installations. @@ -9141,8 +9650,8 @@ function DESTROYRADARSTASK:New( DestroyGroupNames ) self:F() self.Name = 'Destroy Radars' - - self:AddEvent( world.event.S_EVENT_DEAD, self.EventDead ) + + _EventDispatcher:OnDead( self.EventDead , self ) return self end @@ -9193,7 +9702,7 @@ function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGro self.Name = 'Destroy Unit Types' self.GoalVerb = "Destroy " .. DestroyGroupType - self:AddEvent( world.event.S_EVENT_DEAD, self.EventDead ) + _EventDispatcher:OnDead( self.EventDead , self ) return self end @@ -10271,6 +10780,7 @@ Include.File( "Task" ) --- The CLEANUP class. -- @type CLEANUP +-- @extends Base#BASE CLEANUP = { ClassName = "CLEANUP", ZoneNames = {}, @@ -10301,15 +10811,8 @@ function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, self.TimeInterval = TimeInterval end - self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) - self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) - self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) - self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) - --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) - self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) + _EventDispatcher:OnBirth( self._OnEventBirth, self ) - self:EnableEvents() - self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) return self @@ -10345,9 +10848,8 @@ function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) local CleanUpGroupUnits = CleanUpGroup:getUnits() if #CleanUpGroupUnits == 1 then local CleanUpGroupName = CleanUpGroup:getName() - local Event = {["initiator"]=CleanUpUnit,["id"]=8} - world.onEvent( Event ) - trigger.action.deactivateGroup( CleanUpGroup ) + --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) + CleanUpGroup:destroy() self:T( { "Destroyed Group:", CleanUpGroupName } ) else CleanUpUnit:destroy() @@ -10372,13 +10874,43 @@ function CLEANUP:_DestroyMissile( MissileObject ) end end +function CLEANUP:_OnEventBirth( Event ) + self:F( { Event } ) + + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + + _EventDispatcher:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EventDispatcher:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EventDispatcher:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EventDispatcher:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EventDispatcher:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EventDispatcher:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EventDispatcher:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) + + --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) + --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) +-- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) +-- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) +-- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) +-- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) +-- +-- self:EnableEvents() + + +end + --- Detects if a crash event occurs. -- Crashed units go into a CleanUpList for removal. -- @param #CLEANUP self -- @param DCSTypes#Event event -function CLEANUP:_EventCrash( event ) - self:F( { event } ) +function CLEANUP:_EventCrash( Event ) + self:F( { Event } ) + --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. --MESSAGE:New( "Crash ", "Crash", 10, "Crash" ):ToAll() -- self:T("before getGroup") -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired @@ -10387,44 +10919,28 @@ function CLEANUP:_EventCrash( event ) -- self:T("after deactivateGroup") -- event.initiator:destroy() - local CleanUpUnit = event.initiator -- the Unit - local CleanUpUnitName = CleanUpUnit:getName() -- return the name of the Unit - local CleanUpGroup = Unit.getGroup(CleanUpUnit)-- Identify the Group - local CleanUpGroupName = "" - if CleanUpGroup and CleanUpGroup:isExist() then - CleanUpGroupName = CleanUpGroup:getName() -- return the name of the Group - end - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroupName - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + end --- Detects if a unit shoots a missile. -- If this occurs within one of the zones, then the weapon used must be destroyed. -- @param #CLEANUP self -- @param DCSTypes#Event event -function CLEANUP:_EventShot( event ) - self:F( { event } ) +function CLEANUP:_EventShot( Event ) + self:F( { Event } ) - local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - local _groupname = _grp:getName() -- return the name of the group - local _unittable = {event.initiator:getName()} -- return the name of the units in the group - local _SEADmissile = event.weapon -- Identify the weapon fired - --local _SEADmissileName = _SEADmissile:getTypeName() -- return weapon type - --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(_SEADmissileName)), 20) --debug message - -- Start of the 2nd loop - --self:T( "Missile Launched = " .. _SEADmissileName ) - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( event.initiator, self.ZoneNames ) + CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) if ( CurrentLandingZoneID ) then -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. --_SEADmissile:destroy() - routines.scheduleFunction( CLEANUP._DestroyMissile, {self, _SEADmissile}, timer.getTime() + 0.1) + routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) end end @@ -10432,38 +10948,28 @@ end --- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. -- @param #CLEANUP self -- @param DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( event ) - self:F( { event } ) +function CLEANUP:_EventHitCleanUp( Event ) + self:F( { Event } ) - local CleanUpUnit = event.initiator -- the Unit - if CleanUpUnit and CleanUpUnit:isExist() and Object.getCategory(CleanUpUnit) == Object.Category.UNIT then - local CleanUpUnitName = event.initiator:getName() -- return the name of the Unit - - if routines.IsUnitInZones( CleanUpUnit, self.ZoneNames ) ~= nil then - self:T( "Life: " .. CleanUpUnitName .. ' = ' .. CleanUpUnit:getLife() .. "/" .. CleanUpUnit:getLife0() ) - if CleanUpUnit:getLife() < CleanUpUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. CleanUpUnitName ) - routines.scheduleFunction( CLEANUP._DestroyUnit, {self, CleanUpUnit}, timer.getTime() + 0.1) + if Event.IniDCSUnit then + if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then + self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) + if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then + self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) + routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) end end end - local CleanUpTgtUnit = event.target -- the target Unit - if CleanUpTgtUnit and CleanUpTgtUnit:isExist() and Object.getCategory(CleanUpTgtUnit) == Object.Category.UNIT then - local CleanUpTgtUnitName = event.target:getName() -- return the name of the target Unit - local CleanUpTgtGroup = Unit.getGroup(event.target)-- Identify the target Group - local CleanUpTgtGroupName = CleanUpTgtGroup:getName() -- return the name of the target Group - - - if routines.IsUnitInZones( CleanUpTgtUnit, self.ZoneNames ) ~= nil then - self:T( "Life: " .. CleanUpTgtUnitName .. ' = ' .. CleanUpTgtUnit:getLife() .. "/" .. CleanUpTgtUnit:getLife0() ) - if CleanUpTgtUnit:getLife() < CleanUpTgtUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. CleanUpTgtUnitName ) - routines.scheduleFunction( CLEANUP._DestroyUnit, {self, CleanUpTgtUnit}, timer.getTime() + 0.1) + if Event.TgtDCSUnit then + if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then + self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) + if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then + self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) + routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) end end end - end --- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. @@ -10485,24 +10991,20 @@ end --- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. -- @param #CLEANUP self -- @param DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( event ) +function CLEANUP:_EventAddForCleanUp( Event ) - local CleanUpUnit = event.initiator -- the Unit - if CleanUpUnit and Object.getCategory(CleanUpUnit) == Object.Category.UNIT then - local CleanUpUnitName = CleanUpUnit:getName() -- return the name of the Unit - if self.CleanUpList[CleanUpUnitName] == nil then - if routines.IsUnitInZones( CleanUpUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) + if Event.IniDCSUnit then + if self.CleanUpList[Event.IniDCSUnitName] == nil then + if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then + self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) end end end - local CleanUpTgtUnit = event.target -- the target Unit - if CleanUpTgtUnit and Object.getCategory(CleanUpTgtUnit) == Object.Category.UNIT then - local CleanUpTgtUnitName = CleanUpTgtUnit:getName() -- return the name of the target Unit - if self.CleanUpList[CleanUpTgtUnitName] == nil then - if routines.IsUnitInZones( CleanUpTgtUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( CleanUpTgtUnit, CleanUpTgtUnitName ) + if Event.TgtDCSUnit then + if self.CleanUpList[Event.TgtDCSUnitName] == nil then + if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then + self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) end end end @@ -10520,9 +11022,11 @@ local CleanUpSurfaceTypeText = { --- At the defined time interval, CleanUp the Groups within the CleanUpList. -- @param #CLEANUP self function CLEANUP:_CleanUpScheduler() - self:F( "CleanUp Scheduler" ) + self:F( { "CleanUp Scheduler" } ) + local CleanUpCount = 0 for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do + CleanUpCount = CleanUpCount + 1 self:T( { CleanUpUnitName, UnitData } ) local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) @@ -10584,6 +11088,7 @@ function CLEANUP:_CleanUpScheduler() self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE end end + self:T(CleanUpCount) end --- Dynamic spawning of groups (and units). @@ -10663,6 +11168,7 @@ Include.File( "Base" ) Include.File( "Database" ) Include.File( "Group" ) Include.File( "Zone" ) +Include.File( "Event" ) --- SPAWN Class -- @type SPAWN @@ -10709,12 +11215,6 @@ function SPAWN:New( SpawnTemplatePrefix ) else error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) end - - self:AddEvent( world.event.S_EVENT_BIRTH, self._OnBirth ) - self:AddEvent( world.event.S_EVENT_DEAD, self._OnDeadOrCrash ) - self:AddEvent( world.event.S_EVENT_CRASH, self._OnDeadOrCrash ) - - self:EnableEvents() return self end @@ -10753,12 +11253,6 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) end - self:AddEvent( world.event.S_EVENT_BIRTH, self._OnBirth ) - self:AddEvent( world.event.S_EVENT_DEAD, self._OnDeadOrCrash ) - self:AddEvent( world.event.S_EVENT_CRASH, self._OnDeadOrCrash ) - - self:EnableEvents() - return self end @@ -10870,17 +11364,12 @@ end -- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. -- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() function SPAWN:Repeat() - self:F( { self.SpawnTemplatePrefix } ) + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) self.SpawnRepeat = true self.RepeatOnEngineShutDown = false self.RepeatOnLanding = true - self:AddEvent( world.event.S_EVENT_LAND, self._OnLand ) - self:AddEvent( world.event.S_EVENT_TAKEOFF, self._OnTakeOff ) - self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._OnEngineShutDown ) - self:EnableEvents() - return self end @@ -10973,6 +11462,21 @@ function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true self.SpawnGroups[SpawnGroupID].Visible = true + + _EventDispatcher:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) + _EventDispatcher:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) + _EventDispatcher:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.SpawnRepeat then + _EventDispatcher:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) + end + if self.RepeatOnLanding then + _EventDispatcher:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EventDispatcher:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) + end + self.SpawnGroups[SpawnGroupID].Group = _Database:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) SpawnX = SpawnXIndex * SpawnDeltaX @@ -11031,6 +11535,20 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) self.SpawnGroups[self.SpawnIndex].Group:Activate() else self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + _EventDispatcher:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) + _EventDispatcher:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) + _EventDispatcher:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.SpawnRepeat then + _EventDispatcher:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) + end + if self.RepeatOnLanding then + _EventDispatcher:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EventDispatcher:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) + end + self.SpawnGroups[self.SpawnIndex].Group = _Database:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) -- If there is a SpawnFunction hook defined, call it. @@ -11046,7 +11564,7 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) self.SpawnGroups[self.SpawnIndex].Spawned = true return self.SpawnGroups[self.SpawnIndex].Group else - self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) end return nil @@ -11885,11 +12403,11 @@ function MOVEMENT:New( MovePrefixes, MoveMaximum ) self.AliveUnits = 0 -- Contains the counter how many units are currently alive self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) - self:AddEvent( world.event.S_EVENT_DEAD, self.OnDeadOrCrash ) - self:AddEvent( world.event.S_EVENT_CRASH, self.OnDeadOrCrash ) + _EventDispatcher:OnBirth( self.OnBirth, self ) - self:EnableEvents() +-- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) +-- +-- self:EnableEvents() self:ScheduleStart() @@ -11911,43 +12429,39 @@ end --- Captures the birth events when new Units were spawned. -- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnBirth( event ) - self:F( { event } ) +function MOVEMENT:OnBirth( Event ) + self:F( { Event } ) if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - local MovementUnit = event.initiator - local MovementUnitName = MovementUnit:getName() - self:T( "Birth object : " .. MovementUnitName ) - local MovementGroup = MovementUnit:getGroup() - if MovementGroup and MovementGroup:isExist() then - local MovementGroupName = MovementGroup:getName() + if Event.IniDCSUnit then + self:T( "Birth object : " .. Event.IniDCSUnitName ) + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( MovementUnitName, MovePrefix, 1, true ) then + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[MovementUnitName] = MovementGroupName + self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName self:T( self.AliveUnits ) end end end end + _EventDispatcher:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) + _EventDispatcher:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) end end --- Captures the Dead or Crash events when Units crash or are destroyed. -- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnDeadOrCrash( event ) - self:F( { event } ) +function MOVEMENT:OnDeadOrCrash( Event ) + self:F( { Event } ) - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - local MovementUnit = event.initiator - local MovementUnitName = MovementUnit:getName() - self:T( "Dead object : " .. MovementUnitName ) + if Event.IniDCSUnit then + self:T( "Dead object : " .. Event.IniDCSUnitName ) for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( MovementUnitName, MovePrefix, 1, true ) then + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then self.AliveUnits = self.AliveUnits - 1 - self.MoveUnits[MovementUnitName] = nil + self.MoveUnits[Event.IniDCSUnitName] = nil self:T( self.AliveUnits ) end end @@ -11986,6 +12500,7 @@ end -- @author (co) Flightcontrol (Modified and enriched with functionality) Include.File( "Routines" ) +Include.File( "Event" ) Include.File( "Base" ) Include.File( "Mission" ) Include.File( "Client" ) @@ -12024,27 +12539,26 @@ function SEAD:New( SEADGroupPrefixes ) else self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes end - self:AddEvent( world.event.S_EVENT_SHOT, self.EventShot ) - self:EnableEvents() + _EventDispatcher:OnShot( self.EventShot, self ) return self end --- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. -- @see SEAD -function SEAD:EventShot( event ) - self:F( { event } ) +function SEAD:EventShot( Event ) + self:F( { Event } ) - local SEADUnit = event.initiator - local SEADUnitName = SEADUnit:getName() - local SEADWeapon = event.weapon -- Identify the weapon fired - local SEADWeaponName = SEADWeapon:getTypeName() -- return weapon type + local SEADUnit = Event.IniDCSUnit + local SEADUnitName = Event.IniDCSUnitName + local SEADWeapon = Event.Weapon -- Identify the weapon fired + local SEADWeaponName = Event.WeaponName -- return weapon type --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message -- Start of the 2nd loop self:T( "Missile Launched = " .. SEADWeaponName ) if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = Weapon.getTarget(SEADWeapon) -- Identify target + local _targetMim = Event.Weapon:getTarget() -- Identify target local _targetMimname = Unit.getName(_targetMim) local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) local _targetMimgroupName = _targetMimgroup:getName() @@ -12120,71 +12634,83 @@ end --- Taking the lead of AI escorting your flight. -- The ESCORT class allows you to interact with escorting AI on your flight and take the lead. -- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). --- +-- -- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. -- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- +-- -- Find a summary below of the current available commands: --- +-- -- **1. Navigation ...:** Escort group navigation functions: --- --- * **"Hold Position and Stay Low":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"Join-Up and Hold Position NearBy":** The escort group will stop nearby you, and then the group will hover. --- * **"Join-Up and Follow at 100":** The escort group fill follow you at about 100 meters, and they will follow you. --- * **"Join-Up and Follow at 200":** The escort group fill follow you at about 200 meters, and they will follow you. --- * **"Join-Up and Follow at 400":** The escort group fill follow you at about 400 meters, and they will follow you. --- * **"Join-Up and Follow at 800":** The escort group fill follow you at about 800 meters, and they will follow you. +-- +-- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. -- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. -- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. --- --- **2. Report targets ...:** Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). --- +-- +-- **2. Hold position ...:** Escort group navigation functions: +-- +-- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- +-- **3. Report targets ...:** Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). +-- -- * **"Report now":** Will report the current detected targets. -- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. -- * **"Report targets off":** Will stop detecting targets. --- --- **3. Scan targets ...:** Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- +-- +-- **4. Scan targets ...:** Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. +-- -- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. -- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- --- **4. Attack targets ...:** This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. --- --- **5. Request assistance from ...:** This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. +-- +-- **5. Attack targets ...:** This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- +-- **6. Request assistance from ...:** This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. -- This menu item allows to request attack support from other escorts supporting the current client group. -- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. -- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. --- --- **6. ROE ...:** Defines the Rules of Engagement of the escort group when in flight. --- +-- +-- **7. ROE ...:** Defines the Rules of Engagement of the escort group when in flight. +-- -- * **"Hold Fire":** The escort group will hold fire. -- * **"Return Fire":** The escort group will return fire. -- * **"Open Fire":** The escort group will open fire on designated targets. -- * **"Weapon Free":** The escort group will engage with any target. --- --- **7. Evasion ...:** Will define the evasion techniques that the escort group will perform during flight or combat. --- +-- +-- **8. Evasion ...:** Will define the evasion techniques that the escort group will perform during flight or combat. +-- -- * **"Fight until death":** The escort group will have no reaction to threats. -- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. -- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. -- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. --- --- **8. Resume Mission ...:** Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- +-- **9. Resume Mission ...:** Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. -- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. --- --- **9. Abort Current Task:** Cancel the current task and rejoin formation. --- +-- +-- **10. Abort Current Task:** Cancel the current task and rejoin formation. +-- -- 1. ESCORT object construction methods. -- -------------------------------------- -- Create a new SPAWN object with the @{#ESCORT.New} method: --- +-- -- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. --- +-- -- 2. ESCORT object initialization methods. -- ---------------------------------------- --- None. --- --- +-- The following menus can be setup: +-- +-- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. +-- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. +-- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. +-- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. +-- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. +-- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. +-- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. +-- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. +-- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. +-- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. +-- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. +-- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. +-- -- @module Escort -- @author FlightControl @@ -12201,6 +12727,7 @@ Include.File( "Zone" ) -- @field Client#CLIENT EscortClient -- @field Group#GROUP EscortGroup -- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. -- @field #number FollowScheduler The id of the _FollowScheduler function. -- @field #boolean ReportTargets If true, nearby targets are reported. -- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. @@ -12211,6 +12738,11 @@ ESCORT = { EscortName = nil, -- The Escort Name EscortClient = nil, EscortGroup = nil, + EscortMode = nil, + MODE = { + FOLLOW = 1, + MISSION = 2, + }, Targets = {}, -- The identified targets FollowScheduler = nil, ReportTargets = true, @@ -12219,6 +12751,11 @@ ESCORT = { TaskPoints = {} } +--- ESCORT.Mode class +-- @type ESCORT.MODE +-- @field #number FOLLOW +-- @field #number MISSION + --- MENUPARAM type -- @type MENUPARAM -- @field #ESCORT ParamSelf @@ -12234,127 +12771,465 @@ ESCORT = { -- @return #ESCORT self function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - + self:F( { EscortClient, EscortGroup, EscortName } ) + self.EscortClient = EscortClient -- Client#CLIENT self.EscortGroup = EscortGroup -- Group#GROUP self.EscortName = EscortName self.EscortBriefing = EscortBriefing - + self:T( EscortGroup:GetClassNameAndID() ) - + -- Set EscortGroup known at EscortClient. if not self.EscortClient._EscortGroups then - self.EscortClient._EscortGroups = {} + self.EscortClient._EscortGroups = {} end if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then - self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} - + self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} + self.EscortMode = ESCORT.MODE.FOLLOW end - - + + self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) - - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - if EscortGroup:IsHelicopter() or EscortGroup:IsAirPlane() then - -- Escort Navigation - self.EscortMenuHoldPosition = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Position and Stay Low", self.EscortMenuReportNavigation, ESCORT._HoldPosition, { ParamSelf = self } ) - self.EscortMenuJoinUpAndHoldPosition = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Hold Position NearBy", self.EscortMenuReportNavigation, ESCORT._HoldPositionNearBy, { ParamSelf = self } ) - self.EscortMenuJoinUpAndFollow50Meters = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at 100", self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = 100 } ) - self.EscortMenuJoinUpAndFollow100Meters = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at 200", self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = 200 } ) - self.EscortMenuJoinUpAndFollow150Meters = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at 400", self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = 400 } ) - self.EscortMenuJoinUpAndFollow200Meters = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at 800", self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = 800 } ) - end - self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, "Flare", self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) - if EscortGroup:IsGround() or EscortGroup:IsShip() then - self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) + self.EscortGroup:WayPointInitialize(1) + + self.EscortGroup:OptionROTVertical() + self.EscortGroup:OptionROEOpenFire() + + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. + "We're escorting your flight. " .. + "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", + 60, EscortClient + ) + + return self +end + + +--- Defines the default menus +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:Menus() + self:F() + + self:MenuFollowAt( 100 ) + self:MenuFollowAt( 200 ) + self:MenuFollowAt( 300 ) + self:MenuFollowAt( 400 ) + + self:MenuScanForTargets( 100, 60 ) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtLeaderPosition( 30 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuEvasion() + self:MenuResumeMission() + + return self +end + + + +--- Defines a menu slot to let the escort Join and Follow you at a certain distance. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. +-- @return #ESCORT +function ESCORT:MenuFollowAt( Distance ) + self:F(Distance) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + if not self.EscortMenuJoinUpAndFollow then + self.EscortMenuJoinUpAndFollow = {} + end + + self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) + + self.EscortMode = ESCORT.MODE.FOLLOW end - - if EscortGroup:IsHelicopter() or EscortGroup:IsAirPlane() or EscortGroup:IsGround() or EscortGroup:IsShip() then - -- Report Targets + + return self +end + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldPosition then + self.EscortMenuHoldPosition = {} + end + + self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortGroup, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldAtLeaderPosition then + self.EscortMenuHoldAtLeaderPosition = {} + end + + self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortClient, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuScan, + ESCORT._ScanTargets, + { ParamSelf = self, + ParamScanDuration = 30 + } + ) + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuFlare then + self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) + self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) + self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) + self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) + self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) + end + + return self +end + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + if not self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuSmoke then + self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) + self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) + self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) + self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) + self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) + self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) + end + end + + return self +end + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #ESCORT self +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #ESCORT +function ESCORT:MenuReportTargets( Seconds ) + self:F( { Seconds } ) + + if not self.EscortMenuReportNearbyTargets then self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) - self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) end - if EscortGroup:IsHelicopter() then - -- Scanning Targets - self.EscortMenuScanForTargets = MENU_CLIENT:New( self.EscortClient, "Scan targets", self.EscortMenu ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Scan targets 30 seconds", self.EscortMenuScanForTargets, ESCORT._ScanTargets, { ParamSelf = self, ParamScanDuration = 30 } ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Scan targets 60 seconds", self.EscortMenuScanForTargets, ESCORT._ScanTargets, { ParamSelf = self, ParamScanDuration = 60 } ) + if not Seconds then + Seconds = 30 end - + + -- Report Targets + self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) + self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) + self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) + -- Attack Targets self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) - -- Request assistance from other escorts. + + self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuReportTargets. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuAssistedAttack() + self:F() + + -- Request assistance from other escorts. -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) - - -- Rules of Engagement - self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) - if EscortGroup:OptionROEHoldFirePossible() then - self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) - end - if EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) - end - if EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) - end - if EscortGroup:OptionROEWeaponFreePossible() then - self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) + return self +end + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuROE( MenuTextFormat ) + self:F( MenuTextFormat ) + + if not self.EscortMenuROE then + -- Rules of Engagement + self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) + if self.EscortGroup:OptionROEHoldFirePossible() then + self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) + end + if self.EscortGroup:OptionROEReturnFirePossible() then + self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) + end + if self.EscortGroup:OptionROEOpenFirePossible() then + self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) + end + if self.EscortGroup:OptionROEWeaponFreePossible() then + self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) + end end - -- Reaction to Threats - self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) - if EscortGroup:OptionROTNoReactionPossible() then - self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) - end - if EscortGroup:OptionROTPassiveDefensePossible() then - self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) - end - if EscortGroup:OptionROTEvadeFirePossible() then - self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) - end - if EscortGroup:OptionROTVerticalPossible() then - self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) - end - - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume the escort mission", self.EscortMenu ) + return self +end - -- Initialize the EscortGroup - - self.EscortGroup:WayPointInitialize(1) - - self.EscortGroup:OptionROTVertical() - self.EscortGroup:OptionROEOpenFire() - - - self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) - - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuEvasion( MenuTextFormat ) + self:F( MenuTextFormat ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuEvasion then + -- Reaction to Threats + self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) + if self.EscortGroup:OptionROTNoReactionPossible() then + self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) + end + if self.EscortGroup:OptionROTPassiveDefensePossible() then + self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) + end + if self.EscortGroup:OptionROTEvadeFirePossible() then + self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) + end + if self.EscortGroup:OptionROTVerticalPossible() then + self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) + end + end + end + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuResumeMission() + self:F() + + if not self.EscortMenuResumeMission then + -- Mission Resume Menu Root + self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) + end + + return self end @@ -12364,48 +13239,41 @@ function ESCORT._HoldPosition( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - - routines.removeFunction( self.FollowScheduler ) - EscortGroup:SetTask( EscortGroup:TaskHoldPosition( 300 ) ) - EscortGroup:MessageToClient( "Holding Position.", 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._HoldPositionNearBy( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - --MenuParam.ParamSelf.EscortGroup:TaskOrbitCircleAtVec2( MenuParam.ParamSelf.EscortClient:GetPointVec2(), 300, 30, 0 ) + local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP + local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT + local OrbitHeight = MenuParam.ParamHeight + local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet routines.removeFunction( self.FollowScheduler ) - + local PointFrom = {} - local GroupPoint = EscortGroup:GetPointVec2() + local GroupPoint = EscortGroup:GetUnit(1):GetPositionVec3() PointFrom = {} PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y + PointFrom.y = GroupPoint.z PointFrom.speed = 250 PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = EscortClient:GetAltitude() + PointFrom.alt = GroupPoint.y PointFrom.alt_type = AI.Task.AltitudeType.BARO - local ClientPoint = MenuParam.ParamSelf.EscortClient:GetPointVec2() + local OrbitPoint = OrbitUnit:GetPointVec2() local PointTo = {} - PointTo.x = ClientPoint.x - PointTo.y = ClientPoint.y + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y PointTo.speed = 250 PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = EscortClient:GetAltitude() + PointTo.alt = OrbitHeight PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( EscortClient:GetPointVec2(), 300, 30, 0 ) - + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + local Points = { PointFrom, PointTo } - + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Rejoining to your location. Please hold at your location.", 10, EscortClient ) + EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) end --- @param #MENUPARAM MenuParam @@ -12414,10 +13282,10 @@ function ESCORT._JoinUpAndFollow( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - - local Distance = MenuParam.ParamDistance - - self:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) + + self.Distance = MenuParam.ParamDistance + + self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) end --- JoinsUp and Follows a CLIENT. @@ -12431,10 +13299,12 @@ function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) if self.FollowScheduler then routines.removeFunction( self.FollowScheduler ) end - + EscortGroup:OptionROEHoldFire() EscortGroup:OptionROTPassiveDefense() - + + self.EscortMode = ESCORT.MODE.FOLLOW + self.CT1 = 0 self.GT1 = 0 self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + 1, .5 ) @@ -12447,7 +13317,7 @@ function ESCORT._Flare( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - + local Color = MenuParam.ParamColor local Message = MenuParam.ParamMessage @@ -12464,7 +13334,7 @@ function ESCORT._Smoke( MenuParam ) local Color = MenuParam.ParamColor local Message = MenuParam.ParamMessage - + EscortGroup:GetUnit(1):Smoke( Color ) EscortGroup:MessageToClient( Message, 10, EscortClient ) end @@ -12478,7 +13348,7 @@ function ESCORT._ReportNearbyTargetsNow( MenuParam ) local EscortClient = self.EscortClient self:_ReportTargetsScheduler() - + end function ESCORT._SwitchReportNearbyTargets( MenuParam ) @@ -12486,9 +13356,9 @@ function ESCORT._SwitchReportNearbyTargets( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - + self.ReportTargets = MenuParam.ParamReportTargets - + if self.ReportTargets then if not self.ReportTargetsScheduler then self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) @@ -12505,45 +13375,56 @@ function ESCORT._ScanTargets( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - + local ScanDuration = MenuParam.ParamScanDuration if self.FollowScheduler then routines.removeFunction( self.FollowScheduler ) end - + self:T( { "FollowScheduler after removefunction: ", self.FollowScheduler } ) - + if EscortGroup:IsHelicopter() then routines.scheduleFunction( EscortGroup.PushTask, - { EscortGroup, - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - timer.getTime() + 1 - ) + { EscortGroup, + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ) + }, + timer.getTime() + 1 + ) elseif EscortGroup:IsAirPlane() then routines.scheduleFunction( EscortGroup.PushTask, - { EscortGroup, - EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - timer.getTime() + 1 - ) - end - + { EscortGroup, + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ) + }, + timer.getTime() + 1 + ) + end + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - if self.FollowScheduler then + if self.EscortMode == ESCORT.MODE.FOLLOW then self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + ScanDuration, 1 ) end end +function _Resume( EscortGroup ) + env.info( '_Resume' ) + + local Escort = EscortGroup.Escort -- #ESCORT + env.info( "EscortMode = " .. Escort.EscortMode ) + if Escort.EscortMode == ESCORT.MODE.FOLLOW then + Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) + end + +end + --- @param #MENUPARAM MenuParam function ESCORT._AttackTarget( MenuParam ) @@ -12555,40 +13436,37 @@ function ESCORT._AttackTarget( MenuParam ) if self.FollowScheduler then routines.removeFunction( self.FollowScheduler ) end - + self:T( AttackUnit ) - + if EscortGroup:IsAir() then EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTVertical() - routines.scheduleFunction( - EscortGroup.PushTask, - { EscortGroup, + EscortGroup:OptionROTPassiveDefense() + EscortGroup.Escort = self -- Need to do this trick to get the reference for the escort in the _Resume function. + routines.scheduleFunction( + EscortGroup.PushTask, + { EscortGroup, EscortGroup:TaskCombo( { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskOrbitCircle( 500, 350 ) + EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) } ) }, timer.getTime() + 10 ) else - routines.scheduleFunction( - EscortGroup.PushTask, - { EscortGroup, + routines.scheduleFunction( + EscortGroup.PushTask, + { EscortGroup, EscortGroup:TaskCombo( { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) } ) - }, timer.getTime() + 10 + }, timer.getTime() + 10 ) - end + end EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - if self.FollowScheduler then - self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + ScanDuration, 1 ) - end - end --- @param #MENUPARAM MenuParam @@ -12603,34 +13481,34 @@ function ESCORT._AssistTarget( MenuParam ) if self.FollowScheduler then routines.removeFunction( self.FollowScheduler ) end - - + + self:T( AttackUnit ) - + if EscortGroupAttack:IsAir() then EscortGroupAttack:OptionROEOpenFire() EscortGroupAttack:OptionROTVertical() - routines.scheduleFunction( - EscortGroupAttack.PushTask, - { EscortGroupAttack, + routines.scheduleFunction( + EscortGroupAttack.PushTask, + { EscortGroupAttack, EscortGroupAttack:TaskCombo( { EscortGroupAttack:TaskAttackUnit( AttackUnit ), EscortGroupAttack:TaskOrbitCircle( 500, 350 ) } ) - }, timer.getTime() + 10 + }, timer.getTime() + 10 ) else - routines.scheduleFunction( - EscortGroupAttack.PushTask, - { EscortGroupAttack, + routines.scheduleFunction( + EscortGroupAttack.PushTask, + { EscortGroupAttack, EscortGroupAttack:TaskCombo( { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) } ) - }, timer.getTime() + 10 + }, timer.getTime() + 10 ) - end + end EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) end @@ -12641,10 +13519,10 @@ function ESCORT._ROE( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - + local EscortROEFunction = MenuParam.ParamFunction local EscortROEMessage = MenuParam.ParamMessage - + pcall( function() EscortROEFunction() end ) EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) end @@ -12669,21 +13547,21 @@ function ESCORT._ResumeMission( MenuParam ) local self = MenuParam.ParamSelf local EscortGroup = self.EscortGroup local EscortClient = self.EscortClient - + local WayPoint = MenuParam.ParamWayPoint - + routines.removeFunction( self.FollowScheduler ) self.FollowScheduler = nil local WayPoints = EscortGroup:GetTaskRoute() self:T( WayPoint, WayPoints ) - + for WayPointIgnore = 1, WayPoint do table.remove( WayPoints, 1 ) end - + routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) - + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) end @@ -12694,9 +13572,9 @@ function ESCORT:RegisterRoute() self:F() local EscortGroup = self.EscortGroup -- Group#GROUP - + local TaskPoints = EscortGroup:GetTaskRoute() - + self:T( TaskPoints ) return TaskPoints @@ -12707,7 +13585,7 @@ function ESCORT:_FollowScheduler( FollowDistance ) self:F( { FollowDistance }) if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - + local ClientUnit = self.EscortClient:GetClientGroupUnit() local GroupUnit = self.EscortGroup:GetUnit( 1 ) @@ -12723,70 +13601,70 @@ function ESCORT:_FollowScheduler( FollowDistance ) local CV2 = ClientUnit:GetPositionVec3() self.CT1 = CT2 self.CV1 = CV2 - + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 local CT = CT2 - CT1 - + local CS = ( 3600 / CT ) * ( CD / 1000 ) - + self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - + local GT1 = self.GT1 local GT2 = timer.getTime() local GV1 = self.GV1 local GV2 = GroupUnit:GetPositionVec3() self.GT1 = GT2 self.GV1 = GV2 - + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 local GT = GT2 - GT1 - + local GS = ( 3600 / GT ) * ( GD / 1000 ) - + self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - + -- Calculate the group direction vector local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - + -- Calculate GH2, GH2 with the same height as CV2. local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - + -- Calculate the angle of GV to the orthonormal plane local alpha = math.atan2( GV.z, GV.x ) - + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - + local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), + y = GH2.y, + z = CV2.z + FollowDistance * math.sin(alpha), + } + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - + -- Now we can calculate the group destination vector GDV. local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } self:T2( { "CV2:", CV2 } ) self:T2( { "CVI:", CVI } ) self:T2( { "GDV:", GDV } ) - + -- Measure distance between client and group local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5 - - -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome + + -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome -- the requested Distance). local Time = 10 - local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time - + local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time + local Speed = CS + CatchUpSpeed - if Speed < 0 then + if Speed < 0 then Speed = 0 - end + end self:T( { "Client Speed, Escort Speed, Speed, FlyDistance, Time:", CS, GS, Speed, Distance, Time } ) @@ -12803,90 +13681,93 @@ end --- Report Targets Scheduler. -- @param #ESCORT self function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) + self:F( self.EscortGroup:GetName() ) if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then local EscortGroupName = self.EscortGroup:GetName() local EscortTargets = self.EscortGroup:GetDetectedTargets() - + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - + local EscortTargetMessages = "" for EscortTargetID, EscortTarget in pairs( EscortTargets ) do local EscortObject = EscortTarget.object self:T( EscortObject ) if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:New( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - --- local EscortTargetIsDetected, --- EscortTargetIsVisible, --- EscortTargetLastTime, --- EscortTargetKnowType, --- EscortTargetKnowDistance, --- EscortTargetLastPos, --- EscortTargetLastVelocity --- = self.EscortGroup:IsTargetDetected( EscortObject ) --- --- self:T( { EscortTargetIsDetected, --- EscortTargetIsVisible, --- EscortTargetLastTime, --- EscortTargetKnowType, --- EscortTargetKnowDistance, --- EscortTargetLastPos, --- EscortTargetLastVelocity } ) - - local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPositionVec3() - local EscortPositionVec3 = self.EscortGroup:GetPositionVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 + local EscortTargetUnit = UNIT:New( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - if Distance <= 15 then - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPositionVec3() + local EscortPositionVec3 = self.EscortGroup:GetPositionVec3() + local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + + ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + + ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) + + if Distance <= 15 then + + if not ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = {} end + ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit + ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible + ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type + ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance + else + if ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = nil + end + end end end - + self:T( { "Sorting Targets Table:", ClientEscortTargets } ) table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) self:T( { "Sorted Targets Table:", ClientEscortTargets } ) -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - self.EscortMenuTargetAssistance:RemoveSubMenus() - + + if self.EscortMenuTargetAssistance then + self.EscortMenuTargetAssistance:RemoveSubMenus() + end + --for MenuIndex = 1, #self.EscortMenuAttackTargets do -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() --end - + if ClientEscortTargets then for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - + for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - + if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - + local EscortTargetMessage = "" local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() @@ -12895,54 +13776,56 @@ function ESCORT:_ReportTargetsScheduler() else EscortTargetMessage = EscortTargetMessage .. "Unknown target at " end - + local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPositionVec3() local EscortPositionVec3 = self.EscortGroup:GetPositionVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - + local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + + ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + + ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) if ClientEscortTargetData.visible == false then EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" else EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" end - + if ClientEscortTargetData.visible then EscortTargetMessage = EscortTargetMessage .. ", visual" end - + if ClientEscortGroupName == EscortGroupName then - + MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - { ParamSelf = self, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage + EscortTargetMessage, + self.EscortMenuAttackNearbyTargets, + ESCORT._AttackTarget, + { ParamSelf = self, + ParamUnit = ClientEscortTargetData.AttackUnit + } + ) + EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage else - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - MenuTargetAssistance, - ESCORT._AssistTarget, - { ParamSelf = self, - ParamEscortGroup = EscortGroupData.EscortGroup, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) + if self.EscortMenuTargetAssistance then + local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) + MENU_CLIENT_COMMAND:New( self.EscortClient, + EscortTargetMessage, + MenuTargetAssistance, + ESCORT._AssistTarget, + { ParamSelf = self, + ParamEscortGroup = EscortGroupData.EscortGroup, + ParamUnit = ClientEscortTargetData.AttackUnit + } + ) + end end else ClientEscortTargetData = nil end end end - + if EscortTargetMessages ~= "" and self.ReportTargets == true then self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) else @@ -12950,22 +13833,24 @@ function ESCORT:_ReportTargetsScheduler() end end - self.EscortMenuResumeMission:RemoveSubMenus() - --- if self.EscortMenuResumeWayPoints then --- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do --- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) --- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() --- end --- end + if self.EscortMenuResumeMission then + self.EscortMenuResumeMission:RemoveSubMenus() - local TaskPoints = self:RegisterRoute() - for WayPointID, WayPoint in pairs( TaskPoints ) do - local EscortPositionVec3 = self.EscortGroup:GetPositionVec3() - local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + - ( WayPoint.y - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) + -- if self.EscortMenuResumeWayPoints then + -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do + -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) + -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() + -- end + -- end + + local TaskPoints = self:RegisterRoute() + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortPositionVec3 = self.EscortGroup:GetPositionVec3() + local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + + ( WayPoint.y - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) + end end else diff --git a/Moose/CleanUp.lua b/Moose/CleanUp.lua index ac28d7738..236f63e14 100644 --- a/Moose/CleanUp.lua +++ b/Moose/CleanUp.lua @@ -10,6 +10,7 @@ Include.File( "Task" ) --- The CLEANUP class. -- @type CLEANUP +-- @extends Base#BASE CLEANUP = { ClassName = "CLEANUP", ZoneNames = {}, @@ -77,9 +78,8 @@ function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) local CleanUpGroupUnits = CleanUpGroup:getUnits() if #CleanUpGroupUnits == 1 then local CleanUpGroupName = CleanUpGroup:getName() - local Event = {["initiator"]=CleanUpUnit,["id"]=8} - world.onEvent( Event ) - trigger.action.deactivateGroup( CleanUpGroup ) + --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) + CleanUpGroup:destroy() self:T( { "Destroyed Group:", CleanUpGroupName } ) else CleanUpUnit:destroy() @@ -116,6 +116,8 @@ function CLEANUP:_OnEventBirth( Event ) _EventDispatcher:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) _EventDispatcher:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) _EventDispatcher:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EventDispatcher:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EventDispatcher:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) _EventDispatcher:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) _EventDispatcher:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) @@ -135,9 +137,10 @@ end -- Crashed units go into a CleanUpList for removal. -- @param #CLEANUP self -- @param DCSTypes#Event event -function CLEANUP:_EventCrash( event ) - self:F( { event } ) +function CLEANUP:_EventCrash( Event ) + self:F( { Event } ) + --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. --MESSAGE:New( "Crash ", "Crash", 10, "Crash" ):ToAll() -- self:T("before getGroup") -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired @@ -151,6 +154,7 @@ function CLEANUP:_EventCrash( event ) self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + end --- Detects if a unit shoots a missile. @@ -248,9 +252,11 @@ local CleanUpSurfaceTypeText = { --- At the defined time interval, CleanUp the Groups within the CleanUpList. -- @param #CLEANUP self function CLEANUP:_CleanUpScheduler() - self:F( "CleanUp Scheduler" ) + self:F( { "CleanUp Scheduler" } ) + local CleanUpCount = 0 for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do + CleanUpCount = CleanUpCount + 1 self:T( { CleanUpUnitName, UnitData } ) local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) @@ -312,5 +318,6 @@ function CLEANUP:_CleanUpScheduler() self.CleanUpList[CleanUpUnitName] = nil -- Not anymore in the DCSRTE end end + self:T(CleanUpCount) end diff --git a/Moose/Database.lua b/Moose/Database.lua index a05b31fea..58d2a5011 100644 --- a/Moose/Database.lua +++ b/Moose/Database.lua @@ -13,31 +13,31 @@ Include.File( "Event" ) -- @type DATABASE -- @extends Base#BASE DATABASE = { - ClassName = "DATABASE", - Units = {}, - Groups = {}, - NavPoints = {}, - Statics = {}, - Players = {}, - ActivePlayers = {}, - ClientsByName = {}, - ClientsByID = {}, + ClassName = "DATABASE", + Units = {}, + Groups = {}, + NavPoints = {}, + Statics = {}, + Players = {}, + ActivePlayers = {}, + ClientsByName = {}, + ClientsByID = {}, } -DATABASECoalition = -{ - [1] = "Red", - [2] = "Blue", -} +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + } -DATABASECategory = -{ - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", -} +local _DATABASECategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } --- Creates a new DATABASE Object to administer the Groups defined and alive within the DCSRTE. @@ -47,80 +47,79 @@ DATABASECategory = -- DBObject = DATABASE:New() function DATABASE:New() - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - self.Navpoints = {} - self.Units = {} - --Build routines.db.units and self.Navpoints - for coa_name, coa_data in pairs(env.mission.coalition) do + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[coa_name] = {} - if coa_data.nav_points then --navpoints - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - self.Navpoints[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) + self.Navpoints = {} + self.Units = {} + --Build routines.db.units and self.Navpoints + for coa_name, coa_data in pairs(env.mission.coalition) do - self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 - self.Navpoints[coa_name][nav_ind]['point']['z'] = nav_data.y - end - end - end - ------------------------------------------------- - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local countryName = string.lower(cntry_data.name) - self.Units[coa_name][countryName] = {} - self.Units[coa_name][countryName]["countryId"] = cntry_data.id + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + self.Units[coa_name] = {} - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local category = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - self.Units[coa_name][countryName][category] = {} - - for group_num, GroupTemplate in pairs(obj_type_data.group) do - - if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group - self:_RegisterGroup( GroupTemplate ) - end --if GroupTemplate and GroupTemplate.units then - end --for group_num, GroupTemplate in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do + ---------------------------------------------- + -- build nav points DB + self.Navpoints[coa_name] = {} + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do - --self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) - _EventDispatcher:OnDead( self.OnDeadOrCrash, self ) - _EventDispatcher:OnCrash( self.OnDeadOrCrash, self ) - _EventDispatcher:OnHit( self.OnHit, self ) - - self.SchedulerId = routines.scheduleFunction( DATABASE._FollowPlayers, { self }, 0, 5 ) - - self:ScoreMenu() - - - return self + if type(nav_data) == 'table' then + self.Navpoints[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 + self.Navpoints[coa_name][nav_ind]['point']['z'] = nav_data.y + end + end + end + ------------------------------------------------- + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + + local countryName = string.lower(cntry_data.name) + self.Units[coa_name][countryName] = {} + self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local category = obj_type_name + + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + + self.Units[coa_name][countryName][category] = {} + + for group_num, GroupTemplate in pairs(obj_type_data.group) do + + if GroupTemplate and GroupTemplate.units and type(GroupTemplate.units) == 'table' then --making sure again- this is a valid group + self:_RegisterGroup( GroupTemplate ) + end --if GroupTemplate and GroupTemplate.units then + end --for group_num, GroupTemplate in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do + + --self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) + _EventDispatcher:OnDead( self._EventOnDeadOrCrash, self ) + _EventDispatcher:OnCrash( self._EventOnDeadOrCrash, self ) + _EventDispatcher:OnHit( self._EventOnHit, self ) + + self.SchedulerId = routines.scheduleFunction( DATABASE._FollowPlayers, { self }, 0, 5 ) + + self:ScoreMenu() + + return self end @@ -130,49 +129,49 @@ end -- This method is used by the SPAWN class. function DATABASE:Spawn( SpawnTemplate ) - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID, SpawnTemplate.name } ) - - -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. - local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID - local SpawnCountryID = SpawnTemplate.SpawnCountryID - local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - -- Nullify - SpawnTemplate.SpawnCoalitionID = nil - SpawnTemplate.SpawnCountryID = nil - SpawnTemplate.SpawnCategoryID = nil - - self:_RegisterGroup( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID, SpawnTemplate.name } ) - -- Restore - SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID - SpawnTemplate.SpawnCountryID = SpawnCountryID - SpawnTemplate.SpawnCategoryID = SpawnCategoryID + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID + local SpawnCountryID = SpawnTemplate.SpawnCountryID + local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - local SpawnGroup = GROUP:New( Group.getByName( SpawnTemplate.name ) ) - return SpawnGroup + -- Nullify + SpawnTemplate.SpawnCoalitionID = nil + SpawnTemplate.SpawnCountryID = nil + SpawnTemplate.SpawnCategoryID = nil + + self:_RegisterGroup( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID + SpawnTemplate.SpawnCountryID = SpawnCountryID + SpawnTemplate.SpawnCategoryID = SpawnCategoryID + + + local SpawnGroup = GROUP:New( Group.getByName( SpawnTemplate.name ) ) + return SpawnGroup end --- Set a status to a Group within the Database, this to check crossing events for example. function DATABASE:SetStatusGroup( GroupName, Status ) - self:F( Status ) + self:F( Status ) - self.Groups[GroupName].Status = Status + self.Groups[GroupName].Status = Status end --- Get a status to a Group within the Database, this to check crossing events for example. function DATABASE:GetStatusGroup( GroupName ) - self:F( Status ) + self:F( Status ) - if self.Groups[GroupName] then - return self.Groups[GroupName].Status - else - return "" - end + if self.Groups[GroupName] then + return self.Groups[GroupName].Status + else + return "" + end end @@ -183,35 +182,35 @@ end --- Registers new Group Templates within the DATABASE Object. function DATABASE:_RegisterGroup( GroupTemplate ) - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) + local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - if not self.Groups[GroupTemplateName] then - self.Groups[GroupTemplateName] = {} - self.Groups[GroupTemplateName].Status = nil - end - self.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Groups[GroupTemplateName].Template = GroupTemplate - self.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Groups[GroupTemplateName].Units = GroupTemplate.units - - self:T( { "Group", self.Groups[GroupTemplateName].GroupName, self.Groups[GroupTemplateName].UnitCount } ) - - for unit_num, UnitTemplate in pairs(GroupTemplate.units) do - - local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) - self.Units[UnitTemplateName] = {} - self.Units[UnitTemplateName].UnitName = UnitTemplateName - self.Units[UnitTemplateName].Template = UnitTemplate - self.Units[UnitTemplateName].GroupName = GroupTemplateName - self.Units[UnitTemplateName].GroupTemplate = GroupTemplate - self.Units[UnitTemplateName].GroupId = GroupTemplate.groupId - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.ClientsByName[UnitTemplateName] = UnitTemplate - self.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - self:T( { "Unit", self.Units[UnitTemplateName].UnitName } ) - end + if not self.Groups[GroupTemplateName] then + self.Groups[GroupTemplateName] = {} + self.Groups[GroupTemplateName].Status = nil + end + self.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Groups[GroupTemplateName].Template = GroupTemplate + self.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Groups[GroupTemplateName].Units = GroupTemplate.units + + self:T( { "Group", self.Groups[GroupTemplateName].GroupName, self.Groups[GroupTemplateName].UnitCount } ) + + for unit_num, UnitTemplate in pairs(GroupTemplate.units) do + + local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) + self.Units[UnitTemplateName] = {} + self.Units[UnitTemplateName].UnitName = UnitTemplateName + self.Units[UnitTemplateName].Template = UnitTemplate + self.Units[UnitTemplateName].GroupName = GroupTemplateName + self.Units[UnitTemplateName].GroupTemplate = GroupTemplate + self.Units[UnitTemplateName].GroupId = GroupTemplate.groupId + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.ClientsByName[UnitTemplateName] = UnitTemplate + self.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + self:T( { "Unit", self.Units[UnitTemplateName].UnitName } ) + end end @@ -220,96 +219,94 @@ end --- Track DCSRTE DEAD or CRASH events for the internal scoring. -function DATABASE:OnDeadOrCrash( event ) - self:F( { event } ) +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - - TargetUnit = event.initiator - TargetGroup = Unit.getGroup( TargetUnit ) - TargetUnitDesc = TargetUnit:getDesc() - - TargetUnitName = TargetUnit:getName() - if TargetGroup and TargetGroup:isExist() then - TargetGroupName = TargetGroup:getName() - end - TargetPlayerName = TargetUnit:getPlayerName() + if Event.IniDCSUnit then - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnitDesc.category -- Workaround - TargetType = TargetUnit:getTypeName() + TargetUnit = Event.IniDCSUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() - TargetUnitCoalition = DATABASECoalition[TargetCoalition] - TargetUnitCategory = DATABASECategory[TargetCategory] - TargetUnitType = TargetType + TargetCoalition = TargetUnit:getCoalition() + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetType = TargetUnit:getTypeName() - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end + TargetUnitCoalition = _DATABASECoalition[TargetCoalition] + TargetUnitCategory = _DATABASECategory[T1argetCategory] + TargetUnitType = TargetType - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = DATABASECoalition[InitCoalition] - local InitUnitCategory = DATABASECategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got killed" ) - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end + -- Some variables + local InitUnitName = PlayerData.UnitName + local InitUnitType = PlayerData.UnitType + local InitCoalition = PlayerData.UnitCoalition + local InitCategory = PlayerData.UnitCategory + local InitUnitCoalition = _DATABASECoalition[InitCoalition] + local InitUnitCategory = _DATABASECategory[InitCategory] - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreAdd( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreAdd( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is he hitting? + if TargetCategory then + if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + if not PlayerData.Kill[TargetCategory] then + PlayerData.Kill[TargetCategory] = {} + end + if not PlayerData.Kill[TargetCategory][TargetType] then + PlayerData.Kill[TargetCategory][TargetType] = {} + PlayerData.Kill[TargetCategory][TargetType].Score = 0 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 + PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 + end + + if InitCoalition == TargetCoalition then + PlayerData.Penalty = PlayerData.Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreAdd( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + PlayerData.Score = PlayerData.Score + 10 + PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreAdd( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + end + end end @@ -319,20 +316,20 @@ end --- Follows new players entering Clients within the DCSRTE. function DATABASE:_FollowPlayers() - self:F3( "_FollowPlayers" ) + self:F3( "_FollowPlayers" ) - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayers", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end + local ClientUnit = 0 + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } + local unitId + local unitData + local AlivePlayerUnits = {} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "_FollowPlayers", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:_AddPlayerFromUnit( UnitData ) + end + end end @@ -342,102 +339,102 @@ end --- Add a new player entering a Unit. function DATABASE:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) + self:F( UnitData ) - if UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() + if UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + local UnitDesc = UnitData:getDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:getCoalition() + local UnitTypeName = UnitData:getTypeName() - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( DATABASECategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Kill = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( DATABASECategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Kill[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].HitUnits = {} self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. DATABASECoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. DATABASECoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - "", - 2, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - self:ScoreAdd( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, DATABASECoalition[self.Players[PlayerName].UnitCoalition], DATABASECategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, DATABASECoalition[UnitCoalition], DATABASECategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _DATABASECoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _DATABASECoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + "", + 2, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + self:ScoreAdd( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _DATABASECoalition[self.Players[PlayerName].UnitCoalition], _DATABASECategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _DATABASECoalition[UnitCoalition], _DATABASECategory[UnitCategory], UnitData:getTypeName() ) + end + end + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName if self.Players[PlayerName].Penalty > 100 then if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - "", - 30, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() + MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + "", + 30, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 end end - + if self.Players[PlayerName].Penalty > 150 then ClientGroup = GROUP:NewFromDCSUnit( UnitData ) ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - "", - 10, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + "", + 10, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() end - - end + + end end --- Registers Scores the players completing a Mission Task. function DATABASE:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) + self:F( { PlayerUnit, MissionName, Score } ) - local PlayerName = PlayerUnit:getPlayerName() - - if not self.Players[PlayerName].Mission[MissionName] then - self.Players[PlayerName].Mission[MissionName] = {} - self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 - self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( self.Players[PlayerName].Mission[MissionName] ) + local PlayerName = PlayerUnit:getPlayerName() - self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score - self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + if not self.Players[PlayerName].Mission[MissionName] then + self.Players[PlayerName].Mission[MissionName] = {} + self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 + self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 + end - MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", + self:T( PlayerName ) + self:T( self.Players[PlayerName].Mission[MissionName] ) + + self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score + self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", "", 20, "/SCORETASK" .. PlayerName ):ToAll() _Database:ScoreAdd( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) @@ -446,19 +443,19 @@ end --- Registers Mission Scores for possible multiple players that contributed in the Mission. function DATABASE:_AddMissionScore( MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) + self:F( { PlayerUnit, MissionName, Score } ) - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerData.Mission[MissionName] then - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() - _Database:ScoreAdd( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end + for PlayerName, PlayerData in pairs( self.Players ) do + + if PlayerData.Mission[MissionName] then + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", + "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() + _Database:ScoreAdd( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end end @@ -466,373 +463,367 @@ end -- @section Events -function DATABASE:OnHit( event ) - self:F( { event } ) +--- Handles the OnHit event for the scoring. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnHit( Event ) + self:F( { Event } ) - local InitUnit = nil - local InitUnitName = "" - local InitGroupName = "" - local InitPlayerName = "dummy" + local InitUnit = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = "dummy" - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" + local TargetUnit = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil - if event.initiator and event.initiator:getName() then - - if event.initiator and Object.getCategory(event.initiator) == Object.Category.UNIT then - - InitUnit = event.initiator - InitGroup = Unit.getGroup( InitUnit ) - InitUnitDesc = InitUnit:getDesc() - - InitUnitName = InitUnit:getName() - if InitGroup and InitGroup:isExist() then - InitGroupName = InitGroup:getName() - end - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnitDesc.category -- Workaround - InitType = InitUnit:getTypeName() + if Event.IniDCSUnit then - InitUnitCoalition = DATABASECoalition[InitCoalition] - InitUnitCategory = DATABASECategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - self:T( { InitUnitDesc } ) - end + InitUnit = Event.IniDCSUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = InitUnit:getPlayerName() - - if event.target and Object.getCategory(event.target) == Object.Category.UNIT then - - TargetUnit = event.target - TargetGroup = Unit.getGroup( TargetUnit ) - TargetUnitDesc = TargetUnit:getDesc() - - TargetUnitName = TargetUnit:getName() - if TargetGroup and TargetGroup:isExist() then - TargetGroupName = TargetGroup:getName() - end - TargetPlayerName = TargetUnit:getPlayerName() + InitCoalition = InitUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + InitCategory = InitUnit:getDesc().category + InitType = InitUnit:getTypeName() - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnitDesc.category -- Workaround - TargetType = TargetUnit:getTypeName() + InitUnitCoalition = _DATABASECoalition[InitCoalition] + InitUnitCategory = _DATABASECategory[InitCategory] + InitUnitType = InitType - TargetUnitCoalition = DATABASECoalition[TargetCoalition] - TargetUnitCategory = DATABASECategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - self:T( { TargetUnitDesc } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreAdd( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/SCORE" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreAdd( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end - end + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _DATABASECoalition[TargetCoalition] + TargetUnitCategory = _DATABASECategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUnit ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUnit ) + self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 + end + + self:T( "Hitting Something" ) + -- What is he hitting? + if TargetCategory then + if not self.Players[InitPlayerName].Hit[TargetCategory] then + self.Players[InitPlayerName].Hit[TargetCategory] = {} + end + if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 + end + local Score = 0 + if InitCoalition == TargetCoalition then + self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreAdd( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/SCORE" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreAdd( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end end function DATABASE:ReportScoreAll() -env.info( "Hello World " ) + env.info( "Hello World " ) - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) + local ScoreMessage = "" + local PlayerMessage = "" - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) + self:T( "Score Report" ) - -- Some variables - local InitUnitCoalition = DATABASECoalition[PlayerData.UnitCoalition] - local InitUnitCategory = DATABASECategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + -- Some variables + local InitUnitCoalition = _DATABASECoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _DATABASECategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end + local PlayerScore = 0 + local PlayerPenalty = 0 - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask + ScoreMessage = ":\n" - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() end function DATABASE:ReportScorePlayer() -env.info( "Hello World " ) + env.info( "Hello World " ) - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) + local ScoreMessage = "" + local PlayerMessage = "" - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) + self:T( "Score Report" ) - -- Some variables - local InitUnitCoalition = DATABASECoalition[PlayerData.UnitCoalition] - local InitUnitCategory = DATABASECategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( DATABASECategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + -- Some variables + local InitUnitCoalition = _DATABASECoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _DATABASECategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end + local PlayerScore = 0 + local PlayerPenalty = 0 - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask + ScoreMessage = "" - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _DATABASECategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() end function DATABASE:ScoreMenu() - local ReportScore = SUBMENU:New( 'Scoring' ) - local ReportAllScores = COMMANDMENU:New( 'Score All Active Players', ReportScore, DATABASE.ReportScoreAll, self ) - local ReportPlayerScores = COMMANDMENU:New('Your Current Score', ReportScore, DATABASE.ReportScorePlayer, self ) + local ReportScore = SUBMENU:New( 'Scoring' ) + local ReportAllScores = COMMANDMENU:New( 'Score All Active Players', ReportScore, DATABASE.ReportScoreAll, self ) + local ReportPlayerScores = COMMANDMENU:New('Your Current Score', ReportScore, DATABASE.ReportScorePlayer, self ) end @@ -841,112 +832,113 @@ end -- File Logic for tracking the scores function DATABASE:SecondsToClock(sSeconds) -local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end end function DATABASE:ScoreOpen() - if lfs then - local fdir = lfs.writedir() .. [[Logs\]] .. "Player_Scores_" .. os.date( "%Y-%m-%d_%H-%M-%S" ) .. ".csv" - self.StatFile, self.err = io.open(fdir,"w+") - if not self.StatFile then - error( "Error: Cannot open 'Player Scores.csv' file in " .. lfs.writedir() ) - end - self.StatFile:write( '"RunID","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunID = os.date("%y-%m-%d_%H-%M-%S") - end + if lfs then + local fdir = lfs.writedir() .. [[Logs\]] .. "Player_Scores_" .. os.date( "%Y-%m-%d_%H-%M-%S" ) .. ".csv" + self.StatFile, self.err = io.open(fdir,"w+") + if not self.StatFile then + error( "Error: Cannot open 'Player Scores.csv' file in " .. lfs.writedir() ) + end + self.StatFile:write( '"RunID","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunID = os.date("%y-%m-%d_%H-%M-%S") + end end function DATABASE:ScoreAdd( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock(timer.getTime()) - PlayerName = PlayerName:gsub( '"', '_' ) + --write statistic information to file + local ScoreTime = self:SecondsToClock(timer.getTime()) + PlayerName = PlayerName:gsub( '"', '_' ) - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = DATABASECategory[PlayerUnit:getCategory()] - PlayerUnitCategory = DATABASECategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = DATABASECoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) - if lfs then - self.StatFile:write( - '"' .. self.RunID .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.StatFile:write( "\n" ) - end + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = DATABASECategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _DATABASECategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _DATABASECoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + if not TargetUnitCoalition then + TargetUnitCoalition = '' + end + + if not TargetUnitCategory then + TargetUnitCategory = '' + end + + if not TargetUnitType then + TargetUnitType = '' + end + + if not TargetUnitName then + TargetUnitName = '' + end + + if lfs then + self.StatFile:write( + '"' .. self.RunID .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.StatFile:write( "\n" ) + end end - + function LogClose() - if lfs then - self.StatFile:close() - end + if lfs then + self.StatFile:close() + end end _Database = DATABASE:New() -- Database#DATABASE _Database:ScoreOpen() + diff --git a/Moose/Event.lua b/Moose/Event.lua index 6ad91e3cf..a3501b44c 100644 --- a/Moose/Event.lua +++ b/Moose/Event.lua @@ -13,7 +13,7 @@ EVENT = { ClassID = 0, } -local EVENTCODES = { +local _EVENTCODES = { "S_EVENT_SHOT", "S_EVENT_HIT", "S_EVENT_TAKEOFF", @@ -69,6 +69,13 @@ function EVENT:New() return self end +function EVENT:EventText( EventID ) + + local EventText = _EVENTCODES[EventID] + + return EventText +end + --- Initializes the Events structure for the event -- @param #EVENT self @@ -76,16 +83,13 @@ end -- @param #string EventClass -- @return #EVENT.Events function EVENT:Init( EventID, EventClass ) - self:F( { EventID, EventClass } ) + self:F3( { _EVENTCODES[EventID], EventClass } ) if not self.Events[EventID] then self.Events[EventID] = {} end if not self.Events[EventID][EventClass] then self.Events[EventID][EventClass] = {} end - if not self.Events[EventID][EventClass].IniUnit then - self.Events[EventID][EventClass].IniUnit = {} - end return self.Events[EventID][EventClass] end @@ -98,7 +102,7 @@ end -- @param #function OnEventFunction -- @return #EVENT function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) - self:F( EventTemplate ) + self:F2( EventTemplate.name ) for EventUnitID, EventUnit in pairs( EventTemplate.units ) do OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) @@ -113,7 +117,7 @@ end -- @param EventID -- @return #EVENT function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) - self:F( { EventID } ) + self:F2( { EventID } ) local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) Event.EventFunction = EventFunction @@ -130,9 +134,12 @@ end -- @param EventID -- @return #EVENT function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + if not Event.IniUnit then + Event.IniUnit = {} + end Event.IniUnit[EventDCSUnitName] = {} Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf @@ -147,9 +154,11 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( { EventTemplate } ) + self:F( EventTemplate.name ) - return self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) + + return self end --- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. @@ -160,7 +169,9 @@ end function EVENT:OnBirth( EventFunction, EventSelf ) self:F() - return self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self end --- Set a new listener for an S_EVENT_BIRTH event. @@ -172,7 +183,9 @@ end function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self end --- Create an OnCrash event handler for a group @@ -182,9 +195,11 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate ) + self:F( EventTemplate.name ) - return self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) + + return self end --- Set a new listener for an S_EVENT_CRASH event. @@ -195,7 +210,9 @@ end function EVENT:OnCrash( EventFunction, EventSelf ) self:F() - return self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self end --- Set a new listener for an S_EVENT_CRASH event. @@ -206,8 +223,10 @@ end -- @return #EVENT function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + return self end --- Create an OnDead event handler for a group @@ -217,9 +236,11 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate ) + self:F( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) - return self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) + return self end --- Set a new listener for an S_EVENT_DEAD event. @@ -230,7 +251,9 @@ end function EVENT:OnDead( EventFunction, EventSelf ) self:F() - return self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self end @@ -243,7 +266,23 @@ end function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + +--- Set a new listener for an S_EVENT_PILOT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) + + return self end --- Create an OnDead event handler for a group @@ -253,9 +292,11 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate ) + self:F( EventTemplate.name ) - return self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) + + return self end --- Set a new listener for an S_EVENT_LAND event. @@ -267,7 +308,9 @@ end function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) + + return self end --- Create an OnDead event handler for a group @@ -277,9 +320,11 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate ) + self:F( EventTemplate.name ) - return self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) + + return self end --- Set a new listener for an S_EVENT_TAKEOFF event. @@ -291,7 +336,9 @@ end function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) + + return self end --- Create an OnDead event handler for a group @@ -301,9 +348,11 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate ) + self:F( EventTemplate.name ) - return self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) + + return self end --- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. @@ -315,7 +364,9 @@ end function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self end --- Set a new listener for an S_EVENT_ENGINE_STARTUP event. @@ -327,7 +378,9 @@ end function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) + + return self end --- Set a new listener for an S_EVENT_SHOT event. @@ -338,7 +391,9 @@ end function EVENT:OnShot( EventFunction, EventSelf ) self:F() - return self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self end --- Set a new listener for an S_EVENT_SHOT event for a unit. @@ -350,7 +405,9 @@ end function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self end --- Set a new listener for an S_EVENT_HIT event. @@ -361,7 +418,9 @@ end function EVENT:OnHit( EventFunction, EventSelf ) self:F() - return self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self end --- Set a new listener for an S_EVENT_HIT event. @@ -373,13 +432,15 @@ end function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) self:F( EventDCSUnitName ) - return self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self end function EVENT:onEvent( Event ) - self:F( { EVENTCODES[Event.id], Event } ) + self:F( { _EVENTCODES[Event.id], Event } ) if self and self.Events and self.Events[Event.id] then if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then @@ -405,15 +466,15 @@ function EVENT:onEvent( Event ) if Event.weapon then Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() - Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() end for ClassName, EventData in pairs( self.Events[Event.id] ) do if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - self:T( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) + self:T2( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) else if Event.IniDCSUnit and not EventData.IniUnit then - self:T( { "Calling event function for class ", ClassName } ) + self:T2( { "Calling event function for class ", ClassName } ) EventData.EventFunction( EventData.EventSelf, Event ) end end diff --git a/Moose/Sead.lua b/Moose/Sead.lua index 2bbfd31b8..8c1ddbc6b 100644 --- a/Moose/Sead.lua +++ b/Moose/Sead.lua @@ -62,7 +62,7 @@ function SEAD:EventShot( Event ) self:T( "Missile Launched = " .. SEADWeaponName ) if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = Event.WeaponTgtDCSUnit -- Identify target + local _targetMim = Event.Weapon:getTarget() -- Identify target local _targetMimname = Unit.getName(_targetMim) local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) local _targetMimgroupName = _targetMimgroup:getName() diff --git a/Moose/Spawn.lua b/Moose/Spawn.lua index f6659d7e1..9ad27e90e 100644 --- a/Moose/Spawn.lua +++ b/Moose/Spawn.lua @@ -471,7 +471,7 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) self.SpawnGroups[self.SpawnIndex].Spawned = true return self.SpawnGroups[self.SpawnIndex].Group else - self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) end return nil diff --git a/Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.lua b/Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.lua new file mode 100644 index 000000000..1066f424a --- /dev/null +++ b/Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.lua @@ -0,0 +1,10 @@ +Include.File( 'Cleanup' ) +Include.File( 'Spawn' ) +Include.File( 'Event') + +Clean = CLEANUP:New( 'CLEAN_BATUMI', 180 ) + +SpawnRU = SPAWN:New( 'RU Attack Heli Batumi'):Limit( 2, 20 ):SpawnScheduled( 2, 0.2 ) + +SpawnUS = SPAWN:New( 'US Attack Heli Batumi'):Limit( 2, 20 ):SpawnScheduled( 2, 0.2 ) + diff --git a/Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz b/Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz new file mode 100644 index 0000000000000000000000000000000000000000..2627bceb207f01f015f504e0c685d50e43a5c25d GIT binary patch literal 99751 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
~W~aO9RcYxc_Pl*3vx0b9;#b^I6}wwOg^8 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 literal 0 HcmV?d00001