From 1e4cdc4ab662b53b9333a73b63ba05d6e64656e6 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:35:47 -0700 Subject: [PATCH] fix: Fixes criminals having guards called on them (#2348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes three bugs in `GuardedRegion.CallGuards`: - **Operator precedence bug**: The condition `!m.Region.IsPartOf(this) && !m_GuardCandidates.ContainsKey(m)` was inverted from intent. Replaced with explicit split: dictionary members are targeted regardless of region; permanent candidates (reds/AlwaysMurderer) must be inside the region. - **Premature `break`**: Only the first guard candidate was ever processed per "guards" call. Removed the `break` so all valid candidates in range get a guard spawned. - **Misleading message for permanent reds**: "Guards can no longer be called on you." (502276) was sent to permanent reds, but guards can *always* be called on them. Now only sent to temporary criminals whose guard window is actually consumed. Also extracts `IsAlwaysGuardCandidate()` helper and simplifies `IsGuardCandidate()`. ## Edge cases verified (by analysis) - **Multiple players call guards on same red**: `BaseGuard.Spawn` dedup (scans 15 tiles for existing guard with same `Focus`) prevents duplicate guards. Message suppression eliminates spam. - **Red fights spawned guard → criminal → guards called again**: Same dedup prevents infinite guard spawns. Existing guard already has `Focus == red`. ## Test plan - [ ] Temporary criminal in guarded region → guard spawns, receives "Guards can no longer be called on you." - [ ] Permanent red (5+ kills) in guarded region → guard spawns, does NOT receive the message - [ ] Multiple players call "guards" on same red → only 1 guard spawns - [ ] Criminal outside region but in dictionary → still targeted by guards call from inside region - [ ] Red outside guarded region → NOT targeted by guards call (must be inside region) --- Projects/UOContent/Regions/GuardedRegion.cs | 39 ++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Projects/UOContent/Regions/GuardedRegion.cs b/Projects/UOContent/Regions/GuardedRegion.cs index 9502701b4..3d9e883f2 100644 --- a/Projects/UOContent/Regions/GuardedRegion.cs +++ b/Projects/UOContent/Regions/GuardedRegion.cs @@ -282,7 +282,11 @@ public class GuardedRegion : BaseRegion BaseGuard.Spawn(fakeCall, m); timer.Stop(); m_GuardCandidates.Remove(m); - m.SendLocalizedMessage(502276); // Guards can no longer be called on you. + + if (!IsAlwaysGuardCandidate(m)) + { + m.SendLocalizedMessage(502276); // Guards can no longer be called on you. + } } } else @@ -298,28 +302,38 @@ public class GuardedRegion : BaseRegion foreach (var m in Map.GetMobilesInRange(p, 14)) { - if (!IsGuardCandidate(m) || !m.Region.IsPartOf(this) && !m_GuardCandidates.ContainsKey(m)) + if (!IsGuardCandidate(m)) { continue; } - if (m_GuardCandidates.Remove(m, out var timer)) + // Dictionary members can be targeted regardless of region (guards reach outside). + // Non-dictionary permanent candidates (reds) must be in the region. + if (m_GuardCandidates.ContainsKey(m) || m.Region.IsPartOf(this) && IsAlwaysGuardCandidate(m)) { - timer.Stop(); - } + if (m_GuardCandidates.Remove(m, out var timer)) + { + timer.Stop(); + } - queue.Enqueue(m); - break; + queue.Enqueue(m); + } } while (queue.Count > 0) { var m = queue.Dequeue(); BaseGuard.Spawn(this, m); - m.SendLocalizedMessage(502276); // Guards can no longer be called on you. + + if (!IsAlwaysGuardCandidate(m)) + { + m.SendLocalizedMessage(502276); // Guards can no longer be called on you. + } } } + private bool IsAlwaysGuardCandidate(Mobile m) => !AllowReds && m.Murderer; + public bool IsGuardCandidate(Mobile m) { if (m is BaseGuard || !m.Alive || m.AccessLevel > AccessLevel.Player || m.Blessed) @@ -327,14 +341,7 @@ public class GuardedRegion : BaseRegion return false; } - var bc = m as BaseCreature; - - if (bc?.IsInvulnerable == true) - { - return false; - } - - return !AllowReds && m.Murderer || m.Criminal; + return (m as BaseCreature)?.IsInvulnerable != true && (IsAlwaysGuardCandidate(m) || m.Criminal); } private class GuardTimer : Timer