mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2026-07-17 07:33:56 +00:00
Merge remote-tracking branch 'origin/master' into develop
# Conflicts: # Moose Development/Moose/Ops/EasyA2G.lua # Moose Development/Moose/Ops/EasyGCICAP.lua
This commit is contained in:
@@ -1067,13 +1067,111 @@ do
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a single reject zone to MANTIS.
|
||||
-- @param #MANTIS self
|
||||
-- @param Core.Zone#ZONE Zone The zone to be added
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:AddRejectZone(Zone)
|
||||
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
|
||||
table.insert(self.RejectZones,Zone)
|
||||
self.usezones = true
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a single accept zone to MANTIS.
|
||||
-- @param #MANTIS self
|
||||
-- @param Core.Zone#ZONE Zone The zone to be added
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:AddAcceptZone(Zone)
|
||||
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
|
||||
table.insert(self.AcceptZones,Zone)
|
||||
self.usezones = true
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a single conflict zone to MANTIS.
|
||||
-- @param #MANTIS self
|
||||
-- @param Core.Zone#ZONE Zone The zone to be added
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:AddConflictZone(Zone)
|
||||
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
|
||||
table.insert(self.ConflictZones,Zone)
|
||||
self.usezones = true
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zones.
|
||||
-- @param #MANTIS self
|
||||
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:SetCorridorZones(CorridorZones)
|
||||
self:T(self.lid .. "SetCorridorZones")
|
||||
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
|
||||
self.corridorzones = CorridorZones
|
||||
self.usecorridors = true
|
||||
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
|
||||
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
|
||||
self.corridorzones:AddZone(CorridorZones)
|
||||
self.usecorridors = true
|
||||
end
|
||||
if self.intelset then
|
||||
for _,_intel in pairs(self.intelset) do
|
||||
_intel:SetCorridorZones(self.corridorzones)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to add one corridor zone.
|
||||
-- @param #MANTIS self
|
||||
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:AddCorridorZone(CorridorZone)
|
||||
self:T(self.lid .. "AddCorridorZone")
|
||||
self:SetCorridorZones(CorridorZone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in FEET.
|
||||
-- @param #MANTIS self
|
||||
-- @param #number Floor Floor altitude ASL in feet.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in feet.
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
|
||||
self.corridorfloor = UTILS.FeetToMeters(Floor)
|
||||
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
|
||||
if self.intelset then
|
||||
for _,_intel in pairs(self.intelset) do
|
||||
_intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in METERS.
|
||||
-- @param #MANTIS self
|
||||
-- @param #number Floor Floor altitude ASL in meters.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in meters.
|
||||
-- @return #MANTIS self
|
||||
function MANTIS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
|
||||
self.corridorfloor = Floor
|
||||
self.corridorceiling = Ceiling
|
||||
if self.intelset then
|
||||
for _,_intel in pairs(self.intelset) do
|
||||
_intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used)
|
||||
-- @param #MANTIS self
|
||||
-- @param #number radius Radius of the EWR detection zone
|
||||
function MANTIS:SetEWRRange(radius)
|
||||
self:T(self.lid .. "SetEWRRange")
|
||||
--local radius = radius or 80000
|
||||
-- self.acceptrange = radius
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -1628,6 +1726,12 @@ do
|
||||
IntelOne.DetectAccousticRadius = self.DetectAccousticRadius or 2000
|
||||
IntelOne.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER}
|
||||
--IntelOne:SetClusterAnalysis(true,true,true)
|
||||
if self.usecorridors == true then
|
||||
IntelOne:SetCorrdidorZones(self.corridorzones)
|
||||
if self.corridorfloor or self.corridorceiling then
|
||||
IntelOne:SetCooridorLimits(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
IntelOne:Start()
|
||||
|
||||
local IntelTwo = INTEL:New(samset,self.coalition,self.name.." IntelTwo")
|
||||
@@ -1635,6 +1739,12 @@ do
|
||||
IntelTwo.DetectAccousticRadius = self.DetectAccousticRadius or 2000
|
||||
IntelTwo.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER}
|
||||
--IntelTwo:SetClusterAnalysis(true,true,true)
|
||||
if self.usecorridors == true then
|
||||
IntelTwo:SetCorrdidorZones(self.corridorzones)
|
||||
if self.corridorfloor or self.corridorceiling then
|
||||
IntelTwo:SetCooridorLimits(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
IntelTwo:Start()
|
||||
|
||||
local CacheTime = self.DLinkCacheTime or 120
|
||||
@@ -2227,11 +2337,7 @@ do
|
||||
else
|
||||
self.Detection = self:StartIntelDetection()
|
||||
end
|
||||
--[[
|
||||
if self.advAwacs and not self.automode then
|
||||
self.AWACS_Detection = self:StartAwacsDetection()
|
||||
end
|
||||
--]]
|
||||
|
||||
if self.autoshorad then
|
||||
self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.Coalition,self.UseEmOnOff,self.SmokeDecoy,self.SmokeDecoyColor)
|
||||
self.Shorad:SetDefenseLimits(80,95)
|
||||
|
||||
@@ -509,7 +509,7 @@ do
|
||||
-- @field #AWACS
|
||||
AWACS = {
|
||||
ClassName = "AWACS", -- #string
|
||||
version = "0.2.73", -- #string
|
||||
version = "0.2.74", -- #string
|
||||
lid = "", -- #string
|
||||
coalition = coalition.side.BLUE, -- #number
|
||||
coalitiontxt = "blue", -- #string
|
||||
@@ -2060,6 +2060,55 @@ function AWACS:SetRejectionZone(Zone,Draw)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zones.
|
||||
-- @param #AWACS self
|
||||
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
|
||||
-- @return #AWACS self
|
||||
function AWACS:SetCorridorZones(CorridorZones)
|
||||
self:T(self.lid .. "SetCorridorZones")
|
||||
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
|
||||
self.corridorzones = CorridorZones
|
||||
self.usecorridors = true
|
||||
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
|
||||
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
|
||||
self.corridorzones:AddZone(CorridorZones)
|
||||
self.usecorridors = true
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to add one corridor zone.
|
||||
-- @param #AWACS self
|
||||
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
|
||||
-- @return #AWACS self
|
||||
function AWACS:AddCorridorZone(CorridorZone)
|
||||
self:T(self.lid .. "AddCorridorZone")
|
||||
self:SetCorridorZones(CorridorZone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in FEET.
|
||||
-- @param #AWACS self
|
||||
-- @param #number Floor Floor altitude ASL in feet.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in feet.
|
||||
-- @return #AWACS self
|
||||
function AWACS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
|
||||
self.corridorfloor = UTILS.FeetToMeters(Floor)
|
||||
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in METERS.
|
||||
-- @param #AWACS self
|
||||
-- @param #number Floor Floor altitude ASL in meters.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in meters.
|
||||
-- @return #AWACS self
|
||||
function AWACS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
|
||||
self.corridorfloor = Floor
|
||||
self.corridorceiling = Ceiling
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Draw a line around the FEZ on the F10 map.
|
||||
-- @param #AWACS self
|
||||
-- @return #AWACS self
|
||||
@@ -4336,6 +4385,14 @@ function AWACS:_StartIntel(awacs)
|
||||
intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER})
|
||||
end
|
||||
|
||||
-- Corridors
|
||||
if self.usecorridors == true then
|
||||
intel:SetCorridorZones(self.corridorzones)
|
||||
if self.corridorceiling or self.corridorfloor then
|
||||
intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
|
||||
-- Callbacks
|
||||
local function NewCluster(Cluster)
|
||||
self:__NewCluster(5,Cluster)
|
||||
|
||||
@@ -293,7 +293,7 @@ EASYA2G = {
|
||||
|
||||
--- EASYA2G class version.
|
||||
-- @field #string version
|
||||
EASYA2G.version="0.0.2"
|
||||
EASYA2G.version="0.1.3"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- TODO list
|
||||
@@ -806,6 +806,14 @@ function EASYA2G:_StartIntel()
|
||||
BlueIntel:SetRejectZones(self.NoGoZoneSet)
|
||||
BlueIntel:SetConflictZones(self.ConflictZoneSet)
|
||||
BlueIntel:SetVerbosity(0)
|
||||
|
||||
if self.usecorridors == true then
|
||||
BlueIntel:SetCorridorZones(self.corridorzones)
|
||||
if self.corridorfloor or self.corridorceiling then
|
||||
BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
|
||||
BlueIntel:Start()
|
||||
|
||||
if self.debug then
|
||||
|
||||
@@ -284,7 +284,7 @@ EASYGCICAP = {
|
||||
|
||||
--- EASYGCICAP class version.
|
||||
-- @field #string version
|
||||
EASYGCICAP.version="0.1.32"
|
||||
EASYGCICAP.version="0.1.33"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
--
|
||||
@@ -1364,6 +1364,54 @@ function EASYGCICAP:AddConflictZone(Zone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zones.
|
||||
-- @param #EASYGCICAP self
|
||||
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
|
||||
-- @return #EASYGCICAP self
|
||||
function EASYGCICAP:SetCorridorZones(CorridorZones)
|
||||
self:T(self.lid .. "SetCorridorZones")
|
||||
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
|
||||
self.corridorzones = CorridorZones
|
||||
self.usecorridors = true
|
||||
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
|
||||
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
|
||||
self.corridorzones:AddZone(CorridorZones)
|
||||
self.usecorridors = true
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to add one corridor zone.
|
||||
-- @param #EASYGCICAP self
|
||||
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
|
||||
-- @return #EASYGCICAP self
|
||||
function EASYGCICAP:AddCorridorZone(CorridorZone)
|
||||
self:T(self.lid .. "AddCorridorZone")
|
||||
self:SetCorridorZones(CorridorZone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in FEET.
|
||||
-- @param #EASYGCICAP self
|
||||
-- @param #number Floor Floor altitude ASL in feet.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in feet.
|
||||
-- @return #EASYGCICAP self
|
||||
function EASYGCICAP:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
|
||||
self.corridorfloor = UTILS.FeetToMeters(Floor)
|
||||
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in METERS.
|
||||
-- @param #EASYGCICAP self
|
||||
-- @param #number Floor Floor altitude ASL in meters.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in meters.
|
||||
-- @return #EASYGCICAP self
|
||||
function EASYGCICAP:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
|
||||
self.corridorfloor = Floor
|
||||
self.corridorceiling = Ceiling
|
||||
return self
|
||||
end
|
||||
|
||||
--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready.
|
||||
-- @param #EASYGCICAP self
|
||||
@@ -1574,6 +1622,14 @@ function EASYGCICAP:_StartIntel()
|
||||
BlueIntel:SetRejectZones(self.NoGoZoneSet)
|
||||
BlueIntel:SetConflictZones(self.ConflictZoneSet)
|
||||
BlueIntel:SetVerbosity(0)
|
||||
|
||||
if self.usecorridors == true then
|
||||
BlueIntel:SetCorridorZones(self.corridorzones)
|
||||
if self.corridorfloor or self.corridorceiling then
|
||||
BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling)
|
||||
end
|
||||
end
|
||||
|
||||
BlueIntel:Start()
|
||||
|
||||
if self.debug then
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
-- @field Core.Set#SET_ZONE acceptzoneset Set of accept zones. If defined, only contacts in these zones are considered.
|
||||
-- @field Core.Set#SET_ZONE rejectzoneset Set of reject zones. Contacts in these zones are not considered, even if they are in accept zones.
|
||||
-- @field Core.Set#SET_ZONE conflictzoneset Set of conflict zones. Contacts in these zones are considered, even if they are not in accept zones or if they are in reject zones.
|
||||
-- @field Core.Set#SET_ZONE corridorzoneset Set of corridor zones. Contacts in these zones are never considered. Also see corridorfloorheight and corridorfloorceiling.
|
||||
-- @field #number corridorfloor [Air] Contacts below this height (ASL!) are considered, even if they are in a corridor zone.
|
||||
-- @field #number corridorceiling [Air] Contacts above this height (ASL!) are considered, even if they are in a corridor zone.
|
||||
-- @field #table Contacts Table of detected items.
|
||||
-- @field #table ContactsLost Table of lost detected items.
|
||||
-- @field #table ContactsUnknown Table of new detected items.
|
||||
@@ -166,7 +169,7 @@ INTEL.Ctype={
|
||||
|
||||
--- INTEL class version.
|
||||
-- @field #string version
|
||||
INTEL.version="0.3.7"
|
||||
INTEL.version="0.3.9"
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- ToDo list
|
||||
@@ -181,6 +184,7 @@ INTEL.version="0.3.7"
|
||||
-- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone.
|
||||
-- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?!
|
||||
-- DONE: Combine units to groups for all, new and lost.
|
||||
-- DONE: Add corridor zones.
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-- Constructor
|
||||
@@ -272,6 +276,7 @@ function INTEL:New(DetectionSet, Coalition, Alias)
|
||||
self:SetForgetTime()
|
||||
self:SetAcceptZones()
|
||||
self:SetRejectZones()
|
||||
self:SetCorridorZones()
|
||||
self:SetConflictZones()
|
||||
|
||||
------------------------
|
||||
@@ -501,7 +506,63 @@ function INTEL:RemoveConflictZone(ConflictZone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- **OBSOLETE, will be removed in next version!** Set forget contacts time interval.
|
||||
--- Set corrdidor zones. Contacts detected in this/these zone(s) are never reported by the detection.
|
||||
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
|
||||
-- @param #INTEL self
|
||||
-- @param Core.Set#SET_ZONE CorridorZoneSet Set of corrdidor zone(s).
|
||||
-- @return #INTEL self
|
||||
function INTEL:SetCorridorZones(CorridorZoneSet)
|
||||
self.corridorzoneset=CorridorZoneSet or SET_ZONE:New()
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a corrdidor zone. Contacts detected in this zone are corrdidored and not reported by the detection.
|
||||
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
|
||||
-- @param #INTEL self
|
||||
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corrdidor zone set.
|
||||
-- @return #INTEL self
|
||||
function INTEL:AddCorridorZone(CorridorZone)
|
||||
self.corridorzoneset:AddZone(CorridorZone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Remove a corrdidor zone from the corrdidor zone set.
|
||||
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
|
||||
-- @param #INTEL self
|
||||
-- @param Core.Zone#ZONE CorridorZone Remove a zone from the corrdidor zone set.
|
||||
-- @return #INTEL self
|
||||
function INTEL:RemoveCorridorZone(CorridorZone)
|
||||
self.corridorzoneset:Remove(CorridorZone:GetName(), true)
|
||||
return self
|
||||
end
|
||||
|
||||
--- [Air] Add corrdidor zone floor and height. Are considered as ASL (above sea level or barometric) values.
|
||||
-- Overrides corridor exception for objects flying outside this limitations.
|
||||
-- @param #INTEL self
|
||||
-- @param #number Floor Floor altitude in meters.
|
||||
-- @param #number Ceiling Ceiling altitude in meters.
|
||||
-- @return #INTEL self
|
||||
function INTEL:SetCorridorLimits(Floor,Ceiling)
|
||||
self.corridorceiling = Ceiling or 10000
|
||||
self.corridorfloor = Floor or 1
|
||||
return self
|
||||
end
|
||||
|
||||
--- [Air] Add corrdidor zone floor and height. Are considered as ASL (above sea level or barometric) values.
|
||||
-- Overrides corridor exception for objects flying outside this limitations.
|
||||
-- @param #INTEL self
|
||||
-- @param #number Floor Floor altitude in feet.
|
||||
-- @param #number Ceiling Ceiling altitude in feet.
|
||||
-- @return #INTEL self
|
||||
function INTEL:SetCorridorLimitsFeet(Floor,Ceiling)
|
||||
local Ceiling = Ceiling or 25000
|
||||
local Floor = Floor or 15000
|
||||
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
|
||||
self.corridorfloor = UTILS.FeetToMeters(Floor)
|
||||
return self
|
||||
end
|
||||
|
||||
--- **OBSOLETE, not functional!** Set forget contacts time interval.
|
||||
-- Previously known contacts that are not detected any more, are "lost" after this time.
|
||||
-- This avoids fast oscillations between a contact being detected and undetected.
|
||||
-- @param #INTEL self
|
||||
@@ -922,6 +983,29 @@ function INTEL:UpdateIntel()
|
||||
table.insert(remove, unitname)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if unit is in any of the corridor zones.
|
||||
if self.corridorzoneset:Count()>0 then
|
||||
local inzone = false
|
||||
for _,_zone in pairs(self.corridorzoneset.Set) do
|
||||
local zone=_zone --Core.Zone#ZONE
|
||||
if unit:IsInZone(zone) then
|
||||
if unit:IsAir() and (self.corridorfloor ~= nil or self.corridorceiling ~= nil) then
|
||||
local alt = unit:GetAltitude()
|
||||
if self.corridorfloor and alt > self.corridorfloor then inzone = true end
|
||||
if self.corridorceiling and alt < self.corridorceiling then inzone = true end
|
||||
if inzone == true then break end
|
||||
else
|
||||
inzone=true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Unit is inside a corridor zone ==> remove!
|
||||
if inzone then
|
||||
table.insert(remove, unitname)
|
||||
end
|
||||
end
|
||||
|
||||
-- Filter unit categories. Added check that we have a UNIT and not a STATIC object because :GetUnitCategory() is only available for units.
|
||||
if #self.filterCategory>0 and unit:IsInstanceOf("UNIT") then
|
||||
@@ -2209,7 +2293,7 @@ function INTEL:GetClusterCoordinate(Cluster, Update)
|
||||
return Cluster.coordinate
|
||||
end
|
||||
|
||||
--- Check if the coorindate of the cluster changed.
|
||||
--- Check if the coordindate of the cluster changed.
|
||||
-- @param #INTEL self
|
||||
-- @param #INTEL.Cluster Cluster The cluster.
|
||||
-- @param #number Threshold in meters. Default 100 m.
|
||||
|
||||
@@ -61,6 +61,7 @@ do
|
||||
-- @field #number PreviousCount
|
||||
-- @field #boolean CanSmoke
|
||||
-- @field #boolean ShowThreatDetails
|
||||
-- @field #boolean PersistMe
|
||||
-- @extends Core.Fsm#FSM
|
||||
|
||||
|
||||
@@ -98,11 +99,12 @@ PLAYERTASK = {
|
||||
PreviousCount = 0,
|
||||
CanSmoke = true,
|
||||
ShowThreatDetails = true,
|
||||
PersistMe = false,
|
||||
}
|
||||
|
||||
--- PLAYERTASK class version.
|
||||
-- @field #string version
|
||||
PLAYERTASK.version="0.1.29"
|
||||
PLAYERTASK.version="0.1.31"
|
||||
|
||||
--- Generic task condition.
|
||||
-- @type PLAYERTASK.Condition
|
||||
@@ -401,8 +403,17 @@ function PLAYERTASK:CanJoinTask(Group, Client)
|
||||
return true
|
||||
end
|
||||
|
||||
--- [User] Set this task for persistance, if persistance is enabled on the PLAYERTASKCONTROLLER instance.
|
||||
-- @param #PLAYERTASK self
|
||||
-- @return #PLAYERTASK self
|
||||
function PLAYERTASK:EnablePersistance()
|
||||
self.PersistMe = true
|
||||
return self
|
||||
end
|
||||
|
||||
--- [Internal] Add a PLAYERTASKCONTROLLER for this task
|
||||
-- @param #PLAYERTASK self
|
||||
--
|
||||
-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller
|
||||
-- @return #PLAYERTASK self
|
||||
function PLAYERTASK:_SetController(Controller)
|
||||
@@ -1088,7 +1099,6 @@ function PLAYERTASK:onafterStatus(From, Event, To)
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--- [Internal] On after progress call
|
||||
-- @param #PLAYERTASK self
|
||||
-- @param #string From
|
||||
@@ -1363,6 +1373,12 @@ do
|
||||
-- @field Core.ClientMenu#CLIENTMENU MenuNoTask
|
||||
-- @field #boolean InformationMenu Show Radio Info Menu
|
||||
-- @field #number TaskInfoDuration How long to show the briefing info on the screen
|
||||
-- @field #table TaskPersistance Table for persistance data
|
||||
-- @field #boolean TaskPersistanceSwitch Switch for persisting tasks
|
||||
-- @field #string TaskPersistancePath File path for persisting tasks
|
||||
-- @field #string TaskPersistanceFilename File name for persisting tasks
|
||||
-- @field #table TasksPersistable List of persistable tasks
|
||||
-- @field #number SceneryExplosivesAmount Kgs of TNT to explode scenery on task persistance loading
|
||||
-- @extends Core.Fsm#FSM
|
||||
|
||||
---
|
||||
@@ -1669,8 +1685,28 @@ do
|
||||
--
|
||||
-- Set a marker on the map and add the following text to create targets from it: "TARGET". This is effectively the same as adding a COORDINATE object as target.
|
||||
-- The marker can be deleted any time.
|
||||
--
|
||||
-- ## 9 Discussion
|
||||
--
|
||||
-- ## 9 Single Task Persistence for mission designer added tasks
|
||||
--
|
||||
-- The class can persist the state of single tasks of type BOMBING, PRECISIONBOMBING, ARTY and SEAD, i.e. tasks which have a GROUND(!) GROUP, UNIT, STATIC or SCENERY as target.
|
||||
-- This requires the task to have a unique(!) menu name set, a TARGET which already exists on the map at mission start(!), and a flag that this task is actually to be persisted.
|
||||
-- Also, you need to desanitize the mission scripting environment, i.e. "lfs" and "io" must be available so we can write to disk.
|
||||
--
|
||||
-- -- First, we need to enable on the PLAYERTASKCONTROLLER itself
|
||||
-- taskmanager:EnableTaskPersistance([[C:\Users\myname\Saved Games\DCS\Missions\MyMisionFolder\]],"Mission Tasks.csv") -- Path and Filename
|
||||
--
|
||||
-- -- Then, we can design a task marking mission progress that we want to persist
|
||||
-- local RussianRadios = SET_STATIC:New():FilterPrefixes("Comms Tower Russia"):FilterOnce()
|
||||
--
|
||||
-- local RadioTask = PLAYERTASK:New(AUFTRAG.Type.BOMBING,RussianRadios,true,5,"Bombing")
|
||||
-- RadioTask:SetMenuName("Neutralize Comms Towers") -- UNIQUE menu name so we can find the task later!
|
||||
-- RadioTask:AddFreetext("Find and neutralize the two communication towers near NB70 East of Fulda on Streufelsberg!")
|
||||
-- RadioTask:AddFreetextTTS("Find and neutralize the two communication towers naer N;B;7;zero; East of Fulda on Streufelsberg!")
|
||||
-- RadioTask:EnablePersistance() -- Enable persistence for this task
|
||||
--
|
||||
-- taskmanager:AddPlayerTaskToQueue(RadioTask,true,false)
|
||||
--
|
||||
-- ## 10 Discussion
|
||||
--
|
||||
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-playertask channel.
|
||||
--
|
||||
@@ -1720,6 +1756,12 @@ PLAYERTASKCONTROLLER = {
|
||||
MenuNoTask = nil,
|
||||
InformationMenu = false,
|
||||
TaskInfoDuration = 30,
|
||||
TaskPersistance = {},
|
||||
TaskPersistanceSwitch = false,
|
||||
TaskPersistancePath = nil,
|
||||
TaskPersistanceFilename = nil,
|
||||
TasksPersistable = {},
|
||||
SceneryExplosivesAmount = 300,
|
||||
}
|
||||
|
||||
---
|
||||
@@ -1740,6 +1782,7 @@ AUFTRAG.Type.PRECISIONBOMBING = "Precision Bombing"
|
||||
AUFTRAG.Type.CTLD = "Combat Transport"
|
||||
AUFTRAG.Type.CSAR = "Combat Rescue"
|
||||
AUFTRAG.Type.CONQUER = "Conquer"
|
||||
|
||||
---
|
||||
-- @type Scores
|
||||
PLAYERTASKCONTROLLER.Scores = {
|
||||
@@ -1759,6 +1802,24 @@ PLAYERTASKCONTROLLER.Scores = {
|
||||
[AUFTRAG.Type.CAP] = 100,
|
||||
[AUFTRAG.Type.CAPTUREZONE] = 100,
|
||||
}
|
||||
|
||||
---
|
||||
-- @type TasksPersistable
|
||||
PLAYERTASKCONTROLLER.TasksPersistable = {
|
||||
[AUFTRAG.Type.PRECISIONBOMBING] = true,
|
||||
[AUFTRAG.Type.BOMBING] = true,
|
||||
[AUFTRAG.Type.ARTY] = true,
|
||||
[AUFTRAG.Type.SEAD] = true,
|
||||
}
|
||||
|
||||
---
|
||||
-- @type PersistenceData
|
||||
-- @field #number ID
|
||||
-- @field #string Name
|
||||
-- @field #string Type
|
||||
-- @field #number InitialTargets
|
||||
-- @field #number Targetsleft
|
||||
-- @field #boolean updated
|
||||
|
||||
---
|
||||
-- @type SeadAttributes
|
||||
@@ -2174,6 +2235,30 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
|
||||
|
||||
end
|
||||
|
||||
--- [User] Enable Task persistance (for specific gound target tasks)
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param #string Path Path where to save the task data
|
||||
-- @param #string Filename File name under which to save the task data
|
||||
-- @param #number KgsOfTNT (Optional) Explosives kgs used to remove scenery for persistence, defaults to 300
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:EnableTaskPersistance(Path,Filename,KgsOfTNT)
|
||||
self.TaskPersistanceSwitch = true
|
||||
self.TaskPersistancePath = Path
|
||||
self.TaskPersistanceFilename = Filename
|
||||
self.SceneryExplosivesAmount = KgsOfTNT or 300
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Disable Task persistance (for specific gound target tasks)
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:DisableTaskPersistance()
|
||||
self.TaskPersistanceSwitch = false
|
||||
self.TaskPersistancePath = nil
|
||||
self.TaskPersistanceFilename = nil
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Set or create a SCORING object for this taskcontroller
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param Functional.Scoring#SCORING Scoring (optional) the Scoring object
|
||||
@@ -2861,15 +2946,6 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType()
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
for _type,_data in pairs(tasktypes) do
|
||||
self:I("Task Type: ".._type)
|
||||
for _id,_task in pairs(_data) do
|
||||
self:I("Task Name: ".._task.Target:GetName())
|
||||
end
|
||||
end
|
||||
--]]
|
||||
|
||||
return tasktypes
|
||||
end
|
||||
|
||||
@@ -3566,9 +3642,9 @@ function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter)
|
||||
PlayerTask:_SetController(self)
|
||||
PlayerTask:SetCoalition(self.Coalition)
|
||||
self.TaskQueue:Push(PlayerTask)
|
||||
if not Silent then
|
||||
self:__TaskAdded(10,PlayerTask)
|
||||
end
|
||||
--if not Silent then
|
||||
self:__TaskAdded(10,PlayerTask,Silent)
|
||||
--end
|
||||
else
|
||||
self:E(self.lid.."***** NO valid PAYERTASK object sent!")
|
||||
end
|
||||
@@ -4584,6 +4660,76 @@ function PLAYERTASKCONTROLLER:RemoveConflictZone(ConflictZone)
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Add an corridor zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corridor zone set.
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:AddCorridorZone(CorridorZone)
|
||||
self:T(self.lid.."AddCorridorZone")
|
||||
if self.Intel then
|
||||
self.Intel:AddCorridorZone(CorridorZone)
|
||||
else
|
||||
self:E(self.lid.."*****NO detection has been set up (yet)!")
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Add an corridor SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param Core.Set#SET_ZONE CorridorZoneSet Add a SET_ZONE to the corridor zone set.
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:AddCorridorZoneSet(CorridorZoneSet)
|
||||
self:T(self.lid.."AddCorridorZoneSet")
|
||||
if self.Intel then
|
||||
self.Intel.corridorzoneset:AddSet(CorridorZoneSet)
|
||||
else
|
||||
self:E(self.lid.."*****NO detection has been set up (yet)!")
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Remove an corridor zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param Core.Zone#ZONE CorridorZone Remove this zone from the corridor zone set.
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:RemoveCorridorZone(CorridorZone)
|
||||
self:T(self.lid.."RemoveCorridorZone")
|
||||
if self.Intel then
|
||||
self.Intel:RemoveCorridorZone(CorridorZone)
|
||||
else
|
||||
self:E(self.lid.."*****NO detection has been set up (yet)!")
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in FEET.
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param #number Floor Floor altitude ASL in feet.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in feet.
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
|
||||
if self.Intel then
|
||||
self.Intel:SetCorridorLimitsFeet(Floor,Ceiling)
|
||||
else
|
||||
self:E(self.lid.."*****NO detection has been set up (yet)!")
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Function to set corridor zone floor and ceiling in METERS.
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param #number Floor Floor altitude ASL in meters.
|
||||
-- @param #number Ceiling Ceiling altitude ASL in meters.
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
|
||||
if self.Intel then
|
||||
self.Intel:SetCorridorLimits(Floor,Ceiling)
|
||||
else
|
||||
self:E(self.lid.."*****NO detection has been set up (yet)!")
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- [User] Set the top menu name to a custom string.
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param #string Name The name to use as the top menu designation.
|
||||
@@ -4765,6 +4911,128 @@ function PLAYERTASKCONTROLLER:SetSRSBroadcast(Frequency,Modulation)
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
---
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param Ops.PlayerTask#PLASERTASK Task
|
||||
-- @param #number TargetsLeft
|
||||
function PLAYERTASKCONTROLLER:_UpdateTargetsAlive(Task,TargetsLeft)
|
||||
self:T(self.lid.."_UpdateTargetsAlive")
|
||||
local delta = Task.Target:CountTargets() - TargetsLeft
|
||||
if delta > 0 then
|
||||
self:T("Delta targets to be removed: "..delta)
|
||||
local count = 0
|
||||
local targets = Task.Target:GetObjects()
|
||||
for _,_object in pairs(targets or {}) do
|
||||
if _object and _object.ClassName and (_object:IsInstanceOf("GROUP") or _object:IsInstanceOf("UNIT") or _object:IsInstanceOf("STATIC") or _object:IsInstanceOf("SCENERY")) then
|
||||
if count < delta then
|
||||
count = count + 1
|
||||
if not _object:IsInstanceOf("SCENERY") then
|
||||
_object:Destroy(true)
|
||||
else
|
||||
_object:Explode(self.SceneryExplosivesAmount)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:_LoadTasksPersisted()
|
||||
self:T(self.lid.."_LoadTasksPersisted")
|
||||
|
||||
local function MatchTask(Type,Name)
|
||||
local foundtask
|
||||
self.TaskQueue:ForEach(
|
||||
function(_task)
|
||||
local task = _task -- #PLAYERTASK
|
||||
if task.Type == Type and task.Target.name and task.Target.name == Name then
|
||||
foundtask = task
|
||||
end
|
||||
end
|
||||
)
|
||||
return foundtask
|
||||
end
|
||||
|
||||
if lfs and io then
|
||||
local ok,data = UTILS.LoadFromFile(self.TaskPersistancePath,self.TaskPersistanceFilename)
|
||||
if ok == true then
|
||||
table.remove(data, 1)
|
||||
for _,_entry in pairs(data) do
|
||||
-- "--ID;;Name;;InitialTargets;;Targetsleft;;Type\n"
|
||||
local dataset = UTILS.Split(_entry,";;")
|
||||
local Taskdata = {} -- #PersistenceData
|
||||
Taskdata.ID = tonumber(dataset[1])
|
||||
Taskdata.Name = tostring(dataset[2])
|
||||
Taskdata.InitialTargets = tonumber(dataset[3])
|
||||
Taskdata.Targetsleft = tonumber(dataset[4])
|
||||
Taskdata.Type = tostring(dataset[5])
|
||||
Taskdata.Task = MatchTask(Taskdata.Type,Taskdata.Name)
|
||||
if Taskdata.Task == nil then
|
||||
self:E(self.lid.."No actual task found for "..Taskdata.Name)
|
||||
else
|
||||
self:T(self.lid.."Task loaded and match found for "..Taskdata.Name)
|
||||
end
|
||||
Taskdata.updated = Taskdata.InitialTargets == Taskdata.Targetsleft and true or false
|
||||
if Taskdata.Task and Taskdata.updated == false then
|
||||
self:_UpdateTargetsAlive(Taskdata.Task,Taskdata.Targetsleft)
|
||||
Taskdata.updated = true
|
||||
end
|
||||
self.TaskPersistance[Taskdata.ID] = Taskdata
|
||||
end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:ClearPersistedData()
|
||||
if lfs and io then
|
||||
local text = "-- Data Cleared\n"
|
||||
UTILS.SaveToFile(self.TaskPersistancePath,self.TaskPersistanceFilename,text)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:_SaveTasksPersisted()
|
||||
if lfs and io then
|
||||
local text = "--ID;;Name;;InitialTargets;;Targetsleft;;Type\n"
|
||||
for _,_data in pairs(self.TaskPersistance) do
|
||||
local data = _data -- #PersistenceData
|
||||
data.Targetsleft = data.Task.Target:CountTargets() -- recount
|
||||
if data.Task and data.Task:IsDone() then data.Targetsleft = 0 end
|
||||
local tasktext = string.format("%d;;%s;;%d;;%d;;%s\n",data.ID,data.Name,data.InitialTargets,data.Targetsleft,data.Type)
|
||||
text = text..tasktext
|
||||
end
|
||||
UTILS.SaveToFile(self.TaskPersistancePath,self.TaskPersistanceFilename,text)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---
|
||||
-- @param #PLAYERTASKCONTROLLER self
|
||||
-- @param #PLAYERTASK Task
|
||||
function PLAYERTASKCONTROLLER:_AddPersistenceData(Task)
|
||||
local Taskdata = {} -- #PersistenceData
|
||||
if not self.TaskPersistance[Task.PlayerTaskNr] then
|
||||
Taskdata.ID = Task.PlayerTaskNr
|
||||
Taskdata.Name = Task.Target.name or "none"
|
||||
Taskdata.InitialTargets = Task.Target:CountTargets()
|
||||
Taskdata.Targetsleft = Taskdata.InitialTargets
|
||||
Taskdata.Type = Task.Type
|
||||
Taskdata.updated = true
|
||||
Taskdata.Task = Task
|
||||
self.TaskPersistance[Task.PlayerTaskNr] = Taskdata
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------
|
||||
-- FSM Functions PLAYERTASKCONTROLLER
|
||||
-- TODO: FSM Functions PLAYERTASKCONTROLLER
|
||||
@@ -4788,7 +5056,11 @@ function PLAYERTASKCONTROLLER:onafterStart(From, Event, To)
|
||||
self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
|
||||
self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
|
||||
self:HandleEvent(EVENTS.UnitLost, self._EventHandler)
|
||||
self:SetEventPriority(5)
|
||||
self:SetEventPriority(5)
|
||||
-- Persistence
|
||||
if self.TaskPersistanceSwitch == true then
|
||||
self:_LoadTasksPersisted()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -4828,6 +5100,11 @@ function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To)
|
||||
self:I(text)
|
||||
end
|
||||
|
||||
-- Persistence
|
||||
if self.TaskPersistanceSwitch == true then
|
||||
self:_SaveTasksPersisted()
|
||||
end
|
||||
|
||||
if self:GetState() ~= "Stopped" then
|
||||
self:__Status(-30)
|
||||
end
|
||||
@@ -4983,19 +5260,26 @@ end
|
||||
-- @param #string Event
|
||||
-- @param #string To
|
||||
-- @param Ops.PlayerTask#PLAYERTASK Task
|
||||
-- @param #boolean Silent
|
||||
-- @return #PLAYERTASKCONTROLLER self
|
||||
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task)
|
||||
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task, Silent)
|
||||
self:T({From, Event, To})
|
||||
self:T(self.lid.."TaskAdded")
|
||||
local addtxt = self.gettext:GetEntry("TASKADDED",self.locale)
|
||||
local taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.Type))
|
||||
if not self.NoScreenOutput then
|
||||
self:_SendMessageToClients(taskname,15)
|
||||
--local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
|
||||
if not Silent then
|
||||
if not self.NoScreenOutput then
|
||||
self:_SendMessageToClients(taskname,15)
|
||||
--local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
|
||||
end
|
||||
if self.UseSRS then
|
||||
taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
|
||||
self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
|
||||
end
|
||||
end
|
||||
if self.UseSRS then
|
||||
taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
|
||||
self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
|
||||
self:T(self.lid..string.format("Pers = %s | Type = %s | TypePers = %s | TaskFlag = %s",tostring(self.TaskPersistanceSwitch),tostring(Task.Type),tostring(self.TasksPersistable[Task.Type]),tostring(Task.PersistMe)))
|
||||
if self.TaskPersistanceSwitch == true and self.TasksPersistable[Task.Type] == true and Task.PersistMe == true then
|
||||
self:_AddPersistenceData(Task)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user