Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
74 changes: 70 additions & 4 deletions Source/ACE.Server/WorldObjects/Monster_Awareness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ 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.6; // Cache for 0.6 seconds

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm target-cache TTL (0.6s) vs PR summary (0.5s); align across caches.

PR description says 0.5s target cache, but code uses 0.6s. Also, distance cache elsewhere is 0.4s while the PR text mentions 0.25s. Please confirm intended values and align code/PR text accordingly.

Apply if 0.5s is desired:

-        private const double TARGET_CACHE_DURATION = 0.6; // Cache for 0.6 seconds
+        private const double TARGET_CACHE_DURATION = 0.5; // Cache for 0.5 seconds

Run to verify all TTLs:


🏁 Script executed:

#!/bin/bash
rg -nP --type=cs -C2 '(TARGET_CACHE_DURATION|DISTANCE_CACHE_DURATION)\s*=\s*[\d.]+'

Length of output: 1007


Align cache TTLs with PR description.

  • In Source/ACE.Server/WorldObjects/Monster_Awareness.cs (line 31), TARGET_CACHE_DURATION is 0.6 s but the PR summary says 0.5 s.
  • In Source/ACE.Server/WorldObjects/Monster_Navigation.cs (line 30), DISTANCE_CACHE_DURATION is 0.4 s but the PR summary says 0.25 s.

Confirm the intended durations and update the constants or the PR text so they match.

🤖 Prompt for AI Agents
In Source/ACE.Server/WorldObjects/Monster_Awareness.cs around lines 26-32 and
Source/ACE.Server/WorldObjects/Monster_Navigation.cs around line 30, the cache
duration constants do not match the PR summary; update TARGET_CACHE_DURATION
from 0.6 to 0.5 and DISTANCE_CACHE_DURATION from 0.4 to 0.25 to align with the
PR description, or if the 0.6/0.4 values were intentional, change the PR summary
to reflect the actual values—ensure both the code constants and PR text
consistently state the intended durations.

/// <summary>
/// Transitions a monster from idle to awake state
/// </summary>
Expand Down Expand Up @@ -56,6 +63,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 +152,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 +188,8 @@ public virtual bool FindNextTarget()
// although it is not truly random, it is weighted by distance
var targetDistances = BuildTargetDistance(visibleTargets);
AttackTarget = SelectWeightedDistance(targetDistances);
_lastTargetCacheTime = 0.0; // Invalidate cache when target changes
InvalidateDistanceCache(); // Also invalidate distance cache
break;

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

var lastDamager = DamageHistory.LastDamager?.TryGetAttacker() as Creature;
if (lastDamager != null)
{
AttackTarget = lastDamager;
_lastTargetCacheTime = 0.0; // Invalidate cache when target changes
InvalidateDistanceCache(); // Also invalidate distance cache
}
break;

case TargetingTactic.TopDamager:

var topDamager = DamageHistory.TopDamager?.TryGetAttacker() as Creature;
if (topDamager != null)
{
AttackTarget = topDamager;
_lastTargetCacheTime = 0.0; // Invalidate cache when target changes
InvalidateDistanceCache(); // Also invalidate distance cache
}
break;

// these below don't seem to be used in PY16 yet...
Expand All @@ -204,18 +227,24 @@ public virtual bool FindNextTarget()
// so the same player isn't always selected
var lowestLevel = visibleTargets.OrderBy(p => p.Level).FirstOrDefault();
AttackTarget = lowestLevel;
_lastTargetCacheTime = 0.0; // Invalidate cache when target changes
InvalidateDistanceCache(); // Also invalidate distance cache
break;

case TargetingTactic.Strongest:

var highestLevel = visibleTargets.OrderByDescending(p => p.Level).FirstOrDefault();
AttackTarget = highestLevel;
_lastTargetCacheTime = 0.0; // Invalidate cache when target changes
InvalidateDistanceCache(); // Also invalidate distance cache
break;

case TargetingTactic.Nearest:

var nearest = BuildTargetDistance(visibleTargets);
AttackTarget = nearest[0].Target;
_lastTargetCacheTime = 0.0; // Invalidate cache when target changes
InvalidateDistanceCache(); // Also invalidate distance cache
break;
}

Expand All @@ -234,15 +263,49 @@ 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 _cachedVisibleTargets;
}

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

// Update cache
_cachedVisibleTargets = visibleTargets;
_lastTargetCacheTime = currentTime;

return visibleTargets;
}

/// <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)
{
// ensure attackable
if (!creature.Attackable && creature.TargetingTactic == TargetingTactic.None || 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;

if (creature.Teleporting)
continue;

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

visibleTargets.Add(creature);
}

return visibleTargets;
}

Expand Down Expand Up @@ -519,7 +581,11 @@ public void FactionMob_CheckMonsters()
continue;

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

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

// ensure another faction
Expand Down
65 changes: 59 additions & 6 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.4; // Cache for 0.4 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 Down Expand Up @@ -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,7 @@ public bool IsFacing(WorldObject target)
if (target?.Location == null) return false;

var angle = GetAngle(target);
var dist = Math.Max(0, GetDistanceToTarget());
var dist = Math.Max(0, GetCachedDistanceToTarget());

// rotation accuracy?
var threshold = 5.0f;
Expand All @@ -510,7 +563,7 @@ public MovementParameters GetMovementParameters()
// set non-default params for monster movement
mvp.Flags &= ~MovementParamFlags.CanWalk;

var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && GetDistanceToTarget() <= GetSpellMaxRange()) || AiImmobile;
var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && GetCachedDistanceToTarget() <= 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