Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
85 changes: 81 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,22 @@ 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()
{
_lastTargetCacheTime = 0.0;
InvalidateDistanceCache();
}

/// <summary>
/// Transitions a monster from idle to awake state
/// </summary>
Expand Down Expand Up @@ -56,6 +72,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 +161,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 +197,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 +208,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 +233,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 +266,57 @@ 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 with defensive copy
_cachedVisibleTargets = new List<Creature>(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)
{
// 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 +346,6 @@ public List<Creature> GetAttackTargets()

visibleTargets.Add(creature);
}

return visibleTargets;
}

Expand Down Expand Up @@ -519,7 +592,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.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 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