mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2025-10-29 16:58:06 +00:00
Progress
This commit is contained in:
parent
18885f0450
commit
e17de754a3
@ -434,6 +434,8 @@ end
|
||||
function AI_A2A:onafterRTB( AIGroup, From, Event, To )
|
||||
self:F( { AIGroup, From, Event, To } )
|
||||
|
||||
self:E( "Group " .. self.Controllable:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" )
|
||||
|
||||
if AIGroup and AIGroup:IsAlive() then
|
||||
|
||||
self.CheckStatus = false
|
||||
@ -446,7 +448,7 @@ function AI_A2A:onafterRTB( AIGroup, From, Event, To )
|
||||
|
||||
local CurrentCoord = AIGroup:GetCoordinate()
|
||||
local ToTargetCoord = self.HomeAirbase:GetCoordinate()
|
||||
local ToTargetSpeed = math.random( self.MinSpeed, self.MaxSpeed )
|
||||
local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
|
||||
local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) )
|
||||
|
||||
local Distance = CurrentCoord:Get2DDistance( ToTargetCoord )
|
||||
|
||||
@ -126,9 +126,11 @@ AI_A2A_CAP = {
|
||||
-- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
|
||||
-- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h.
|
||||
-- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h.
|
||||
-- @param Dcs.DCSTypes#Speed EngageMinSpeed The minimum speed of the @{Controllable} in km/h when engaging a target.
|
||||
-- @param Dcs.DCSTypes#Speed EngageMaxSpeed The maximum speed of the @{Controllable} in km/h when engaging a target.
|
||||
-- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
|
||||
-- @return #AI_A2A_CAP
|
||||
function AI_A2A_CAP:New( AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )
|
||||
function AI_A2A_CAP:New( AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType )
|
||||
|
||||
-- Inherits from BASE
|
||||
local self = BASE:Inherit( self, AI_A2A_PATROL:New( AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2A_CAP
|
||||
@ -136,7 +138,10 @@ function AI_A2A_CAP:New( AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeiling
|
||||
self.Accomplished = false
|
||||
self.Engaging = false
|
||||
|
||||
self:AddTransition( { "Patrolling", "Engaging", "RTB" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP.
|
||||
self.EngageMinSpeed = EngageMinSpeed
|
||||
self.EngageMaxSpeed = EngageMaxSpeed
|
||||
|
||||
self:AddTransition( { "Patrolling", "Engaging", "Returning" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP.
|
||||
|
||||
--- OnBefore Transition Handler for Event Engage.
|
||||
-- @function [parent=#AI_A2A_CAP] OnBeforeEngage
|
||||
@ -394,7 +399,7 @@ function AI_A2A_CAP:onafterEngage( AIGroup, From, Event, To, AttackSetUnit )
|
||||
--- Calculate the target route point.
|
||||
local CurrentCoord = AIGroup:GetCoordinate()
|
||||
local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate()
|
||||
local ToTargetSpeed = math.random( self.MinSpeed, self.MaxSpeed )
|
||||
local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed )
|
||||
local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) )
|
||||
|
||||
--- Create a route point of type air.
|
||||
|
||||
@ -302,7 +302,7 @@ do -- AI_A2A_DISPATCHER
|
||||
--- @param #AI_A2A_DISPATCHER self
|
||||
-- @param Core.Event#EVENTDATA EventData
|
||||
function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData )
|
||||
self.Detection:ForgetDetectedUnit( EventData.IniUnitName )
|
||||
self.Detection:ForgetDetectedUnit( EventData.IniUnitName )
|
||||
end
|
||||
|
||||
--- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission.
|
||||
@ -513,11 +513,13 @@ do -- AI_A2A_DISPATCHER
|
||||
-- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed.
|
||||
-- @param #number FloorAltitude The minimum altitude at which the cap can be executed.
|
||||
-- @param #number CeilingAltitude the maximum altitude at which the cap can be executed.
|
||||
-- @param #number MinSpeed The minimum speed at which the cap can be executed.
|
||||
-- @param #number MaxSpeed The maximum speed at which the cap can be executed.
|
||||
-- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed.
|
||||
-- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed.
|
||||
-- @param #number EngageMinSpeed The minimum speed at which the engage can be executed.
|
||||
-- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed.
|
||||
-- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
|
||||
-- @return #AI_A2A_DISPATCHER
|
||||
function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, FloorAltitude, CeilingAltitude, MinSpeed, MaxSpeed, AltType )
|
||||
function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType )
|
||||
|
||||
self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
|
||||
self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {}
|
||||
@ -529,8 +531,10 @@ do -- AI_A2A_DISPATCHER
|
||||
Cap.Zone = Zone
|
||||
Cap.FloorAltitude = FloorAltitude
|
||||
Cap.CeilingAltitude = CeilingAltitude
|
||||
Cap.MinSpeed = MinSpeed
|
||||
Cap.MaxSpeed = MaxSpeed
|
||||
Cap.PatrolMinSpeed = PatrolMinSpeed
|
||||
Cap.PatrolMaxSpeed = PatrolMaxSpeed
|
||||
Cap.EngageMinSpeed = EngageMinSpeed
|
||||
Cap.EngageMaxSpeed = EngageMaxSpeed
|
||||
Cap.AltType = AltType
|
||||
|
||||
self:SetSquadronCapInterval( SquadronName, 2, 180, 600, 1 )
|
||||
@ -615,18 +619,18 @@ do -- AI_A2A_DISPATCHER
|
||||
---
|
||||
-- @param #AI_A2A_DISPATCHER self
|
||||
-- @param #string SquadronName The squadron name.
|
||||
-- @param #number MinSpeed The minimum speed at which the gci can be executed.
|
||||
-- @param #number MaxSpeed The maximum speed at which the gci can be executed.
|
||||
-- @param #number EngageMinSpeed The minimum speed at which the gci can be executed.
|
||||
-- @param #number EngageMaxSpeed The maximum speed at which the gci can be executed.
|
||||
-- @return #AI_A2A_DISPATCHER
|
||||
function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, MinSpeed, MaxSpeed )
|
||||
function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed )
|
||||
|
||||
self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
|
||||
self.DefenderSquadrons[SquadronName].Intercept = self.DefenderSquadrons[SquadronName].Intercept or {}
|
||||
|
||||
local Intercept = self.DefenderSquadrons[SquadronName].Intercept
|
||||
Intercept.Name = SquadronName
|
||||
Intercept.MinSpeed = MinSpeed
|
||||
Intercept.MaxSpeed = MaxSpeed
|
||||
Intercept.EngageMinSpeed = EngageMinSpeed
|
||||
Intercept.EngageMaxSpeed = EngageMaxSpeed
|
||||
end
|
||||
|
||||
--- Defines the amount of extra planes that will take-off as part of the defense system.
|
||||
@ -725,7 +729,11 @@ do -- AI_A2A_DISPATCHER
|
||||
for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do
|
||||
if DefenderTask.Type == "CAP" then
|
||||
if AIGroup:IsAlive() then
|
||||
CapCount = CapCount + 1
|
||||
-- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive!
|
||||
-- The CAP could be damaged, lost control, or out of fuel!
|
||||
if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) then
|
||||
CapCount = CapCount + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -772,8 +780,6 @@ do -- AI_A2A_DISPATCHER
|
||||
if Friendly and Friendly:IsAlive() then
|
||||
-- Ok, so we have a friendly near the potential target.
|
||||
-- Now we need to check if the AIGroup has a Task.
|
||||
self:F( { FriendlyName = Friendly:GetName() } )
|
||||
self:F( { FriendlyDistance = FriendlyDistance } )
|
||||
local DefenderTask = self:GetDefenderTask( Friendly )
|
||||
if DefenderTask then
|
||||
-- The Task should be CAP or INTERCEPT
|
||||
@ -783,7 +789,7 @@ do -- AI_A2A_DISPATCHER
|
||||
Friendlies = Friendlies or {}
|
||||
Friendlies[Friendly] = Friendly
|
||||
DefenderCount = DefenderCount + Friendly:GetSize()
|
||||
self:F( { Friendly = Friendly:GetName() } )
|
||||
self:F( { Friendly = Friendly:GetName(), FriendlyDistance = FriendlyDistance } )
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -819,7 +825,7 @@ do -- AI_A2A_DISPATCHER
|
||||
|
||||
if AIGroup then
|
||||
|
||||
local Fsm = AI_A2A_CAP:New( AIGroup, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.MinSpeed, Cap.MaxSpeed, Cap.AltType )
|
||||
local Fsm = AI_A2A_CAP:New( AIGroup, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType )
|
||||
Fsm:SetDispatcher( self )
|
||||
Fsm:SetHomeAirbase( DefenderSquadron.Airbase )
|
||||
Fsm:Start()
|
||||
@ -917,7 +923,7 @@ do -- AI_A2A_DISPATCHER
|
||||
|
||||
DefendersCount = DefendersCount - AIGroup:GetSize()
|
||||
|
||||
local Fsm = AI_A2A_INTERCEPT:New( AIGroup, Intercept.MinSpeed, Intercept.MaxSpeed )
|
||||
local Fsm = AI_A2A_INTERCEPT:New( AIGroup, Intercept.EngageMinSpeed, Intercept.EngageMaxSpeed )
|
||||
Fsm:SetDispatcher( self )
|
||||
Fsm:SetHomeAirbase( DefenderSquadron.Airbase )
|
||||
Fsm:Start()
|
||||
@ -954,6 +960,8 @@ do -- AI_A2A_DISPATCHER
|
||||
-- First, count the active AIGroups Units, targetting the DetectedSet
|
||||
local DefenderCount = self:CountDefendersEngaged( DetectedItem )
|
||||
local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount )
|
||||
|
||||
self:F( { DefenderCount = DefenderCount } )
|
||||
|
||||
-- Only allow ENGAGE when:
|
||||
-- 1. There are friendly units near the detected attackers.
|
||||
@ -982,6 +990,7 @@ do -- AI_A2A_DISPATCHER
|
||||
-- First, count the active AIGroups Units, targetting the DetectedSet
|
||||
local DefenderCount = self:CountDefendersEngaged( Target )
|
||||
local DefendersMissing = AttackerCount - DefenderCount
|
||||
self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } )
|
||||
|
||||
local Friendlies = self:CountDefendersToBeEngaged( Target, DefenderCount )
|
||||
|
||||
@ -1057,9 +1066,9 @@ do -- AI_A2A_DISPATCHER
|
||||
-- Show tactical situation
|
||||
for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
|
||||
local Defender = Defender -- Wrapper.Group#GROUP
|
||||
local Message = string.format( "%s, %s", Defender:GetName(), DefenderTask.Type )
|
||||
local Message = string.format( "%s ( %s - %s )", Defender:GetName(), DefenderTask.Type, DefenderTask.Fsm:GetState() )
|
||||
if DefenderTask.Target then
|
||||
Message = Message .. " => " .. DefenderTask.Target.Index .. " : " .. DefenderTask.Target.Set:GetObjectNames()
|
||||
Message = Message .. string.format( " => %s : %s", DefenderTask.Target.ItemID, DefenderTask.Target.Set:GetObjectNames() )
|
||||
end
|
||||
self:F( { Tactical = Message } )
|
||||
end
|
||||
|
||||
@ -119,7 +119,7 @@ AI_A2A_INTERCEPT = {
|
||||
-- @param #AI_A2A_INTERCEPT self
|
||||
-- @param Wrapper.Group#GROUP AIGroup
|
||||
-- @return #AI_A2A_INTERCEPT
|
||||
function AI_A2A_INTERCEPT:New( AIGroup, MinSpeed, MaxSpeed )
|
||||
function AI_A2A_INTERCEPT:New( AIGroup, EngageMinSpeed, EngageMaxSpeed )
|
||||
|
||||
-- Inherits from BASE
|
||||
local self = BASE:Inherit( self, AI_A2A:New( AIGroup ) ) -- #AI_A2A_INTERCEPT
|
||||
@ -127,12 +127,12 @@ function AI_A2A_INTERCEPT:New( AIGroup, MinSpeed, MaxSpeed )
|
||||
self.Accomplished = false
|
||||
self.Engaging = false
|
||||
|
||||
self.MinSpeed = MinSpeed
|
||||
self.MaxSpeed = MaxSpeed
|
||||
self.EngageMinSpeed = EngageMinSpeed
|
||||
self.EngageMaxSpeed = EngageMaxSpeed
|
||||
|
||||
self.PatrolAltType = "RADIO"
|
||||
|
||||
self:AddTransition( { "Started", "Engaging", "RTB" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_INTERCEPT.
|
||||
self:AddTransition( { "Started", "Engaging", "Returning" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_INTERCEPT.
|
||||
|
||||
--- OnBefore Transition Handler for Event Engage.
|
||||
-- @function [parent=#AI_A2A_INTERCEPT] OnBeforeEngage
|
||||
@ -366,7 +366,7 @@ function AI_A2A_INTERCEPT:onafterEngage( AIGroup, From, Event, To, AttackSetUnit
|
||||
local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate()
|
||||
self:SetTargetDistance( ToTargetCoord ) -- For RTB status check
|
||||
|
||||
local ToTargetSpeed = math.random( self.MinSpeed, self.MaxSpeed )
|
||||
local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed )
|
||||
local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) )
|
||||
|
||||
--- Create a route point of type air.
|
||||
@ -379,7 +379,7 @@ function AI_A2A_INTERCEPT:onafterEngage( AIGroup, From, Event, To, AttackSetUnit
|
||||
)
|
||||
|
||||
self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } )
|
||||
self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } )
|
||||
self:T2( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } )
|
||||
|
||||
EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint
|
||||
|
||||
@ -403,7 +403,7 @@ function AI_A2A_INTERCEPT:onafterEngage( AIGroup, From, Event, To, AttackSetUnit
|
||||
if #AttackTasks == 0 then
|
||||
self:E("No targets found -> Going RTB")
|
||||
self:Return()
|
||||
self:__RTB( 1 )
|
||||
self:RTB()
|
||||
else
|
||||
AttackTasks[#AttackTasks+1] = AIGroup:TaskFunction( 1, #AttackTasks, "AI_A2A_INTERCEPT.InterceptRoute" )
|
||||
EngageRoute[1].task = AIGroup:TaskCombo( AttackTasks )
|
||||
@ -419,7 +419,7 @@ function AI_A2A_INTERCEPT:onafterEngage( AIGroup, From, Event, To, AttackSetUnit
|
||||
else
|
||||
self:E("No targets found -> Going RTB")
|
||||
self:Return()
|
||||
self:__RTB( 1 )
|
||||
self:RTB()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr
|
||||
|
||||
-- Initialize the ObjectSchedulers array, which is a weakly coupled table.
|
||||
-- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array.
|
||||
self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } ) -- or {}
|
||||
self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } )
|
||||
|
||||
if Scheduler.MasterObject then
|
||||
self.ObjectSchedulers[self.CallID] = Scheduler
|
||||
@ -154,7 +154,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr
|
||||
self:Stop( Scheduler, CallID )
|
||||
end
|
||||
else
|
||||
self:E( "Scheduled obscolete call for CallID: " .. CallID )
|
||||
self:E( "Scheduled obsolete call for CallID: " .. CallID )
|
||||
end
|
||||
|
||||
return nil
|
||||
|
||||
@ -84,11 +84,6 @@ function SET_BASE:New( Database )
|
||||
self.TimeInterval = 0.001
|
||||
|
||||
self.Set = {}
|
||||
|
||||
self.List = {}
|
||||
self.List.__index = self.List
|
||||
self.List = setmetatable( { Count = 0 }, self.List )
|
||||
|
||||
self.Index = {}
|
||||
|
||||
self.CallScheduler = SCHEDULER:New( self )
|
||||
@ -126,24 +121,8 @@ end
|
||||
function SET_BASE:Add( ObjectName, Object )
|
||||
self:F( ObjectName )
|
||||
|
||||
local t = { _ = Object }
|
||||
|
||||
if self.List.last then
|
||||
self.List.last._next = t
|
||||
t._prev = self.List.last
|
||||
self.List.last = t
|
||||
else
|
||||
-- this is the first node
|
||||
self.List.first = t
|
||||
self.List.last = t
|
||||
end
|
||||
|
||||
self.List.Count = self.List.Count + 1
|
||||
|
||||
self.Set[ObjectName] = Object
|
||||
|
||||
table.insert( self.Index, ObjectName )
|
||||
|
||||
end
|
||||
|
||||
--- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index.
|
||||
@ -166,43 +145,19 @@ end
|
||||
-- @param #string ObjectName
|
||||
function SET_BASE:Remove( ObjectName )
|
||||
|
||||
local t = self.Set[ObjectName]
|
||||
local Object = self.Set[ObjectName]
|
||||
|
||||
self:F3( { ObjectName, t } )
|
||||
self:F3( { ObjectName, Object } )
|
||||
|
||||
if t then
|
||||
if t._next then
|
||||
if t._prev then
|
||||
t._next._prev = t._prev
|
||||
t._prev._next = t._next
|
||||
else
|
||||
-- this was the first node
|
||||
t._next._prev = nil
|
||||
self.List._first = t._next
|
||||
end
|
||||
elseif t._prev then
|
||||
-- this was the last node
|
||||
t._prev._next = nil
|
||||
self.List._last = t._prev
|
||||
else
|
||||
-- this was the only node
|
||||
self.List._first = nil
|
||||
self.List._last = nil
|
||||
end
|
||||
|
||||
t._next = nil
|
||||
t._prev = nil
|
||||
self.List.Count = self.List.Count - 1
|
||||
|
||||
if Object then
|
||||
for Index, Key in ipairs( self.Index ) do
|
||||
if Key == ObjectName then
|
||||
table.remove( self.Index, Index )
|
||||
self.Set[ObjectName] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
self.Set[ObjectName] = nil
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@ -214,12 +169,10 @@ end
|
||||
function SET_BASE:Get( ObjectName )
|
||||
self:F( ObjectName )
|
||||
|
||||
local t = self.Set[ObjectName]
|
||||
|
||||
self:T3( { ObjectName, t } )
|
||||
|
||||
return t
|
||||
local Object = self.Set[ObjectName]
|
||||
|
||||
self:T3( { ObjectName, Object } )
|
||||
return Object
|
||||
end
|
||||
|
||||
--- Gets the first object from the @{Set#SET_BASE} and derived classes.
|
||||
@ -260,7 +213,7 @@ end
|
||||
-- @return #number Count
|
||||
function SET_BASE:Count()
|
||||
|
||||
return #self.Index or 0
|
||||
return self.Index and #self.Index or 0
|
||||
end
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' )
|
||||
env.info( 'Moose Generation Timestamp: 20170606_1812' )
|
||||
env.info( 'Moose Generation Timestamp: 20170608_1332' )
|
||||
|
||||
local base = _G
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user