Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Source/ACE.Server/Command/Handlers/AdminCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2200,6 +2200,7 @@ private static void DoCopyChar(Session session, string existingCharName, uint ex
});
}


/// <summary>
/// Creates an object or objects in the world
/// </summary>
Expand Down
90 changes: 85 additions & 5 deletions Source/ACE.Server/WorldObjects/Monster_Awareness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ partial class Creature
/// </summary>
public bool IsAwake = false;

/// <summary>
/// Cache for visible targets to reduce expensive lookups
/// </summary>
private List<Creature> _cachedVisibleTargets = new List<Creature>();
private double _lastTargetCacheTime = 0.0;
private const double TARGET_CACHE_DURATION = 0.5; // Cache for 0.5 seconds

/// <summary>
/// Invalidates both target and distance caches
/// </summary>
private void InvalidateTargetCaches()
{
_cachedVisibleTargets.Clear();
_lastTargetCacheTime = 0.0;
InvalidateDistanceCache();
}

/// <summary>
/// Transitions a monster from idle to awake state
/// </summary>
Expand Down Expand Up @@ -56,6 +73,11 @@ public virtual void Sleep()
IsMoving = false;
MonsterState = State.Idle;

// Clear target cache when sleeping
_cachedVisibleTargets.Clear();
_lastTargetCacheTime = 0.0;
InvalidateDistanceCache(); // Also clear distance cache

PhysicsObj.CachedVelocity = Vector3.Zero;

ClearRetaliateTargets();
Expand Down Expand Up @@ -140,7 +162,8 @@ public virtual bool FindNextTarget()
SelectTargetingTactic();
SetNextTargetTime();

var visibleTargets = GetAttackTargets();
// Don't use cached targets for critical target finding decisions
var visibleTargets = GetAttackTargetsUncached();
if (visibleTargets.Count == 0)
{
if (MonsterState != State.Return)
Expand Down Expand Up @@ -175,6 +198,7 @@ public virtual bool FindNextTarget()
// although it is not truly random, it is weighted by distance
var targetDistances = BuildTargetDistance(visibleTargets);
AttackTarget = SelectWeightedDistance(targetDistances);
InvalidateTargetCaches();
break;

case TargetingTactic.Focused:
Expand All @@ -185,14 +209,20 @@ public virtual bool FindNextTarget()

var lastDamager = DamageHistory.LastDamager?.TryGetAttacker() as Creature;
if (lastDamager != null)
{
AttackTarget = lastDamager;
InvalidateTargetCaches();
}
break;

case TargetingTactic.TopDamager:

var topDamager = DamageHistory.TopDamager?.TryGetAttacker() as Creature;
if (topDamager != null)
{
AttackTarget = topDamager;
InvalidateTargetCaches();
}
break;

// these below don't seem to be used in PY16 yet...
Expand All @@ -204,18 +234,21 @@ public virtual bool FindNextTarget()
// so the same player isn't always selected
var lowestLevel = visibleTargets.OrderBy(p => p.Level).FirstOrDefault();
AttackTarget = lowestLevel;
InvalidateTargetCaches();
break;

case TargetingTactic.Strongest:

var highestLevel = visibleTargets.OrderByDescending(p => p.Level).FirstOrDefault();
AttackTarget = highestLevel;
InvalidateTargetCaches();
break;

case TargetingTactic.Nearest:

var nearest = BuildTargetDistance(visibleTargets);
AttackTarget = nearest[0].Target;
InvalidateTargetCaches();
break;
}

Expand All @@ -234,15 +267,58 @@ public virtual bool FindNextTarget()

/// <summary>
/// Returns a list of attackable targets currently visible to this monster
/// Uses caching to reduce expensive lookups
/// </summary>
public List<Creature> GetAttackTargets()
{
var currentTime = Timers.RunningTime;

// Check if cache is still valid
if (_lastTargetCacheTime > 0.0 && (currentTime - _lastTargetCacheTime) < TARGET_CACHE_DURATION)
{
return new List<Creature>(_cachedVisibleTargets);
}

// Cache expired, refresh it
var visibleTargets = GetAttackTargetsUncached();

// Update cache reusing the list to reduce allocations
_cachedVisibleTargets.Clear();
_cachedVisibleTargets.AddRange(visibleTargets);
_lastTargetCacheTime = currentTime;

return new List<Creature>(_cachedVisibleTargets);
}

/// <summary>
/// Returns a list of attackable targets currently visible to this monster
/// Always performs fresh calculation (no caching)
/// </summary>
public List<Creature> GetAttackTargetsUncached()
{
var visibleTargets = new List<Creature>();
var listOfCreatures = PhysicsObj.ObjMaint.GetVisibleTargetsValuesOfTypeCreature();

foreach (var creature in listOfCreatures)
{
// exclude dead creatures
if (creature.IsDead)
continue;

// ensure attackable
if (!creature.Attackable && creature.TargetingTactic == TargetingTactic.None || creature.Teleporting) continue;
if (!creature.Attackable)
continue;

// hidden players shouldn't be valid visible targets
if (creature is Player p && (p.Hidden ?? false))
continue;

// Only apply TargetingTactic-based skip to non-players (players are excluded above)
if (creature.TargetingTactic == TargetingTactic.None && !(creature is Player))
continue;

if (creature.Teleporting)
continue;

// ensure within 'detection radius' ?
var chaseDistSq = creature == AttackTarget ? MaxChaseRangeSq : VisualAwarenessRangeSq;
Expand Down Expand Up @@ -272,7 +348,6 @@ public List<Creature> GetAttackTargets()

visibleTargets.Add(creature);
}

return visibleTargets;
}

