CreatureAI Cleanups
This commit is contained in:
@@ -435,6 +435,15 @@ namespace Framework.Constants
|
|||||||
Other
|
Other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum SelectTargetMethod
|
||||||
|
{
|
||||||
|
Random = 0, // just pick a random target
|
||||||
|
MaxThreat, // prefer targets higher in the threat list
|
||||||
|
MinThreat, // prefer targets lower in the threat list
|
||||||
|
MaxDistance, // prefer targets further from us
|
||||||
|
MinDistance // prefer targets closer to us
|
||||||
|
}
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum GroupAIFlags
|
public enum GroupAIFlags
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,11 +120,13 @@ namespace Framework.Constants
|
|||||||
NoDamage = 4, // used also in case when damage applied to health but not applied to spell channelInterruptFlags/etc
|
NoDamage = 4, // used also in case when damage applied to health but not applied to spell channelInterruptFlags/etc
|
||||||
Self = 5
|
Self = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WeaponDamageRange
|
public enum WeaponDamageRange
|
||||||
{
|
{
|
||||||
MinDamage,
|
MinDamage,
|
||||||
MaxDamage
|
MaxDamage
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum UnitMods
|
public enum UnitMods
|
||||||
{
|
{
|
||||||
StatStrength, // STAT_STRENGTH..UNIT_MOD_STAT_INTELLECT must be in existed order, it's accessed by index values of Stats enum.
|
StatStrength, // STAT_STRENGTH..UNIT_MOD_STAT_INTELLECT must be in existed order, it's accessed by index values of Stats enum.
|
||||||
|
|||||||
@@ -530,254 +530,4 @@ namespace Game.AI
|
|||||||
return _aiSpellInfo.LookupByKey((spellId, difficulty));
|
return _aiSpellInfo.LookupByKey((spellId, difficulty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SelectTargetMethod
|
|
||||||
{
|
|
||||||
Random = 0, // just pick a random target
|
|
||||||
MaxThreat, // prefer targets higher in the threat list
|
|
||||||
MinThreat, // prefer targets lower in the threat list
|
|
||||||
MaxDistance, // prefer targets further from us
|
|
||||||
MinDistance // prefer targets closer to us
|
|
||||||
}
|
|
||||||
|
|
||||||
// default predicate function to select target based on distance, player and/or aura criteria
|
|
||||||
public class DefaultTargetSelector : ICheck<Unit>
|
|
||||||
{
|
|
||||||
Unit _me;
|
|
||||||
float _dist;
|
|
||||||
bool _playerOnly;
|
|
||||||
Unit _exception;
|
|
||||||
int _aura;
|
|
||||||
|
|
||||||
/// <param name="unit">the reference unit</param>
|
|
||||||
/// <param name="dist">if 0: ignored, if > 0: maximum distance to the reference unit, if < 0: minimum distance to the reference unit</param>
|
|
||||||
/// <param name="playerOnly">self explaining</param>
|
|
||||||
/// <param name="withTank">allow current tank to be selected</param>
|
|
||||||
/// <param name="aura">if 0: ignored, if > 0: the target shall have the aura, if < 0, the target shall NOT have the aura</param>
|
|
||||||
public DefaultTargetSelector(Unit unit, float dist, bool playerOnly, bool withTank, int aura)
|
|
||||||
{
|
|
||||||
_me = unit;
|
|
||||||
_dist = dist;
|
|
||||||
_playerOnly = playerOnly;
|
|
||||||
_exception = !withTank ? unit.GetThreatManager().GetLastVictim() : null;
|
|
||||||
_aura = aura;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Invoke(Unit target)
|
|
||||||
{
|
|
||||||
if (_me == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (target == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_exception != null && target == _exception)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_playerOnly && !target.IsTypeId(TypeId.Player))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_dist > 0.0f && !_me.IsWithinCombatRange(target, _dist))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_dist < 0.0f && _me.IsWithinCombatRange(target, -_dist))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_aura != 0)
|
|
||||||
{
|
|
||||||
if (_aura > 0)
|
|
||||||
{
|
|
||||||
if (!target.HasAura((uint)_aura))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (target.HasAura((uint)-_aura))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target selector for spell casts checking range, auras and attributes
|
|
||||||
// todo Add more checks from Spell.CheckCast
|
|
||||||
public class SpellTargetSelector : ICheck<Unit>
|
|
||||||
{
|
|
||||||
Unit _caster;
|
|
||||||
SpellInfo _spellInfo;
|
|
||||||
|
|
||||||
public SpellTargetSelector(Unit caster, uint spellId)
|
|
||||||
{
|
|
||||||
_caster = caster;
|
|
||||||
_spellInfo = Global.SpellMgr.GetSpellInfo(spellId, caster.GetMap().GetDifficultyID());
|
|
||||||
|
|
||||||
Cypher.Assert(_spellInfo != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Invoke(Unit target)
|
|
||||||
{
|
|
||||||
if (target == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_spellInfo.CheckTarget(_caster, target) != SpellCastResult.SpellCastOk)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// copypasta from Spell.CheckRange
|
|
||||||
float minRange = 0.0f;
|
|
||||||
float maxRange = 0.0f;
|
|
||||||
float rangeMod = 0.0f;
|
|
||||||
if (_spellInfo.RangeEntry != null)
|
|
||||||
{
|
|
||||||
if (_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Melee))
|
|
||||||
{
|
|
||||||
rangeMod = _caster.GetCombatReach() + 4.0f / 3.0f;
|
|
||||||
rangeMod += target.GetCombatReach();
|
|
||||||
|
|
||||||
rangeMod = Math.Max(rangeMod, SharedConst.NominalMeleeRange);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
float meleeRange = 0.0f;
|
|
||||||
if (_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Ranged))
|
|
||||||
{
|
|
||||||
meleeRange = _caster.GetCombatReach() + 4.0f / 3.0f;
|
|
||||||
meleeRange += target.GetCombatReach();
|
|
||||||
|
|
||||||
meleeRange = Math.Max(meleeRange, SharedConst.NominalMeleeRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
minRange = _caster.GetSpellMinRangeForTarget(target, _spellInfo) + meleeRange;
|
|
||||||
maxRange = _caster.GetSpellMaxRangeForTarget(target, _spellInfo);
|
|
||||||
|
|
||||||
rangeMod = _caster.GetCombatReach();
|
|
||||||
rangeMod += target.GetCombatReach();
|
|
||||||
|
|
||||||
if (minRange > 0.0f && !_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Ranged))
|
|
||||||
minRange += rangeMod;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_caster.IsMoving() && target.IsMoving() && !_caster.IsWalking() && !target.IsWalking() &&
|
|
||||||
(_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Melee) || target.IsTypeId(TypeId.Player)))
|
|
||||||
rangeMod += 8.0f / 3.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxRange += rangeMod;
|
|
||||||
|
|
||||||
minRange *= minRange;
|
|
||||||
maxRange *= maxRange;
|
|
||||||
|
|
||||||
if (target != _caster)
|
|
||||||
{
|
|
||||||
if (_caster.GetExactDistSq(target) > maxRange)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (minRange > 0.0f && _caster.GetExactDistSq(target) < minRange)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Very simple target selector, will just skip main target
|
|
||||||
// NOTE: When passing to UnitAI.SelectTarget remember to use 0 as position for random selection
|
|
||||||
// because tank will not be in the temporary list
|
|
||||||
public class NonTankTargetSelector : ICheck<Unit>
|
|
||||||
{
|
|
||||||
Unit _source;
|
|
||||||
bool _playerOnly;
|
|
||||||
|
|
||||||
public NonTankTargetSelector(Unit source, bool playerOnly = true)
|
|
||||||
{
|
|
||||||
_source = source;
|
|
||||||
_playerOnly = playerOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Invoke(Unit target)
|
|
||||||
{
|
|
||||||
if (target == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_playerOnly && !target.IsTypeId(TypeId.Player))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Unit currentVictim = _source.GetThreatManager().GetCurrentVictim();
|
|
||||||
if (currentVictim != null)
|
|
||||||
return target != currentVictim;
|
|
||||||
|
|
||||||
return target != _source.GetVictim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple selector for units using mana
|
|
||||||
class PowerUsersSelector : ICheck<Unit>
|
|
||||||
{
|
|
||||||
Unit _me;
|
|
||||||
PowerType _power;
|
|
||||||
float _dist;
|
|
||||||
bool _playerOnly;
|
|
||||||
|
|
||||||
public PowerUsersSelector(Unit unit, PowerType power, float dist, bool playerOnly)
|
|
||||||
{
|
|
||||||
_me = unit;
|
|
||||||
_power = power;
|
|
||||||
_dist = dist;
|
|
||||||
_playerOnly = playerOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Invoke(Unit target)
|
|
||||||
{
|
|
||||||
if (_me == null || target == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (target.GetPowerType() != _power)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_playerOnly && target.GetTypeId() != TypeId.Player)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_dist > 0.0f && !_me.IsWithinCombatRange(target, _dist))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_dist < 0.0f && _me.IsWithinCombatRange(target, -_dist))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FarthestTargetSelector : ICheck<Unit>
|
|
||||||
{
|
|
||||||
Unit _me;
|
|
||||||
float _dist;
|
|
||||||
bool _playerOnly;
|
|
||||||
bool _inLos;
|
|
||||||
|
|
||||||
public FarthestTargetSelector(Unit unit, float dist, bool playerOnly, bool inLos)
|
|
||||||
{
|
|
||||||
_me = unit;
|
|
||||||
_dist = dist;
|
|
||||||
_playerOnly = playerOnly;
|
|
||||||
_inLos = inLos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Invoke(Unit target)
|
|
||||||
{
|
|
||||||
if (_me == null || target == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_playerOnly && target.GetTypeId() != TypeId.Player)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_dist > 0.0f && !_me.IsWithinCombatRange(target, _dist))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_inLos && !_me.IsWithinLOSInMap(target))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
// Copyright (c) CypherCore <http://github.com/CypherCore> All rights reserved.
|
||||||
|
// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
|
using Framework.Constants;
|
||||||
|
using Game.Combat;
|
||||||
|
using Game.Entities;
|
||||||
|
using Game.Spells;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Game.AI
|
||||||
|
{
|
||||||
|
// default predicate function to select target based on distance, player and/or aura criteria
|
||||||
|
public class DefaultTargetSelector : ICheck<Unit>
|
||||||
|
{
|
||||||
|
Unit _me;
|
||||||
|
float _dist;
|
||||||
|
bool _playerOnly;
|
||||||
|
Unit _exception;
|
||||||
|
int _aura;
|
||||||
|
|
||||||
|
/// <param name="unit">the reference unit</param>
|
||||||
|
/// <param name="dist">if 0: ignored, if > 0: maximum distance to the reference unit, if < 0: minimum distance to the reference unit</param>
|
||||||
|
/// <param name="playerOnly">self explaining</param>
|
||||||
|
/// <param name="withTank">allow current tank to be selected</param>
|
||||||
|
/// <param name="aura">if 0: ignored, if > 0: the target shall have the aura, if < 0, the target shall NOT have the aura</param>
|
||||||
|
public DefaultTargetSelector(Unit unit, float dist, bool playerOnly, bool withTank, int aura)
|
||||||
|
{
|
||||||
|
_me = unit;
|
||||||
|
_dist = dist;
|
||||||
|
_playerOnly = playerOnly;
|
||||||
|
_exception = !withTank ? unit.GetThreatManager().GetLastVictim() : null;
|
||||||
|
_aura = aura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Invoke(Unit target)
|
||||||
|
{
|
||||||
|
if (_me == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_exception != null && target == _exception)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_playerOnly && !target.IsTypeId(TypeId.Player))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_dist > 0.0f && !_me.IsWithinCombatRange(target, _dist))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_dist < 0.0f && _me.IsWithinCombatRange(target, -_dist))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_aura != 0)
|
||||||
|
{
|
||||||
|
if (_aura > 0)
|
||||||
|
{
|
||||||
|
if (!target.HasAura((uint)_aura))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (target.HasAura((uint)-_aura))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target selector for spell casts checking range, auras and attributes
|
||||||
|
// todo Add more checks from Spell.CheckCast
|
||||||
|
public class SpellTargetSelector : ICheck<Unit>
|
||||||
|
{
|
||||||
|
Unit _caster;
|
||||||
|
SpellInfo _spellInfo;
|
||||||
|
|
||||||
|
public SpellTargetSelector(Unit caster, uint spellId)
|
||||||
|
{
|
||||||
|
_caster = caster;
|
||||||
|
_spellInfo = Global.SpellMgr.GetSpellInfo(spellId, caster.GetMap().GetDifficultyID());
|
||||||
|
|
||||||
|
Cypher.Assert(_spellInfo != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Invoke(Unit target)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_spellInfo.CheckTarget(_caster, target) != SpellCastResult.SpellCastOk)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// copypasta from Spell.CheckRange
|
||||||
|
float minRange = 0.0f;
|
||||||
|
float maxRange = 0.0f;
|
||||||
|
float rangeMod = 0.0f;
|
||||||
|
if (_spellInfo.RangeEntry != null)
|
||||||
|
{
|
||||||
|
if (_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Melee))
|
||||||
|
{
|
||||||
|
rangeMod = _caster.GetCombatReach() + 4.0f / 3.0f;
|
||||||
|
rangeMod += target.GetCombatReach();
|
||||||
|
|
||||||
|
rangeMod = Math.Max(rangeMod, SharedConst.NominalMeleeRange);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float meleeRange = 0.0f;
|
||||||
|
if (_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Ranged))
|
||||||
|
{
|
||||||
|
meleeRange = _caster.GetCombatReach() + 4.0f / 3.0f;
|
||||||
|
meleeRange += target.GetCombatReach();
|
||||||
|
|
||||||
|
meleeRange = Math.Max(meleeRange, SharedConst.NominalMeleeRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
minRange = _caster.GetSpellMinRangeForTarget(target, _spellInfo) + meleeRange;
|
||||||
|
maxRange = _caster.GetSpellMaxRangeForTarget(target, _spellInfo);
|
||||||
|
|
||||||
|
rangeMod = _caster.GetCombatReach();
|
||||||
|
rangeMod += target.GetCombatReach();
|
||||||
|
|
||||||
|
if (minRange > 0.0f && !_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Ranged))
|
||||||
|
minRange += rangeMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_caster.IsMoving() && target.IsMoving() && !_caster.IsWalking() && !target.IsWalking() &&
|
||||||
|
(_spellInfo.RangeEntry.Flags.HasAnyFlag(SpellRangeFlag.Melee) || target.IsTypeId(TypeId.Player)))
|
||||||
|
rangeMod += 8.0f / 3.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRange += rangeMod;
|
||||||
|
|
||||||
|
minRange *= minRange;
|
||||||
|
maxRange *= maxRange;
|
||||||
|
|
||||||
|
if (target != _caster)
|
||||||
|
{
|
||||||
|
if (_caster.GetExactDistSq(target) > maxRange)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (minRange > 0.0f && _caster.GetExactDistSq(target) < minRange)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Very simple target selector, will just skip main target
|
||||||
|
// NOTE: When passing to UnitAI.SelectTarget remember to use 0 as position for random selection
|
||||||
|
// because tank will not be in the temporary list
|
||||||
|
public class NonTankTargetSelector : ICheck<Unit>
|
||||||
|
{
|
||||||
|
Unit _source;
|
||||||
|
bool _playerOnly;
|
||||||
|
|
||||||
|
public NonTankTargetSelector(Unit source, bool playerOnly = true)
|
||||||
|
{
|
||||||
|
_source = source;
|
||||||
|
_playerOnly = playerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Invoke(Unit target)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_playerOnly && !target.IsTypeId(TypeId.Player))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Unit currentVictim = _source.GetThreatManager().GetCurrentVictim();
|
||||||
|
if (currentVictim != null)
|
||||||
|
return target != currentVictim;
|
||||||
|
|
||||||
|
return target != _source.GetVictim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple selector for units using mana
|
||||||
|
class PowerUsersSelector : ICheck<Unit>
|
||||||
|
{
|
||||||
|
Unit _me;
|
||||||
|
PowerType _power;
|
||||||
|
float _dist;
|
||||||
|
bool _playerOnly;
|
||||||
|
|
||||||
|
public PowerUsersSelector(Unit unit, PowerType power, float dist, bool playerOnly)
|
||||||
|
{
|
||||||
|
_me = unit;
|
||||||
|
_power = power;
|
||||||
|
_dist = dist;
|
||||||
|
_playerOnly = playerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Invoke(Unit target)
|
||||||
|
{
|
||||||
|
if (_me == null || target == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (target.GetPowerType() != _power)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_playerOnly && target.GetTypeId() != TypeId.Player)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_dist > 0.0f && !_me.IsWithinCombatRange(target, _dist))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_dist < 0.0f && _me.IsWithinCombatRange(target, -_dist))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FarthestTargetSelector : ICheck<Unit>
|
||||||
|
{
|
||||||
|
Unit _me;
|
||||||
|
float _dist;
|
||||||
|
bool _playerOnly;
|
||||||
|
bool _inLos;
|
||||||
|
|
||||||
|
public FarthestTargetSelector(Unit unit, float dist, bool playerOnly, bool inLos)
|
||||||
|
{
|
||||||
|
_me = unit;
|
||||||
|
_dist = dist;
|
||||||
|
_playerOnly = playerOnly;
|
||||||
|
_inLos = inLos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Invoke(Unit target)
|
||||||
|
{
|
||||||
|
if (_me == null || target == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_playerOnly && target.GetTypeId() != TypeId.Player)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_dist > 0.0f && !_me.IsWithinCombatRange(target, _dist))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_inLos && !_me.IsWithinLOSInMap(target))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user