Skip to content

Commit

Permalink
Improve NPC proximity aggro responsiveness
Browse files Browse the repository at this point in the history
* `AddToAggroList` now automatically switches to aggro state and reschedule the next think tick.
* `CheckPlayerAggro` and `CheckNpcAggro` are no longer be allowed to run if we're waiting for at least one los check. This relies on `LosCheckForAggroCallback` to be called for every successful call to `SendLosCheckForAggro`.
* Replaced various explicit calls to `Think` and `AttackMostWanted` by next think tick reschedule.
  • Loading branch information
bm01 committed Jul 7, 2024
1 parent 53a4d91 commit b1184c6
Show file tree
Hide file tree
Showing 17 changed files with 62 additions and 74 deletions.
2 changes: 1 addition & 1 deletion CoreServer/ConsolePacketLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public void SendMovingObjectCreate(GameMovingObject obj) { }
public void SendWarmapUpdate(ICollection<IGameKeep> list) { }
public void SendWarmapDetailUpdate(List<List<byte>> fights, List<List<byte>> groups) { }
public void SendWarmapBonuses() { }
public void SendCheckLos(GameObject source, GameObject target, CheckLosResponse callback) { }
public bool SendCheckLos(GameObject source, GameObject target, CheckLosResponse callback) { return true; }
public void SendLivingDataUpdate(GameLiving living, bool updateStrings) { }
public void SendPlayerTitles() { }
public void SendPlayerTitleUpdate(GamePlayer player) { }
Expand Down
11 changes: 1 addition & 10 deletions GameServer/ECS-Effects/ConfusionECSEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,8 @@ public override void OnEffectPulse()
brain.ClearAggroList();
npc.StopAttack();
npc.StopCurrentSpellcast();
GameLiving target = targetList[Util.Random(targetList.Count - 1)] as GameLiving;
GameLiving target = targetList[Util.Random(targetList.Count - 1)];
brain.ForceAddToAggroList(target, 1);

if (brain.HasAggro)
{
if (brain.FSM.GetCurrentState() != brain.FSM.GetState(eFSMStateType.AGGRO))
brain.FSM.SetCurrentState(eFSMStateType.AGGRO);

brain.AttackMostWanted();
brain.Think();
}
}
}
}
Expand Down
23 changes: 6 additions & 17 deletions GameServer/ai/brain/Animist/TurretFNFBrain.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using DOL.GS;
using DOL.GS.PacketHandler;
using DOL.GS.ServerProperties;

namespace DOL.AI.Brain
Expand All @@ -21,7 +20,7 @@ public override bool CheckProximityAggro()
{
// FnF turrets need to add all players and NPCs to their aggro list to be able to switch target randomly and effectively.
CheckPlayerAggro();
CheckNPCAggro();
CheckNpcAggro();
return HasAggro;
}

Expand All @@ -40,13 +39,13 @@ protected override void CheckPlayerAggro()
continue;

if (Properties.CHECK_LOS_BEFORE_AGGRO_FNF)
player.Out.SendCheckLos(Body, player, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(player, player);
else
AddToAggroList(player, 1);
}
}