Expand Down Expand Up @@ -518,8 +593,13 @@ public void FactionMob_CheckMonsters()
if (creature is Player || creature is CombatPet)
continue;

// ensure attackable
if (creature.IsDead || !creature.Attackable && creature.TargetingTactic == TargetingTactic.None || creature.Teleporting)
// ensure valid/attackable
if (creature.IsDead || creature.Teleporting)
continue;
if (!creature.Attackable)
continue;
// Don't skip players based on TargetingTactic - that's for monster behavior, not target validity
if (creature.TargetingTactic == TargetingTactic.None && !(creature is Player))
continue;

// ensure another faction
Expand Down
74 changes: 66 additions & 8 deletions Source/ACE.Server/WorldObjects/Monster_Navigation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,59 @@ partial class Creature
public static readonly float MaxChaseRange = 96.0f;
public static readonly float MaxChaseRangeSq = MaxChaseRange * MaxChaseRange;

/// <summary>
/// Cache for physics calculations to reduce expensive operations
/// </summary>
private float _cachedDistanceToTarget = -1.0f;
private double _lastDistanceCacheTime = 0.0;
private const double DISTANCE_CACHE_DURATION = 0.25; // Cache for 0.25 seconds

/// <summary>
/// Invalidate distance cache when target changes
/// </summary>
public void InvalidateDistanceCache()
{
_cachedDistanceToTarget = -1.0f;
_lastDistanceCacheTime = 0.0;
}

/// <summary>
/// Cached distance calculation to reduce expensive physics operations
/// </summary>
public float GetCachedDistanceToTarget()
{
var currentTime = Timers.RunningTime;

// Check if cache is still valid
if (currentTime - _lastDistanceCacheTime < DISTANCE_CACHE_DURATION && _cachedDistanceToTarget >= 0)
{
return _cachedDistanceToTarget;
}

// Cache expired or invalid, calculate new distance
var myPhysics = PhysicsObj;
var target = AttackTarget;
if (target == null)
{
_cachedDistanceToTarget = float.MaxValue;
}
else
{
var targetPhysics = target.PhysicsObj;
if (myPhysics == null || targetPhysics == null)
{
_cachedDistanceToTarget = float.MaxValue;
}
else
{
_cachedDistanceToTarget = (float)myPhysics.get_distance_to_object(targetPhysics, true);
}
}

_lastDistanceCacheTime = currentTime;
return _cachedDistanceToTarget;
}

/// <summary>
/// Determines if a monster is within melee range of target
/// </summary>
Expand Down Expand Up @@ -94,7 +147,7 @@ public void StartTurn()
IsTurning = true;

// send network actions
var targetDist = GetDistanceToTarget();
var targetDist = GetCachedDistanceToTarget();
var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && targetDist <= GetSpellMaxRange()) || AiImmobile;
if (turnTo)
TurnTo(AttackTarget);
Expand All @@ -111,7 +164,7 @@ public void StartTurn()
// Initialize stuck detection
LastStuckCheckTime = Timers.RunningTime;

var mvp = GetMovementParameters();
var mvp = GetMovementParameters(targetDist);
if (turnTo)
PhysicsObj.TurnToObject(AttackTarget.PhysicsObj, mvp);
else
Expand All @@ -135,7 +188,7 @@ public override void OnMoveComplete(WeenieError status)

if (AiImmobile && CurrentAttack == CombatType.Melee)
{
var targetDist = GetDistanceToTarget();
var targetDist = GetCachedDistanceToTarget();
if (targetDist > MaxRange)
ResetAttack();
}
Expand All @@ -161,15 +214,15 @@ public float EstimateTurnTo()
/// </summary>
public bool IsMeleeRange()
{
return GetDistanceToTarget() <= MaxMeleeRange;
return GetCachedDistanceToTarget() <= MaxMeleeRange;
}

/// <summary>
/// Returns TRUE if monster in range for current attack type
/// </summary>
public bool IsAttackRange()
{
return GetDistanceToTarget() <= MaxRange;
return GetCachedDistanceToTarget() <= MaxRange;
}

/// <summary>
Expand Down Expand Up @@ -487,7 +540,11 @@ public bool IsFacing(WorldObject target)
if (target?.Location == null) return false;

var angle = GetAngle(target);
var dist = Math.Max(0, GetDistanceToTarget());
var dist = target == AttackTarget
? Math.Max(0, GetCachedDistanceToTarget())
: (PhysicsObj != null && target?.PhysicsObj != null
? (float)PhysicsObj.get_distance_to_object(target.PhysicsObj, true)
: float.MaxValue);

// rotation accuracy?
var threshold = 5.0f;
Expand All @@ -503,14 +560,15 @@ public bool IsFacing(WorldObject target)
return angle < threshold;
}

public MovementParameters GetMovementParameters()
public MovementParameters GetMovementParameters(float? targetDistance = null)
{
var mvp = new MovementParameters();

// set non-default params for monster movement
mvp.Flags &= ~MovementParamFlags.CanWalk;

var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && GetDistanceToTarget() <= GetSpellMaxRange()) || AiImmobile;
var distance = targetDistance ?? GetCachedDistanceToTarget();
var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && distance <= GetSpellMaxRange()) || AiImmobile;

if (!turnTo)
mvp.Flags |= MovementParamFlags.FailWalk | MovementParamFlags.UseFinalHeading | MovementParamFlags.Sticky | MovementParamFlags.MoveAway;
Expand Down
2 changes: 1 addition & 1 deletion Source/ACE.Server/WorldObjects/Monster_Tick.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace ACE.Server.WorldObjects
{
partial class Creature
{
protected const double monsterTickInterval = 0.2;
protected const double monsterTickInterval = 0.3;

public double NextMonsterTickTime;

Expand Down