From f211607d63c80a3bb3eaa781acd4b81706eadbf1 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:22:22 -0700 Subject: [PATCH] perf(ai): make IsEnemy the single authority for Honor/Ethereal Voyage (#2655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Profiling with thousands of creatures in range of each other showed `BaseAI.IsInvalidFactionTarget` checking Ethereal Voyage and active Honor on every acquisition candidate and then calling `BaseCreature.IsEnemy`, which checked both again. This makes `IsEnemy` the single authority and removes the duplicate per-pair work. ## Changes - **`BaseCreature.IsEnemy`**: the Ethereal Voyage check sat below the `m is not BaseCreature` early return, so it never reached players — the only mobiles that cast it. It is hoisted next to the Honor veto. `GetMaster()` was called three times per creature pair (inside `Ethics.Player.Find(m, true)` and twice at the bottom); it is now computed once. - **`BaseAI.IsInvalidFactionTarget`**: reduced to `IsFriend` / `IsEnemy` / `CanBeHarmful`. The removed `Combatant != m` Honor exception was dead code — `IsEnemy` vetoed Honor unconditionally on the following line. - **`Ethics.Player.Find`** / **`TransformationSpellHelper.GetContext`**: small simplifications on the same path. ## Behavior - `ShouldAcquireOnApproach` and `OnAggressiveAction` only consult `IsEnemy`, so movement-triggered acquisition now respects Ethereal Voyage for players (previously a player under Ethereal Voyage walking past a monster was still acquired on approach). - `BaseFactionGuard.IsEnemy` does not call base, so faction guards no longer skip honoring/voyaging enemy-faction players. Accepted: both mechanics describe monsters, not guards. - `HealerAI`/`BerserkAI`/`PredatorAI` (`bFacFriend`) callers are unaffected — `IsFriend` already required a `BaseCreature`, so players were excluded before these checks ran. `MilitiaFighter`/`MilitiaCanoneer` return `false` for all players, so they are unaffected too. ## Testing - `dotnet build` clean. - Pure predicate reorder; no new tests. --- .../UOContent/Engines/Ethics/Core/Player.cs | 14 ++----------- .../UOContent/Mobiles/AI/BaseAI/BaseAI.cs | 20 +++---------------- Projects/UOContent/Mobiles/BaseCreature.cs | 17 ++++++++-------- Projects/UOContent/Spells/Base/SpellHelper.cs | 7 +------ 4 files changed, 14 insertions(+), 44 deletions(-) diff --git a/Projects/UOContent/Engines/Ethics/Core/Player.cs b/Projects/UOContent/Engines/Ethics/Core/Player.cs index d8aad9eb3..be530dddf 100644 --- a/Projects/UOContent/Engines/Ethics/Core/Player.cs +++ b/Projects/UOContent/Engines/Ethics/Core/Player.cs @@ -93,19 +93,9 @@ public partial class Player : EthicsEntity public static Player Find(Mobile mob, bool inherit) { - var pm = mob as PlayerMobile; - - if (pm == null) + if ((inherit ? (mob as BaseCreature)?.GetMaster() ?? mob : mob) is not PlayerMobile pm) { - if (inherit && mob is BaseCreature bc) - { - pm = bc.GetMaster() as PlayerMobile; - } - - if (pm == null) - { - return null; - } + return null; } var pl = pm.EthicPlayer; diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs index 3b5e1dc5e..9b42136a6 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs @@ -16,10 +16,8 @@ using System; using Server.Engines.Quests.Necro; using Server.Engines.Spawners; -using Server.Engines.Virtues; using Server.Factions; using Server.Spells; -using Server.Spells.Spellweaving; using Server.Targets; namespace Server.Mobiles; @@ -998,21 +996,9 @@ public abstract partial class BaseAI Mobile.IsAnimatedDead && (pm != null || bc?.IsAnimatedDead == true || bc?.Controlled == true); } - private bool IsInvalidFactionTarget(Mobile m, bool bFacFriend, bool bFacFoe) - { - if (bFacFriend && !Mobile.IsFriend(m)) - { - return true; - } - - if (TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell)) || - Mobile.Combatant != m && VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true) - { - return true; - } - - return bFacFoe && (!Mobile.IsEnemy(m) || !bFacFriend && !Mobile.CanBeHarmful(m, false)); - } + private bool IsInvalidFactionTarget(Mobile m, bool bFacFriend, bool bFacFoe) => + bFacFriend && !Mobile.IsFriend(m) || + bFacFoe && (!Mobile.IsEnemy(m) || !bFacFriend && !Mobile.CanBeHarmful(m, false)); private bool IsInvalidFightModeTarget(Mobile m, FightMode acqType, BaseCreature bc) { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 6aa05c1f7..9d7e77959 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -1704,36 +1704,35 @@ namespace Server.Mobiles return false; } + var c = m as BaseCreature; + var cMaster = c?.GetMaster(); + var ourEthic = EthicAllegiance; - var pl = Ethics.Player.Find(m, true); + var pl = Ethics.Player.Find(cMaster ?? m, false); if (pl?.IsShielded == true && (ourEthic == null || ourEthic == pl.Ethic)) { return false; } - if (VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true) + // Player-only protections; they must veto before the non-creature early return below. + if (VirtueSystem.GetVirtues(m as PlayerMobile)?.HonorActive == true || + TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell))) { return false; } - if (m is not BaseCreature c || m is MilitiaFighter) + if (c == null || m is MilitiaFighter) { return true; } - if (TransformationSpellHelper.UnderTransformation(m, typeof(EtherealVoyageSpell))) - { - return false; - } - if (_team != c.Team || FightMode == FightMode.Evil && m.Karma < 0 || c.FightMode == FightMode.Evil && Karma < 0) { return true; } var master = GetMaster(); - var cMaster = c.GetMaster(); if (master == null) { diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index dc7e22f80..e373bc6d8 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -1340,12 +1340,7 @@ namespace Server.Spells context.Spell.RemoveEffect(m); } - public static TransformContext GetContext(Mobile m) - { - _table.TryGetValue(m, out var context); - - return context; - } + public static TransformContext GetContext(Mobile m) => _table.GetValueOrDefault(m); public static bool UnderTransformation(Mobile m) => GetContext(m) != null;