protected override void CheckNPCAggro()
protected override void CheckNpcAggro()
{
// Copy paste of 'base.CheckNPCAggro()' except we add all NPCs in range.
foreach (GameNPC npc in Body.GetNPCsInRadius((ushort) AggroRange))
Expand All @@ -61,12 +60,12 @@ protected override void CheckNPCAggro()
{
if (npc.Brain is ControlledMobBrain theirControlledNpcBrain && theirControlledNpcBrain.GetPlayerOwner() is GamePlayer theirOwner)
{
theirOwner.Out.SendCheckLos(Body, npc, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(theirOwner, npc);
continue;
}
else if (this is ControlledMobBrain ourControlledNpcBrain && ourControlledNpcBrain.GetPlayerOwner() is GamePlayer ourOwner)
{
ourOwner.Out.SendCheckLos(Body, npc, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(ourOwner, npc);
continue;
}
}
Expand All @@ -75,17 +74,7 @@ protected override void CheckNPCAggro()
}
}

protected override void LosCheckForAggroCallback(GamePlayer player, eLosCheckResponse response, ushort sourceOID, ushort targetOID)
{
// Copy paste of 'base.LosCheckForAggroCallback()' except we don't care if we already have aggro.
if (response is eLosCheckResponse.TRUE)
{
GameObject gameObject = Body.CurrentRegion.GetObject(targetOID);

if (gameObject is GameLiving gameLiving)
AddToAggroList(gameLiving, 1);
}
}
protected override bool CanAddToAggroListFromMultipleLosChecks => true;

protected override bool ShouldBeIgnoredFromAggroList(GameLiving living)
{
Expand Down
5 changes: 2 additions & 3 deletions GameServer/ai/brain/GuardBrain.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using DOL.GS;
using DOL.GS.PacketHandler;

namespace DOL.AI.Brain
{
Expand All @@ -24,12 +23,12 @@ protected override void CheckPlayerAggro()
if (player.effectListComponent.ContainsEffectForEffectType(eEffect.Shade))
continue;

player.Out.SendCheckLos(Body, player, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(player, player);
// We don't know if the LoS check will be positive, so we have to ask other players
}
}

protected override void CheckNPCAggro()
protected override void CheckNpcAggro()
{
foreach (GameNPC npc in Body.GetNPCsInRadius((ushort)AggroRange))
{
Expand Down
6 changes: 3 additions & 3 deletions GameServer/ai/brain/Guards/KeepGuardBrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ protected override void CheckPlayerAggro()
continue;

WarMapMgr.AddGroup((byte) player.CurrentZone.ID, player.X, player.Y, player.Name, (byte) player.Realm);
player.Out.SendCheckLos(Body, player, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(player, player);
// We don't know if the LoS check will be positive, so we have to ask other players
}
}

/// <summary>
/// Check area for NPCs to attack
/// </summary>
protected override void CheckNPCAggro()
protected override void CheckNpcAggro()
{
foreach (GameNPC npc in Body.GetNPCsInRadius((ushort)AggroRange))
{
Expand All @@ -108,7 +108,7 @@ protected override void CheckNPCAggro()
continue;

WarMapMgr.AddGroup((byte)player.CurrentZone.ID, player.X, player.Y, player.Name, (byte)player.Realm);
player.Out.SendCheckLos(Body, npc, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(player, npc);
// We don't know if the LoS check will be positive, so we have to ask other players
}
}
Expand Down
6 changes: 1 addition & 5 deletions GameServer/ai/brain/Scout/ScoutMobBrainState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,10 @@ void ReportToFriends()
return;

_brain.AddAggroListTo(_friend);
_friend.AttackMostWanted();

// This includes us.
foreach (StandardMobBrain otherFriendlyBrain in _friend.GetFriendlyAndAvailableBrainsInRadiusOrderedByDistance(ADDS_RADIUS, MAX_ADDS))
{
// This includes us.
_brain.AddAggroListTo(otherFriendlyBrain);
otherFriendlyBrain.AttackMostWanted();
}

_state = ScoutMobState.FIGHTING;
}
Expand Down
4 changes: 2 additions & 2 deletions GameServer/ai/brain/Special/AlluvianGlobuleBrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public override void Think()
if (!Body.attackComponent.AttackState && AggroLevel > 0)
{
CheckPlayerAggro();
CheckNPCAggro();
CheckNpcAggro();
}
if (HasAggro)
{
Expand Down Expand Up @@ -142,4 +142,4 @@ public void Grow()
hasGrown = true;
}
}
}
}
48 changes: 30 additions & 18 deletions GameServer/ai/brain/StandardMob/StandardMobBrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ public virtual bool CheckProximityAggro()
FireAmbientSentence();

// Check aggro only if our aggro list is empty and we're not in combat.
if (AggroLevel > 0 && AggroRange > 0 && !HasAggro && Body.CurrentSpellHandler == null)
if (AggroLevel > 0 && AggroRange > 0 && Body.CurrentSpellHandler == null && !HasAggro && !IsWaitingForLosCheck)
{
CheckPlayerAggro();
CheckNPCAggro();
CheckNpcAggro();
}

// Some calls rely on this method to return if there's something in the aggro list, not necessarily to perform a proximity aggro check.
Expand Down Expand Up @@ -118,20 +118,21 @@ protected virtual void CheckPlayerAggro()
continue;

