first commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
|
||||
namespace ClassicAssist.Data.Targeting
|
||||
{
|
||||
public enum TargetFriendType
|
||||
{
|
||||
Include,
|
||||
Only,
|
||||
None
|
||||
}
|
||||
|
||||
public enum TargetBodyType
|
||||
{
|
||||
Any,
|
||||
Humanoid,
|
||||
Transformation,
|
||||
Both
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum TargetNotoriety
|
||||
{
|
||||
None = 0b0,
|
||||
Innocent = 0b1,
|
||||
Criminal = 0b10,
|
||||
Enemy = 0b100,
|
||||
Murderer = 0b1000,
|
||||
Friend = 0b10000,
|
||||
Gray = 0b100000,
|
||||
Any = 0b111111
|
||||
}
|
||||
|
||||
public enum TargetDistance
|
||||
{
|
||||
Next,
|
||||
Nearest,
|
||||
Closest,
|
||||
Previous
|
||||
}
|
||||
|
||||
public enum TargetInfliction
|
||||
{
|
||||
Any,
|
||||
Lowest,
|
||||
Poisoned,
|
||||
Mortaled,
|
||||
Paralyzed,
|
||||
Dead,
|
||||
Unmounted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ClassicAssist.Data.Targeting
|
||||
{
|
||||
public class TargetBodyData
|
||||
{
|
||||
public TargetBodyType BodyType { get; set; }
|
||||
public int Graphic { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ClassicAssist.Shared;
|
||||
using ClassicAssist.Data.Macros;
|
||||
using ClassicAssist.Data.Macros.Commands;
|
||||
using ClassicAssist.Shared.Resources;
|
||||
using ClassicAssist.UO;
|
||||
using ClassicAssist.UO.Data;
|
||||
using ClassicAssist.UO.Network.Packets;
|
||||
using ClassicAssist.UO.Objects;
|
||||
using Newtonsoft.Json;
|
||||
using UOC = ClassicAssist.Shared.UO.Commands;
|
||||
|
||||
namespace ClassicAssist.Data.Targeting
|
||||
{
|
||||
public class TargetManager
|
||||
{
|
||||
public delegate void dTargetChanged( int newSerial, int oldSerial );
|
||||
|
||||
private const int MAX_DISTANCE = 18;
|
||||
private static TargetManager _instance;
|
||||
private static readonly object _lock = new object();
|
||||
private static readonly List<Mobile> _ignoreList = new List<Mobile>();
|
||||
private readonly List<TargetBodyData> _bodyData;
|
||||
|
||||
private TargetManager()
|
||||
{
|
||||
string dataPath = Path.Combine( Engine.StartupPath ?? Environment.CurrentDirectory, "Data" );
|
||||
|
||||
_bodyData = JsonConvert
|
||||
.DeserializeObject<TargetBodyData[]>( File.ReadAllText( Path.Combine( dataPath, "Bodies.json" ) ) )
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static event dTargetChanged EnemyChangedEvent;
|
||||
public static event dTargetChanged FriendChangedEvent;
|
||||
public static event dTargetChanged LastTargetChangedEvent;
|
||||
|
||||
public void SetEnemy( Entity m )
|
||||
{
|
||||
if ( !UOMath.IsMobile( m.Serial ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !MacroManager.QuietMode )
|
||||
{
|
||||
MsgCommands.HeadMsg( Options.CurrentOptions.EnemyTargetMessage, m.Serial );
|
||||
}
|
||||
|
||||
MsgCommands.HeadMsg( $"Target: {m.Name?.Trim() ?? "Unknown"}" );
|
||||
|
||||
if ( m.Serial != Engine.Player.EnemyTargetSerial )
|
||||
{
|
||||
EnemyChangedEvent?.Invoke( m.Serial, Engine.Player.EnemyTargetSerial );
|
||||
LastTargetChangedEvent?.Invoke( m.Serial, Engine.Player.LastTargetSerial );
|
||||
}
|
||||
|
||||
Engine.Player.LastTargetSerial = m.Serial;
|
||||
Engine.Player.EnemyTargetSerial = m.Serial;
|
||||
Engine.SendPacketToClient( new ChangeCombatant( m.Serial ) );
|
||||
}
|
||||
|
||||
public void SetFriend( Entity m )
|
||||
{
|
||||
if ( !UOMath.IsMobile( m.Serial ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !MacroManager.QuietMode )
|
||||
{
|
||||
MsgCommands.HeadMsg( Options.CurrentOptions.FriendTargetMessage, m.Serial );
|
||||
}
|
||||
|
||||
MsgCommands.HeadMsg( $"Target: {m.Name?.Trim() ?? "Unknown"}" );
|
||||
|
||||
if ( m.Serial != Engine.Player.FriendTargetSerial )
|
||||
{
|
||||
FriendChangedEvent?.Invoke( m.Serial, Engine.Player.FriendTargetSerial );
|
||||
LastTargetChangedEvent?.Invoke( m.Serial, Engine.Player.LastTargetSerial );
|
||||
}
|
||||
|
||||
Engine.Player.LastTargetSerial = m.Serial;
|
||||
Engine.Player.FriendTargetSerial = m.Serial;
|
||||
Engine.SendPacketToClient( new ChangeCombatant( m.Serial ) );
|
||||
}
|
||||
|
||||
public void SetLastTarget( Entity m )
|
||||
{
|
||||
if ( m.Serial != Engine.Player.LastTargetSerial )
|
||||
{
|
||||
LastTargetChangedEvent?.Invoke( m.Serial, Engine.Player.LastTargetSerial );
|
||||
}
|
||||
|
||||
Engine.Player.LastTargetSerial = m.Serial;
|
||||
|
||||
if ( !UOMath.IsMobile( m.Serial ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !MacroManager.QuietMode )
|
||||
{
|
||||
MsgCommands.HeadMsg( Options.CurrentOptions.LastTargetMessage, m.Serial );
|
||||
}
|
||||
|
||||
MsgCommands.HeadMsg( $"Target: {m.Name?.Trim() ?? "Unknown"}" );
|
||||
Engine.SendPacketToClient( new ChangeCombatant( m.Serial ) );
|
||||
}
|
||||
|
||||
public Mobile GetClosestMobile( IEnumerable<Notoriety> notoriety, TargetBodyType bodyType = TargetBodyType.Any,
|
||||
TargetFriendType friendType = TargetFriendType.Include,
|
||||
TargetInfliction inflictionType = TargetInfliction.Any )
|
||||
{
|
||||
Mobile mobile;
|
||||
|
||||
Func<int, bool> bodyTypePredicate;
|
||||
|
||||
switch ( bodyType )
|
||||
{
|
||||
case TargetBodyType.Any:
|
||||
bodyTypePredicate = i => true;
|
||||
break;
|
||||
case TargetBodyType.Humanoid:
|
||||
bodyTypePredicate = i =>
|
||||
_bodyData.Where( bd => bd.BodyType == TargetBodyType.Humanoid ).Select( bd => bd.Graphic )
|
||||
.Contains( i );
|
||||
break;
|
||||
case TargetBodyType.Transformation:
|
||||
bodyTypePredicate = i =>
|
||||
_bodyData.Where( bd => bd.BodyType == TargetBodyType.Transformation ).Select( bd => bd.Graphic )
|
||||
.Contains( i );
|
||||
break;
|
||||
case TargetBodyType.Both:
|
||||
bodyTypePredicate = i =>
|
||||
_bodyData.Where( bd =>
|
||||
bd.BodyType == TargetBodyType.Humanoid || bd.BodyType == TargetBodyType.Transformation )
|
||||
.Select( bd => bd.Graphic ).Contains( i );
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException( nameof( bodyType ), bodyType, null );
|
||||
}
|
||||
|
||||
if ( friendType == TargetFriendType.Only )
|
||||
{
|
||||
mobile = Engine.Mobiles.SelectEntities( m =>
|
||||
MobileCommands.InFriendList( m.Serial ) && bodyTypePredicate( m.ID ) &&
|
||||
( !Options.CurrentOptions.GetFriendEnemyUsesIgnoreList ||
|
||||
!ObjectCommands.IgnoreList.Contains( m.Serial ) ) && m.Serial != Engine.Player?.Serial )
|
||||
.OrderBy( m => m.Distance ).ByInflication( inflictionType ).FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
mobile = Engine.Mobiles.SelectEntities( m =>
|
||||
notoriety.Contains( m.Notoriety ) && m.Distance < MAX_DISTANCE && bodyTypePredicate( m.ID ) &&
|
||||
( friendType == TargetFriendType.Include || !MobileCommands.InFriendList( m.Serial ) ) &&
|
||||
( !Options.CurrentOptions.GetFriendEnemyUsesIgnoreList ||
|
||||
!ObjectCommands.IgnoreList.Contains( m.Serial ) ) && m.Serial != Engine.Player?.Serial )
|
||||
.OrderBy( m => m.Distance ).ByInflication( inflictionType )?.FirstOrDefault();
|
||||
}
|
||||
|
||||
return mobile;
|
||||
}
|
||||
|
||||
public Mobile GetNextMobile( IEnumerable<Notoriety> notoriety, TargetBodyType bodyType = TargetBodyType.Any,
|
||||
int distance = MAX_DISTANCE, TargetFriendType friendType = TargetFriendType.Include,
|
||||
TargetInfliction inflictionType = TargetInfliction.Any, bool reverse = false, Mobile previousMobile = null )
|
||||
{
|
||||
bool looped = false;
|
||||
|
||||
while ( true )
|
||||
{
|
||||
Mobile[] mobiles;
|
||||
|
||||
Func<int, bool> bodyTypePredicate;
|
||||
|
||||
switch ( bodyType )
|
||||
{
|
||||
case TargetBodyType.Any:
|
||||
bodyTypePredicate = i => true;
|
||||
break;
|
||||
case TargetBodyType.Humanoid:
|
||||
bodyTypePredicate = i =>
|
||||
_bodyData.Where( bd => bd.BodyType == TargetBodyType.Humanoid ).Select( bd => bd.Graphic )
|
||||
.Contains( i );
|
||||
break;
|
||||
case TargetBodyType.Transformation:
|
||||
bodyTypePredicate = i =>
|
||||
_bodyData.Where( bd => bd.BodyType == TargetBodyType.Transformation )
|
||||
.Select( bd => bd.Graphic ).Contains( i );
|
||||
break;
|
||||
case TargetBodyType.Both:
|
||||
bodyTypePredicate = i =>
|
||||
_bodyData.Where( bd =>
|
||||
bd.BodyType == TargetBodyType.Humanoid ||
|
||||
bd.BodyType == TargetBodyType.Transformation )
|
||||
.Select( bd => bd.Graphic ).Contains( i );
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException( nameof( bodyType ), bodyType, null );
|
||||
}
|
||||
|
||||
if ( previousMobile != null )
|
||||
{
|
||||
_ignoreList.Clear();
|
||||
}
|
||||
|
||||
if ( friendType == TargetFriendType.Only )
|
||||
{
|
||||
//Notoriety, bodyType ignored
|
||||
mobiles = Engine.Mobiles.SelectEntities( m =>
|
||||
m.Distance < distance && MobileCommands.InFriendList( m.Serial ) && bodyTypePredicate( m.ID ) &&
|
||||
!_ignoreList.Contains( m ) && ( !Options.CurrentOptions.GetFriendEnemyUsesIgnoreList ||
|
||||
!ObjectCommands.IgnoreList.Contains( m.Serial ) ) &&
|
||||
m.Serial != Engine.Player?.Serial );
|
||||
}
|
||||
else
|
||||
{
|
||||
mobiles = Engine.Mobiles.SelectEntities( m =>
|
||||
notoriety.Contains( m.Notoriety ) && m.Distance < distance && bodyTypePredicate( m.ID ) &&
|
||||
!_ignoreList.Contains( m ) &&
|
||||
( friendType == TargetFriendType.Include || !MobileCommands.InFriendList( m.Serial ) ) &&
|
||||
( !Options.CurrentOptions.GetFriendEnemyUsesIgnoreList ||
|
||||
!ObjectCommands.IgnoreList.Contains( m.Serial ) ) && m.Serial != Engine.Player?.Serial );
|
||||
}
|
||||
|
||||
if ( previousMobile != null )
|
||||
{
|
||||
mobiles = mobiles.Where( m => m.Serial != previousMobile.Serial ).OrderBy( m =>
|
||||
Math.Max( Math.Abs( m.X - previousMobile.X ), Math.Abs( m.Y - previousMobile.Y ) ) ).ToArray();
|
||||
|
||||
if ( mobiles.Length == 0 )
|
||||
{
|
||||
mobiles = new[] { previousMobile };
|
||||
}
|
||||
}
|
||||
|
||||
if ( reverse )
|
||||
{
|
||||
mobiles = mobiles.Reverse().ToArray();
|
||||
}
|
||||
|
||||
mobiles = mobiles.ByInflication( inflictionType );
|
||||
|
||||
if ( mobiles == null || mobiles.Length == 0 )
|
||||
{
|
||||
_ignoreList.Clear();
|
||||
|
||||
if ( looped )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
looped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
Mobile mobile = mobiles.FirstOrDefault();
|
||||
_ignoreList.Add( mobile );
|
||||
return mobile;
|
||||
}
|
||||
}
|
||||
|
||||
public Entity PromptTarget()
|
||||
{
|
||||
int serial = UOC.GetTargeSerialAsync( Strings.Target_object___ ).Result;
|
||||
|
||||
if ( serial == 0 )
|
||||
{
|
||||
UOC.SystemMessage( Strings.Invalid_or_unknown_object_id );
|
||||
return null;
|
||||
}
|
||||
|
||||
Mobile mobile = Engine.Mobiles.GetMobile( serial );
|
||||
|
||||
if ( mobile != null )
|
||||
{
|
||||
return mobile;
|
||||
}
|
||||
|
||||
UOC.SystemMessage( Strings.Mobile_not_found___ );
|
||||
return null;
|
||||
}
|
||||
|
||||
public static TargetManager GetInstance()
|
||||
{
|
||||
// ReSharper disable once InvertIf
|
||||
if ( _instance == null )
|
||||
{
|
||||
lock ( _lock )
|
||||
{
|
||||
if ( _instance != null )
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
|
||||
_instance = new TargetManager();
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
|
||||
public bool GetEnemy( TargetNotoriety notoFlags, TargetBodyType bodyType, TargetDistance targetDistance,
|
||||
TargetFriendType friendType = TargetFriendType.None,
|
||||
TargetInfliction inflictionType = TargetInfliction.Any )
|
||||
{
|
||||
Mobile m = GetMobile( notoFlags, bodyType, targetDistance, friendType, inflictionType,
|
||||
targetDistance == TargetDistance.Nearest ? AliasCommands.GetAlias( "enemy" ) : 0 );
|
||||
|
||||
if ( m == null )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetEnemy( m );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GetFriend( TargetNotoriety notoFlags, TargetBodyType bodyType, TargetDistance targetDistance,
|
||||
TargetFriendType friendType = TargetFriendType.Include,
|
||||
TargetInfliction inflictionType = TargetInfliction.Any )
|
||||
{
|
||||
Mobile m = GetMobile( notoFlags, bodyType, targetDistance, friendType, inflictionType,
|
||||
targetDistance == TargetDistance.Nearest ? AliasCommands.GetAlias( "friend" ) : 0 );
|
||||
|
||||
if ( m == null )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetFriend( m );
|
||||
return true;
|
||||
}
|
||||
|
||||
public Mobile GetMobile( TargetNotoriety notoFlags, TargetBodyType bodyType, TargetDistance targetDistance,
|
||||
TargetFriendType friendType, TargetInfliction inflictionType, int previousSerial = 0 )
|
||||
{
|
||||
Notoriety[] noto = NotoFlagsToArray( notoFlags );
|
||||
|
||||
Mobile m;
|
||||
|
||||
switch ( targetDistance )
|
||||
{
|
||||
case TargetDistance.Next:
|
||||
|
||||
m = GetNextMobile( noto, bodyType, MAX_DISTANCE, friendType, inflictionType );
|
||||
|
||||
break;
|
||||
case TargetDistance.Nearest:
|
||||
|
||||
Mobile previousMobile = Engine.Mobiles.GetMobile( previousSerial );
|
||||
|
||||
if ( previousMobile == null || previousMobile.Distance > MAX_DISTANCE )
|
||||
{
|
||||
m = GetClosestMobile( noto, bodyType, friendType, inflictionType );
|
||||
}
|
||||
else
|
||||
{
|
||||
m = GetNextMobile( noto, bodyType, MAX_DISTANCE, friendType, inflictionType, false,
|
||||
previousMobile );
|
||||
}
|
||||
|
||||
break;
|
||||
case TargetDistance.Closest:
|
||||
|
||||
m = GetClosestMobile( noto, bodyType, friendType, inflictionType );
|
||||
|
||||
break;
|
||||
case TargetDistance.Previous:
|
||||
|
||||
m = GetNextMobile( noto, bodyType, MAX_DISTANCE, friendType, inflictionType, true );
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException( nameof( targetDistance ), targetDistance, null );
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Notoriety[] NotoFlagsToArray( TargetNotoriety notoFlags )
|
||||
{
|
||||
List<Notoriety> notos = new List<Notoriety>();
|
||||
|
||||
if ( notoFlags.HasFlag( TargetNotoriety.Criminal ) || notoFlags.HasFlag( TargetNotoriety.Any ) )
|
||||
{
|
||||
notos.Add( Notoriety.Criminal );
|
||||
}
|
||||
|
||||
if ( notoFlags.HasFlag( TargetNotoriety.Enemy ) || notoFlags.HasFlag( TargetNotoriety.Any ) )
|
||||
{
|
||||
notos.Add( Notoriety.Enemy );
|
||||
}
|
||||
|
||||
if ( notoFlags.HasFlag( TargetNotoriety.Gray ) || notoFlags.HasFlag( TargetNotoriety.Any ) )
|
||||
{
|
||||
notos.Add( Notoriety.Attackable );
|
||||
}
|
||||
|
||||
if ( notoFlags.HasFlag( TargetNotoriety.Innocent ) || notoFlags.HasFlag( TargetNotoriety.Any ) )
|
||||
{
|
||||
notos.Add( Notoriety.Innocent );
|
||||
}
|
||||
|
||||
if ( notoFlags.HasFlag( TargetNotoriety.Murderer ) || notoFlags.HasFlag( TargetNotoriety.Any ) )
|
||||
{
|
||||
notos.Add( Notoriety.Murderer );
|
||||
}
|
||||
|
||||
if ( notoFlags.HasFlag( TargetNotoriety.Friend ) || notoFlags.HasFlag( TargetNotoriety.Any ) )
|
||||
{
|
||||
notos.Add( Notoriety.Ally );
|
||||
}
|
||||
|
||||
return notos.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MobileEnumerableExtensionMethods
|
||||
{
|
||||
public static Mobile[] ByInflication( this IEnumerable<Mobile> mobiles, TargetInfliction inflictionType )
|
||||
{
|
||||
switch ( inflictionType )
|
||||
{
|
||||
case TargetInfliction.Any:
|
||||
return mobiles.ToArray();
|
||||
case TargetInfliction.Lowest:
|
||||
return mobiles.Where( m => m.Hits < m.HitsMax && !m.IsDead ).OrderBy( m => m.Hits ).ToArray();
|
||||
case TargetInfliction.Poisoned:
|
||||
return mobiles.Where( m => m.IsPoisoned ).ToArray();
|
||||
case TargetInfliction.Mortaled:
|
||||
return mobiles.Where( m => m.IsYellowHits ).ToArray();
|
||||
case TargetInfliction.Paralyzed:
|
||||
return mobiles.Where( m => m.IsFrozen ).ToArray();
|
||||
case TargetInfliction.Dead:
|
||||
return mobiles.Where( m => m.IsDead ).ToArray();
|
||||
case TargetInfliction.Unmounted:
|
||||
return mobiles.Where( m => m.GetLayer( Layer.Mount ) == 0 ).ToArray();
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException( nameof( inflictionType ), inflictionType, null );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#region License
|
||||
|
||||
// Copyright (C) 2020 Reetus
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ClassicAssist.Data.Targeting
|
||||
{
|
||||
public class TargetQueue<T>
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private Queue<T> _queue = new Queue<T>();
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock ( _lock )
|
||||
{
|
||||
return _queue.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Enqueue( T obj )
|
||||
{
|
||||
lock ( _lock )
|
||||
{
|
||||
// If queue is full, pop n-1 entries off the queue and make a new queue
|
||||
if ( _queue.Count >= Options.CurrentOptions.MaxTargetQueueLength )
|
||||
{
|
||||
List<T> list = new List<T>();
|
||||
|
||||
for ( int i = 0; i < Options.CurrentOptions.MaxTargetQueueLength - 1; i++ )
|
||||
{
|
||||
list.Add( _queue.Dequeue() );
|
||||
}
|
||||
|
||||
_queue = new Queue<T>( list );
|
||||
}
|
||||
|
||||
_queue.Enqueue( obj );
|
||||
}
|
||||
}
|
||||
|
||||
public T Peek()
|
||||
{
|
||||
lock ( _lock )
|
||||
{
|
||||
try
|
||||
{
|
||||
return _queue.Peek();
|
||||
}
|
||||
catch ( InvalidOperationException )
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object Dequeue()
|
||||
{
|
||||
lock ( _lock )
|
||||
{
|
||||
return _queue.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock ( _lock )
|
||||
{
|
||||
_queue.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user