if (Properties.CHECK_LOS_BEFORE_AGGRO)
// We don't know if the LoS check will be positive, so we have to ask other players
player.Out.SendCheckLos(Body, player, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(player, player);
else
{
AddToAggroList(player, 1);
return;
}

// We don't know if the LoS check will be positive, so we have to ask other players
}
}

/// <summary>
/// Check for aggro against close NPCs
/// </summary>
protected virtual void CheckNPCAggro()
protected virtual void CheckNpcAggro()
{
foreach (GameNPC npc in Body.GetNPCsInRadius((ushort) AggroRange))
{
Expand All @@ -146,12 +147,12 @@ protected virtual void CheckNPCAggro()
// Check LoS if either the target or the current mob is a pet
if (npc.Brain is ControlledMobBrain theirControlledNpcBrain && theirControlledNpcBrain.GetPlayerOwner() is GamePlayer theirOwner)
{
theirOwner.Out.SendCheckLos(Body, npc, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(theirOwner, npc);
continue;
}
else if (this is ControlledMobBrain ourControlledNpcBrain && ourControlledNpcBrain.GetPlayerOwner() is GamePlayer ourOwner)
{
ourOwner.Out.SendCheckLos(Body, npc, new CheckLosResponse(LosCheckForAggroCallback));
SendLosCheckForAggro(ourOwner, npc);
continue;
}
}
Expand Down Expand Up @@ -332,6 +333,13 @@ public void ForceAddToAggroList(GameLiving living, long aggroAmount)
}
}

// Change state and reschedule the next think tick to improve responsiveness.
if (FSM.GetCurrentState() != FSM.GetState(eFSMStateType.AGGRO) && HasAggro)
{
FSM.SetCurrentState(eFSMStateType.AGGRO);
NextThinkTick = GameLoop.GameLoopTime;
}

static AggroAmount Add(GameLiving key, long arg)
{
return new(Math.Max(0, arg));
Expand Down Expand Up @@ -389,8 +397,7 @@ public bool UnsetTemporaryAggroList()
if (FSM.GetCurrentState() != FSM.GetState(eFSMStateType.AGGRO))
FSM.SetCurrentState(eFSMStateType.AGGRO);

AttackMostWanted();
Think();
NextThinkTick = GameLoop.GameLoopTime;
}

return true;
Expand Down Expand Up @@ -440,13 +447,23 @@ public virtual void AttackMostWanted()
}

private long _isHandlingAdditionToAggroListFromLosCheck;
private long _losCheckCount;
private bool StartAddToAggroListFromLosCheck => Interlocked.Exchange(ref _isHandlingAdditionToAggroListFromLosCheck, 1) == 0; // Returns true the first time it's called.
private bool IsWaitingForLosCheck => Interlocked.Read(ref _losCheckCount) > 0;
protected virtual bool CanAddToAggroListFromMultipleLosChecks => false;

protected virtual void LosCheckForAggroCallback(GamePlayer player, eLosCheckResponse response, ushort sourceOID, ushort targetOID)
protected void SendLosCheckForAggro(GamePlayer player, GameObject target)
{
if (player.Out.SendCheckLos(Body, target, new CheckLosResponse(LosCheckForAggroCallback)))
Interlocked.Increment(ref _losCheckCount);
}

protected void LosCheckForAggroCallback(GamePlayer player, eLosCheckResponse response, ushort sourceOID, ushort targetOID)
{
// Make sure only one thread can enter this block to prevent multiple entities from being added to the aggro list.
// Otherwise mobs could kill one player and immediately go for another one.
if (response is eLosCheckResponse.TRUE && StartAddToAggroListFromLosCheck)
// This method should not be allowed to be executed at the same time as `CheckPlayerAggro` or `CheckNPCAggro`.
if (response is eLosCheckResponse.TRUE && (CanAddToAggroListFromMultipleLosChecks || StartAddToAggroListFromLosCheck))
{
if (!HasAggro)
{
Expand All @@ -458,6 +475,8 @@ protected virtual void LosCheckForAggroCallback(GamePlayer player, eLosCheckResp

_isHandlingAdditionToAggroListFromLosCheck = 0;
}

Interlocked.Decrement(ref _losCheckCount);
}

protected virtual bool ShouldBeRemovedFromAggroList(GameLiving living)
Expand Down Expand Up @@ -594,12 +613,6 @@ public virtual void OnAttackedByEnemy(AttackData ad)

if (ad.GeneratesAggro)
ConvertDamageToAggroAmount(ad.Attacker, Math.Max(1, ad.Damage + ad.CriticalDamage));

if (FSM.GetCurrentState() != FSM.GetState(eFSMStateType.AGGRO) && HasAggro)
{
FSM.SetCurrentState(eFSMStateType.AGGRO);
Think();
}
}

/// <summary>
Expand Down Expand Up @@ -698,7 +711,6 @@ protected virtual void BringFriends(GameLiving puller)
target = puller;

brain.AddToAggroList(target, 1);
brain.AttackMostWanted();
}

int GetMaxAddsCountFromBaf(GameLiving puller, out List<GamePlayer> otherTargets)
Expand Down
5 changes: 2 additions & 3 deletions GameServer/packets/Client/168/CheckLosResponseHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;

namespace DOL.GS.PacketHandler.Client.v168
{
Expand Down Expand Up @@ -45,10 +44,10 @@ public bool TryAddCallback(CheckLosResponse callback)
{
if (Callbacks == null)
{
Callbacks ??= new();
Callbacks ??= [];
Callbacks.Add(callback);
}
else if (Callbacks.Any())
else if (Callbacks.Count != 0)
Callbacks.Add(callback);
else
return false;
Expand Down
2 changes: 1 addition & 1 deletion GameServer/packets/Server/IPacketLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ void SendDialogBox(eDialogCode code, ushort data1, ushort data2, ushort data3, u
bool autoWrapText, string message);

void SendCustomDialog(string msg, CustomDialogResponse callback);
void SendCheckLos(GameObject source, GameObject target, CheckLosResponse callback);
bool SendCheckLos(GameObject source, GameObject target, CheckLosResponse callback);
void SendGuildLeaveCommand(GamePlayer invitingPlayer, string inviteMessage);
void SendGuildInviteCommand(GamePlayer invitingPlayer, string inviteMessage);
void SendQuestOfferWindow(GameNPC questNPC, GamePlayer player, RewardQuest quest);
Expand Down
8 changes: 5 additions & 3 deletions GameServer/packets/Server/PacketLib168.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1489,16 +1489,16 @@ public virtual void SendCustomDialog(string msg, CustomDialogResponse callback)
}
}

public virtual void SendCheckLos(GameObject source, GameObject target, CheckLosResponse callback)
public virtual bool SendCheckLos(GameObject source, GameObject target, CheckLosResponse callback)
{
if (m_gameClient.Player == null || source == null || target == null)
return;
return false;

ushort sourceObjectId = (ushort) source.ObjectID;
ushort targetObjectId = (ushort) target.ObjectID;

if (!HandleCallback(m_gameClient, sourceObjectId, targetObjectId, callback))
return;
return false;

using (var pak = new GSTCPPacketOut(GetPacketCode(eServerPackets.CheckLOSRequest)))
{
Expand All @@ -1509,6 +1509,8 @@ public virtual void SendCheckLos(GameObject source, GameObject target, CheckLosR
SendTCP(pak);
}

return true;

static bool HandleCallback(GameClient client, ushort sourceObjectId, ushort targetObjectId, CheckLosResponse callback)
{
CheckLosResponseHandler.TimeoutTimer timer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public void ResistsTwo()
Body.AbilityBonus[(int)eProperty.StyleAbsorb] = 10;
}
}
protected override void CheckNPCAggro()
protected override void CheckNpcAggro()
{
if (Body.attackComponent.AttackState)
return;
Expand Down
2 changes: 1 addition & 1 deletion GameServer/scripts/namedmobs/Dodens/MistressOfRunes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public override void Think()
base.Think();
}

protected override void CheckNPCAggro()
protected override void CheckNpcAggro()
{
if (Body.attackComponent.AttackState)
return;
Expand Down
2 changes: 1 addition & 1 deletion GameServer/spells/Masterlevel/Convoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ public override int AggroRange
{
get { return 400; }
}
protected override void CheckNPCAggro()
protected override void CheckNpcAggro()
{
//Check if we are already attacking, return if yes
if (Body.attackComponent.AttackState)
Expand Down
Loading

0 comments on commit b1184c6

Please sign in to comment.