From 843a29e7dc7b402ee9405e7055a384fd597ee1b3 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:45:24 -0700 Subject: [PATCH] refactor: master views over BaseCreature's one master reference (#2674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary After #2670, `BaseCreature` stores one master. Callers still reached it through three differently gated names plus hand-rolled combinations of the `Controlled`/`Summoned` flags. Each gate is now one named view: | Member | Definition | Meaning | |---|---|---| | `Master` | the reference; its setter does the follower bookkeeping; serialized as before | whoever the creature answers to | | `ControlMaster` | `Controlled ? Master : null` | the owner | | `SummonMaster` | `Summoned ? Master : null` | the summoner | | `GetMaster()` | `ControlMaster ?? SummonMaster` | who answers for the creature | `ControlMaster` and `SummonMaster` stay settable so custom code keeps compiling: both setters assign `Master`. There is no guard, so a write through either one can read back as null until the matching flag is set. An enraged creature is the one shape with a master and neither flag. It keeps its meer through `Master`, while `GetMaster()` stays null, so the meer never answers for it (notoriety, kill credit). Readers move onto the view they mean: - "Owner or summoner" pairs and `ControlMaster ?? SummonMaster` collapse into `GetMaster()`: pack instinct, familiars, golem, house access, mounts, pack horse, Solen friendship, spell target and guild checks, and the `[` pet command. - Checks such as `Controlled && ControlMaster == from` drop the flag the view already applies. Checks that don't reduce keep it: `Controlled && ControlMaster != from`, a possibly-null `from`, and `MageAI.CanDispel`'s `Summoned &&`. **No behavior change, save format unchanged** (the schema generator produces no diff). Edge cases for custom code and shard operators: - `SummonMaster = x` on a plain pet now replaces its owner (#2670 made that a no-op), and `ControlMaster = null` on an uncontrolled summon now clears its caster (it did nothing before). - `SpellHelper.GetGuildFor` no longer falls back to an enraged creature's meer. Meer mages have no guild. ## Docs - `dev-docs/content-patterns.md` § Masters: which view answers which question, how to set a master, and which flag checks are redundant. - `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § Masters: RunUO's two-field reads mapped onto the views, plus the gotcha for creatures that set `SummonMaster` without `Summoned` (the write compiles, the read now returns null; use `Master`). - The `modernuo-content-patterns` and `migrate-items-mobiles` skills point at both. ## Test plan - [x] `MasterViewsTests`: all four views across wild, pet, controlled summon, uncontrolled summon and enraged shapes; reassigning `Master` moves follower slots; `SetControlMaster(null)` keeps an uncontrolled summon's caster; deleting a summon returns the caster's slots - [x] Serialization tests updated to the new views (round-trip, legacy v22, v23 migration) - [x] UOContent.Tests (1178 passed, 2 skipped) and Server.Tests (907) green --- .../Mobiles/AI/GuardFriendlyFireTests.cs | 4 +- .../Tests/Mobiles/AI/PetOrderTests.cs | 2 +- .../Tests/Mobiles/AI/PetRetaliationTests.cs | 2 +- .../Mobiles/AI/SummonAcquireRulesTests.cs | 2 +- .../Mobiles/BaseCreatureSerializationTests.cs | 28 +--- .../Tests/Mobiles/MasterViewsTests.cs | 150 ++++++++++++++++++ Projects/UOContent/Commands/Handlers.cs | 2 +- Projects/UOContent/Engines/ConPVP/Arena.cs | 2 +- Projects/UOContent/Engines/ConPVP/SafeZone.cs | 2 +- .../Doom/LeverPuzzle/LeverPuzzleRegions.cs | 2 +- .../UOContent/Gumps/ConfirmReleaseGump.cs | 2 +- .../UOContent/Items/Containers/Container.cs | 2 +- .../Items/Weapons/Abilities/Dismount.cs | 2 +- .../UOContent/Items/Weapons/BaseWeapon.cs | 11 +- Projects/UOContent/Misc/Notoriety.cs | 2 +- .../UOContent/Mobiles/AI/BaseAI/BaseAI.cs | 8 +- .../UOContent/Mobiles/AI/BaseAI/PetOrders.cs | 2 +- Projects/UOContent/Mobiles/AI/FamiliarAI.cs | 2 +- .../Summon Undead/SummonUndeadCounter.cs | 2 +- .../Mobiles/Animals/Misc/PackHorse.cs | 2 +- .../Mobiles/Animals/Mounts/BaseMount.cs | 3 +- .../Mobiles/Animals/Mounts/Beetle.cs | 4 +- Projects/UOContent/Mobiles/BaseCreature.cs | 103 ++++++------ .../Mobiles/Familiars/BaseFamiliar.cs | 2 +- .../UOContent/Mobiles/Familiars/DarkWolf.cs | 2 +- .../UOContent/Mobiles/Familiars/ShadowWisp.cs | 2 +- .../Mobiles/Monsters/Ants/SolenHelper.cs | 34 +--- .../Monsters/LBR/Meers/EnragedCreatures.cs | 12 +- .../Mobiles/Monsters/LBR/Meers/MeerMage.cs | 4 +- .../Monsters/Misc/Melee/BladeSpirits.cs | 2 +- .../Monsters/Misc/Melee/EnergyVortex.cs | 2 +- .../Mobiles/Monsters/Misc/Melee/Golem.cs | 23 ++- .../Mobiles/Monsters/SE/FireBeetle.cs | 4 +- Projects/UOContent/Multis/Houses/BaseHouse.cs | 7 +- Projects/UOContent/Skills/Discordance.cs | 2 +- Projects/UOContent/Spells/Base/SpellHelper.cs | 48 +----- .../UOContent/Spells/Ninjitsu/MirrorImage.cs | 2 +- .../migrate-items-mobiles.md | 1 + .../modernuo-content-patterns.md | 6 + dev-docs/content-patterns.md | 31 ++++ .../09-items-mobiles-creatures.md | 34 ++++ 41 files changed, 346 insertions(+), 213 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs index 8994bd391..ec11a3ba5 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/GuardFriendlyFireTests.cs @@ -123,7 +123,7 @@ public class GuardFriendlyFireTests : IDisposable var (owner, pet) = Spawn(); var summon = OtherPet(owner, new Point3D(1498, 1600, owner.Z)); summon.Summoned = true; - summon.SummonMaster = owner; + summon.Master = owner; summon.Combatant = owner; Assert.Same(owner, summon.Combatant); pet.AggressiveAction(summon, false); @@ -136,7 +136,7 @@ public class GuardFriendlyFireTests : IDisposable public void GuardDefendsAgainstOwnUncontrolledSummonAttackingOwner() { var (owner, pet) = Spawn(); - var vortex = new PetTestStub { Summoned = true, SummonMaster = owner }; + var vortex = new PetTestStub { Summoned = true, Master = owner }; _created.Add(vortex); vortex.MoveToWorld(new Point3D(1498, 1600, owner.Z), owner.Map); vortex.AIObject.AITimer.Stop(); diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs index ca30ee7d9..7d12ed744 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetOrderTests.cs @@ -762,7 +762,7 @@ public class PetOrderTests : IDisposable { var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0)); pet.Summoned = true; - pet.SummonMaster = master; + pet.Master = master; pet.ControlOrder = OrderType.Release; diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs index d5074704c..41b52a189 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/PetRetaliationTests.cs @@ -197,7 +197,7 @@ public class PetRetaliationTests : IDisposable var summon = new StandDownPet(); summon.Summoned = true; - summon.SummonMaster = caster; + summon.Master = caster; summon.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca); summon.AIObject.AITimer?.Stop(); _created.Add(summon); diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/SummonAcquireRulesTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/SummonAcquireRulesTests.cs index 7bd3b7c70..28b311186 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/SummonAcquireRulesTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/SummonAcquireRulesTests.cs @@ -24,7 +24,7 @@ public class SummonAcquireRulesTests : IDisposable { _created.Add(summon); summon.Summoned = true; - summon.SummonMaster = caster; + summon.Master = caster; summon.MoveToWorld(new Point3D(x, 1600, caster.Map.GetAverageZ(x, 1600)), caster.Map); summon.AIObject.AITimer.Stop(); return summon; diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs index 3b95f452e..ba106218c 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs @@ -201,31 +201,16 @@ public class BaseCreatureSerializationTests : IDisposable } else { - bc.SummonMaster = master; + bc.Master = master; } var copy = Load(Snapshot(bc)); Assert.Equal(controlled, copy.Controlled); Assert.Equal(summoned, copy.Summoned); + Assert.Equal(master, copy.Master); Assert.Equal(controlled ? master : null, copy.ControlMaster); - Assert.Equal(controlled && !summoned ? null : master, copy.SummonMaster); - } - - [Fact] - public void SummonMaster_OnPlainPet_KeepsTheOwner() - { - var bc = NewCreature(); - var owner = NewMaster(); - var other = NewMaster(); - - bc.SetControlMaster(owner); - bc.SummonMaster = other; - - Assert.Equal(owner, bc.ControlMaster); - Assert.Null(bc.SummonMaster); - Assert.Equal(bc.ControlSlots, owner.Followers); - Assert.Equal(0, other.Followers); + Assert.Equal(summoned ? master : null, copy.SummonMaster); } [Fact] @@ -446,8 +431,8 @@ public class BaseCreatureSerializationTests : IDisposable Assert.Equal(0.6, copy.CurrentMoveSpeed); // passive mode, inheriting } - // A summon master can exist without Summoned (EnragedCreature), and a controlled summon - // reports the same mobile as both. + // A master survives without Summoned (EnragedCreature), and a controlled summon reports the + // same mobile as both. [Theory] [InlineData(true, true, false, false)] // controlled pet [InlineData(true, true, true, true)] // controlled summon, SummonEnd on the wire @@ -473,7 +458,8 @@ public class BaseCreatureSerializationTests : IDisposable Assert.Equal(controlled, copy.Controlled); Assert.Equal(summoned, copy.Summoned); Assert.Equal(hasControlMaster ? master : null, copy.ControlMaster); - Assert.Equal(hasSummonMaster ? master : null, copy.SummonMaster); + Assert.Equal(master, copy.Master); + Assert.Equal(summoned ? master : null, copy.SummonMaster); if (summoned) { diff --git a/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs new file mode 100644 index 000000000..20578ecfc --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Mobiles/MasterViewsTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Mobiles; +using Xunit; + +namespace UOContent.Tests.Mobiles; + +// One stored master, read through gates on Controlled and Summoned. +[Collection("Sequential UOContent Tests")] +public class MasterViewsTests : IDisposable +{ + private readonly List _created = []; + + public void Dispose() + { + for (var i = 0; i < _created.Count; i++) + { + _created[i].Delete(); + } + } + + private sealed class Stub : BaseCreature + { + public Stub() : base(AIType.AI_Melee) => Body = 0xC9; + + public Stub(Serial serial) : base(serial) + { + } + } + + private Stub NewCreature() + { + var bc = new Stub(); + _created.Add(bc); + return bc; + } + + private PlayerMobile NewPlayer() + { + var pm = new PlayerMobile(World.NewMobile); + pm.DefaultMobileInit(); + World.AddEntity(pm); + _created.Add(pm); + return pm; + } + + public enum Shape + { + Wild, + Pet, + ControlledSummon, + UncontrolledSummon, + Enraged + } + + [Theory] + [InlineData(Shape.Wild)] + [InlineData(Shape.Pet)] + [InlineData(Shape.ControlledSummon)] + [InlineData(Shape.UncontrolledSummon)] + [InlineData(Shape.Enraged)] + public void Views_ByShape(Shape shape) + { + var bc = NewCreature(); + var master = NewPlayer(); + + switch (shape) + { + case Shape.Pet: + { + bc.SetControlMaster(master); + break; + } + case Shape.ControlledSummon: + { + bc.SetControlMaster(master); + bc.Summoned = true; + break; + } + case Shape.UncontrolledSummon: + { + bc.Summoned = true; + bc.Master = master; + break; + } + case Shape.Enraged: + { + bc.Master = master; + break; + } + } + + var controlled = shape is Shape.Pet or Shape.ControlledSummon; + var summoned = shape is Shape.ControlledSummon or Shape.UncontrolledSummon; + + Assert.Equal(shape == Shape.Wild ? null : master, bc.Master); + Assert.Equal(controlled ? master : null, bc.ControlMaster); + Assert.Equal(summoned ? master : null, bc.SummonMaster); + Assert.Equal(controlled || summoned ? master : null, bc.GetMaster()); + } + + [Fact] + public void Master_Reassignment_MovesFollowerSlots() + { + var bc = NewCreature(); + bc.ControlSlots = 2; + var first = NewPlayer(); + var second = NewPlayer(); + + bc.Master = first; + Assert.Equal(2, first.Followers); + + bc.Master = second; + Assert.Equal(0, first.Followers); + Assert.Equal(2, second.Followers); + Assert.Null(bc.ControlMaster); // not Controlled + + bc.Master = null; + Assert.Equal(0, second.Followers); + } + + [Fact] + public void SetControlMasterNull_KeepsAnUncontrolledSummonsCaster() + { + var bc = NewCreature(); + var caster = NewPlayer(); + bc.Summoned = true; + bc.Master = caster; + + bc.SetControlMaster(null); + + Assert.Equal(caster, bc.SummonMaster); + Assert.Equal(bc.ControlSlots, caster.Followers); + } + + [Fact] + public void Delete_ReturnsTheCastersFollowerSlots() + { + var bc = NewCreature(); + var caster = NewPlayer(); + bc.Summoned = true; + bc.Master = caster; + + bc.Delete(); + + Assert.Equal(0, caster.Followers); + Assert.Null(bc.Master); + } +} diff --git a/Projects/UOContent/Commands/Handlers.cs b/Projects/UOContent/Commands/Handlers.cs index d3a9d3350..d1a20707e 100644 --- a/Projects/UOContent/Commands/Handlers.cs +++ b/Projects/UOContent/Commands/Handlers.cs @@ -213,7 +213,7 @@ namespace Server.Commands { if (m is BaseCreature bc) { - if (bc.Controlled && bc.ControlMaster == master || bc.Summoned && bc.SummonMaster == master) + if (bc.GetMaster() == master) { pets.Add(bc); } diff --git a/Projects/UOContent/Engines/ConPVP/Arena.cs b/Projects/UOContent/Engines/ConPVP/Arena.cs index aebfed7b0..d31820339 100644 --- a/Projects/UOContent/Engines/ConPVP/Arena.cs +++ b/Projects/UOContent/Engines/ConPVP/Arena.cs @@ -637,7 +637,7 @@ namespace Server.Engines.ConPVP foreach (var mob in facet.GetMobilesInBounds(m_Bounds)) { - if (mob is BaseCreature pet && pet.Controlled && pet.ControlMaster != null && + if (mob is BaseCreature pet && pet.ControlMaster != null && Players.Contains(pet.ControlMaster)) { pets.Add(pet); diff --git a/Projects/UOContent/Engines/ConPVP/SafeZone.cs b/Projects/UOContent/Engines/ConPVP/SafeZone.cs index 69ba11a66..e800fdd34 100644 --- a/Projects/UOContent/Engines/ConPVP/SafeZone.cs +++ b/Projects/UOContent/Engines/ConPVP/SafeZone.cs @@ -31,7 +31,7 @@ namespace Server.Engines.ConPVP } var pm = m as PlayerMobile ?? - (m is BaseCreature bc && bc.Summoned ? bc.SummonMaster as PlayerMobile : null); + (m as BaseCreature)?.SummonMaster as PlayerMobile; if (pm?.DuelContext?.StartedBeginCountdown == true) { diff --git a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleRegions.cs b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleRegions.cs index 6c983e0e9..55cb60ea1 100644 --- a/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleRegions.cs +++ b/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleRegions.cs @@ -47,7 +47,7 @@ namespace Server.Engines.Doom return; } } - else if (m is BaseCreature bc && (bc.Controlled && bc.ControlMaster == m_Controller.Successful || + else if (m is BaseCreature bc && (bc.ControlMaster == m_Controller.Successful || bc.Summoned)) { return; diff --git a/Projects/UOContent/Gumps/ConfirmReleaseGump.cs b/Projects/UOContent/Gumps/ConfirmReleaseGump.cs index aa3bdc4bc..b3f4efc35 100644 --- a/Projects/UOContent/Gumps/ConfirmReleaseGump.cs +++ b/Projects/UOContent/Gumps/ConfirmReleaseGump.cs @@ -36,7 +36,7 @@ public class ConfirmReleaseGump : StaticGump public override void OnResponse(NetState sender, in RelayInfo info) { if (info.ButtonID != 2 || _pet.Deleted || - !(_pet.Controlled && _from == _pet.ControlMaster && + !(_from == _pet.ControlMaster && _from.CheckAlive() && _pet.Map == _from.Map && _pet.InRange(_from, 14))) { return; diff --git a/Projects/UOContent/Items/Containers/Container.cs b/Projects/UOContent/Items/Containers/Container.cs index 164c285c4..77620db3c 100644 --- a/Projects/UOContent/Items/Containers/Container.cs +++ b/Projects/UOContent/Items/Containers/Container.cs @@ -237,7 +237,7 @@ public partial class StrongBackpack : Backpack // Used on Pack animals base.CheckHold(m, item, false, checkItems, plusItems, plusWeight); public override bool CheckContentDisplay(Mobile from) => - RootParent is BaseCreature creature && creature.Controlled && creature.ControlMaster == from || + RootParent is BaseCreature creature && creature.ControlMaster == from || base.CheckContentDisplay(from); } diff --git a/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs b/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs index df50f1449..3121b9516 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/Dismount.cs @@ -102,7 +102,7 @@ public class Dismount : WeaponAbility { playerMobile.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, true); } - else if (Core.ML && attacker is BaseCreature { Controlled: true, ControlMaster: PlayerMobile pm }) + else if (Core.ML && attacker is BaseCreature { ControlMaster: PlayerMobile pm }) { pm.SetMountBlock(BlockMountType.DismountRecovery, RemountDelay, false); } diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index df5361a13..e4f0d4def 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -1698,12 +1698,12 @@ public abstract partial class BaseWeapon return 0; } - if (attacker is not BaseCreature bc || bc.PackInstinct == PackInstinct.None || !bc.Controlled && !bc.Summoned) + if (attacker is not BaseCreature bc || bc.PackInstinct == PackInstinct.None) { return 0; } - var master = bc.ControlMaster ?? bc.SummonMaster; + var master = bc.GetMaster(); if (master == null) { @@ -1713,8 +1713,7 @@ public abstract partial class BaseWeapon var inPack = 1; foreach (var m in defender.GetMobilesInRange(1)) { - if (m != attacker && (m.PackInstinct & bc.PackInstinct) != 0 && (m.Controlled || m.Summoned) && - master == (m.ControlMaster ?? m.SummonMaster) && m.Combatant == defender) + if (m != attacker && (m.PackInstinct & bc.PackInstinct) != 0 && master == m.GetMaster() && m.Combatant == defender) { inPack++; } @@ -1736,7 +1735,7 @@ public abstract partial class BaseWeapon { foreach (var m in defender.GetMobilesInRange(4)) { - if (m?.Summoned == true && m.SummonMaster == defender) + if (m?.SummonMaster == defender) { // Your attack has been diverted to a nearby mirror image of your target! attacker.SendLocalizedMessage(1063141); @@ -2100,7 +2099,7 @@ public abstract partial class BaseWeapon if (attacker is VampireBatFamiliar bc) { - var caster = bc.ControlMaster ?? bc.SummonMaster; + var caster = bc.GetMaster(); if (caster != null && caster.Map == bc.Map) { diff --git a/Projects/UOContent/Misc/Notoriety.cs b/Projects/UOContent/Misc/Notoriety.cs index fc57c3462..f31ad3450 100644 --- a/Projects/UOContent/Misc/Notoriety.cs +++ b/Projects/UOContent/Misc/Notoriety.cs @@ -228,7 +228,7 @@ namespace Server.Misc } if (bcTarg?.Controlled == true - || bcTarg?.Summoned == true && bcTarg.SummonMaster != from && bcTarg.SummonMaster?.Player == true) + || bcTarg?.SummonMaster is { Player: true } summoner && summoner != from) { return false; // Cannot harm other controlled mobiles from players } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs index 64eecb50e..0c5b9423b 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs @@ -144,7 +144,7 @@ public abstract partial class BaseAI } // Only a creature somebody can command has been told anything; wild creatures rest on None. - var toldToStandDown = Mobile.Controlled && Mobile.ControlMaster != null && Mobile.Commandable && + var toldToStandDown = Mobile.ControlMaster != null && Mobile.Commandable && IsStandDownOrder(Mobile.ControlOrder); if (Mobile.StandsDownOnCommand && toldToStandDown) @@ -1049,15 +1049,15 @@ public abstract partial class BaseAI private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc) { // A summon whose caster was deleted before a reload comes back with no master. - if (Core.AOS && bc?.Summoned == true && - (bc.SummonMaster == Mobile || bc.SummonMaster is { Player: false } summonMaster && IsHostile(summonMaster))) + if (Core.AOS && + (bc?.SummonMaster == Mobile || bc?.SummonMaster is { Player: false } summonMaster && IsHostile(summonMaster))) { return true; } var master = Mobile.SummonMaster; - if (!Mobile.Summoned || master == null) + if (master == null) { return false; } diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs index dc605b976..e3224ea93 100644 --- a/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs +++ b/Projects/UOContent/Mobiles/AI/BaseAI/PetOrders.cs @@ -775,7 +775,7 @@ public abstract partial class BaseAI var master = Mobile.ControlMaster; return target == Mobile || master != null && - (target == master || target is BaseCreature { Controlled: true } creature && creature.ControlMaster == master); + (target == master || target is BaseCreature creature && creature.ControlMaster == master); } /// diff --git a/Projects/UOContent/Mobiles/AI/FamiliarAI.cs b/Projects/UOContent/Mobiles/AI/FamiliarAI.cs index 2da24dc8d..2b5b22c29 100644 --- a/Projects/UOContent/Mobiles/AI/FamiliarAI.cs +++ b/Projects/UOContent/Mobiles/AI/FamiliarAI.cs @@ -152,7 +152,7 @@ public class FamiliarAI : BaseAI var combatant = target.Combatant; return combatant == Mobile || combatant == master || - combatant is BaseCreature { Controlled: true } pet && pet.ControlMaster == master; + combatant is BaseCreature pet && pet.ControlMaster == master; } // Closest mobile in a fight with the caster's side. diff --git a/Projects/UOContent/Mobiles/Abilities/Summon Undead/SummonUndeadCounter.cs b/Projects/UOContent/Mobiles/Abilities/Summon Undead/SummonUndeadCounter.cs index 3666d21ef..bfbf7be55 100644 --- a/Projects/UOContent/Mobiles/Abilities/Summon Undead/SummonUndeadCounter.cs +++ b/Projects/UOContent/Mobiles/Abilities/Summon Undead/SummonUndeadCounter.cs @@ -48,7 +48,7 @@ public abstract class SummonUndeadCounter : MonsterAbility summon = CreateSummon(source); summon.Team = source.Team; summon.Summoned = true; - summon.SummonMaster = source; + summon.Master = source; summon.FightMode = FightMode.Closest; summon.Combatant = target; summon.Home = source.Home; diff --git a/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs b/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs index ca2d4e3dd..822027184 100644 --- a/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs +++ b/Projects/UOContent/Mobiles/Animals/Misc/PackHorse.cs @@ -137,7 +137,7 @@ namespace Server.Mobiles public static bool CheckAccess(BaseCreature animal, Mobile from) => from == animal || from.AccessLevel >= AccessLevel.GameMaster || from.Alive && animal.Controlled && !animal.IsDeadPet && - (from == animal.ControlMaster || from == animal.SummonMaster || animal.IsPetFriend(from)); + (from == animal.GetMaster() || animal.IsPetFriend(from)); public static void CombineBackpacks(BaseCreature animal) { diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs b/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs index 43e7a598c..f4b516447 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/BaseMount.cs @@ -239,8 +239,7 @@ public abstract partial class BaseMount : BaseCreature, IMount if (from.InRange(this, 1)) { var canAccess = from.AccessLevel >= AccessLevel.GameMaster - || Controlled && ControlMaster == from - || Summoned && SummonMaster == from; + || GetMaster() == from; if (canAccess) { diff --git a/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs b/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs index bba194d3d..ffef6350b 100644 --- a/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs +++ b/Projects/UOContent/Mobiles/Animals/Mounts/Beetle.cs @@ -71,7 +71,7 @@ namespace Server.Mobiles public override void OnHarmfulSpell(Mobile from) { - if (!Controlled && ControlMaster == null) + if (!Controlled) { CurrentSpeed = BoostedSpeed; } @@ -79,7 +79,7 @@ namespace Server.Mobiles public override void OnCombatantChange() { - if (Combatant == null && !Controlled && ControlMaster == null) + if (Combatant == null && !Controlled) { CurrentSpeed = PassiveSpeed; } diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index d37465796..e1b5dbdcf 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -445,9 +445,7 @@ namespace Server.Mobiles InvalidateProperties(); } - // ControlMaster and SummonMaster are views of this one reference, gated by Controlled and Summoned. - [SerializableField(15, getter: "private", setter: "private")] - [SaveFlag(nameof(ShouldSerializeMaster))] + // Follower bookkeeping brackets the assignment, so Master is hand-written. private Mobile _master; private bool ShouldSerializeMaster() => _master != null; @@ -1152,51 +1150,49 @@ namespace Server.Mobiles } } - // Assigned before Controlled is raised (SetControlMaster), so the setter cannot be gated. + // Whoever this creature answers to. ControlMaster, SummonMaster and GetMaster() gate it by + // Controlled and Summoned; an enraged creature has a master with neither flag. + [SerializableProperty(15, useField: nameof(_master))] + [SaveFlag(nameof(ShouldSerializeMaster))] [CommandProperty(AccessLevel.GameMaster)] - public Mobile ControlMaster + public Mobile Master { - get => _controlled ? _master : null; + get => _master; set { - if (ControlMaster == value || this == value) + if (_master == value || this == value) { return; } - SetMaster(value); + RemoveFollowers(); + _master = value; + AddFollowers(); if (value != null) { StopDeleteTimer(); } + + Delta(MobileDelta.Noto); + this.MarkDirty(); } } - // The summoner, controlled or not. Enraged creatures have one without being Summoned. - // A plain pet has no summoner, so assigning one would overwrite its owner. + /// The owner. Setting it sets . + [CommandProperty(AccessLevel.GameMaster)] + public Mobile ControlMaster + { + get => _controlled ? _master : null; + set => Master = value; + } + + /// The summoner, controlled or not. Setting it sets . [CommandProperty(AccessLevel.GameMaster)] public Mobile SummonMaster { - get => _controlled && !_summoned ? null : _master; - set - { - if (SummonMaster == value || this == value || _controlled && !_summoned) - { - return; - } - - SetMaster(value); - } - } - - private void SetMaster(Mobile value) - { - RemoveFollowers(); - Master = value; - AddFollowers(); - - Delta(MobileDelta.Noto); + get => _summoned ? _master : null; + set => Master = value; } // Fires on every assignment, not only changes: a reissued order is a command (retarget, re-anchor). @@ -2726,7 +2722,7 @@ namespace Server.Mobiles AnimateDeadSpell.Unregister(SummonMaster, this); } - if (Summoned && SummonMaster != null) + if (SummonMaster != null) { SummonFamiliarSpell.Unregister(SummonMaster, this); } @@ -2796,7 +2792,7 @@ namespace Server.Mobiles { base.AggressiveAction(aggressor, criminal); - if (Controlled && ControlMaster != null && NotorietyHandlers.CheckAggressor(ControlMaster.Aggressors, aggressor)) + if (ControlMaster != null && NotorietyHandlers.CheckAggressor(ControlMaster.Aggressors, aggressor)) { aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true)); } @@ -2904,7 +2900,7 @@ namespace Server.Mobiles } public override bool IsHarmfulCriminal(Mobile target) => - target != ControlMaster && (!Summoned || target != SummonMaster) && + target != GetMaster() && (target is not BaseCreature { InitialInnocent: true } creature || creature.Controlled) && (target is not PlayerMobile mobile || mobile.PermaFlags.Count <= 0) && base.IsHarmfulCriminal(target); @@ -2912,9 +2908,9 @@ namespace Server.Mobiles { base.CriminalAction(message); - if ((Controlled || Summoned) && _master?.Player == true) + if (GetMaster() is { Player: true } master) { - _master.CriminalAction(false); + master.CriminalAction(false); } } @@ -2922,7 +2918,7 @@ namespace Server.Mobiles { base.DoHarmful(target, indirect); - if (target == this || target == _master || !Controlled && !Summoned) + if (target == this || GetMaster() is not { } owner || target == owner) { return; } @@ -3343,20 +3339,8 @@ namespace Server.Mobiles return bonus; } - public Mobile GetMaster() - { - if (Controlled && ControlMaster != null) - { - return ControlMaster; - } - - if (Summoned && SummonMaster != null) - { - return SummonMaster; - } - - return null; - } + // Who answers for this creature: its owner or its summoner. Never an enraged creature's meer. + public Mobile GetMaster() => ControlMaster ?? SummonMaster; public virtual bool IsMonster => !Controlled || (GetMaster() as BaseCreature)?.IsMonster == true; @@ -3702,7 +3686,7 @@ namespace Server.Mobiles var m = ControlMaster; SetControlMaster(null); - SummonMaster = null; + Master = null; ReceivedHonorContext?.Cancel(); base.OnDelete(); @@ -3757,14 +3741,19 @@ namespace Server.Mobiles } public override bool CanBeRenamedBy(Mobile from) => - Controlled && from == ControlMaster && !from.Region.IsPartOf() || + from == ControlMaster && !from.Region.IsPartOf() || base.CanBeRenamedBy(from); public bool SetControlMaster(Mobile m) { if (m == null) { - ControlMaster = null; + // An uncontrolled summon keeps its caster. + if (_controlled) + { + Master = null; + } + Controlled = false; ControlTarget = null; ControlOrder = OrderType.None; @@ -3787,7 +3776,7 @@ namespace Server.Mobiles Home = Point3D.Zero; - ControlMaster = m; + Master = m; Controlled = true; ControlTarget = null; ControlOrder = OrderType.Come; @@ -3849,7 +3838,7 @@ namespace Server.Mobiles creature.RangeHome = 10; creature.Summoned = true; - creature.SummonMaster = caster; + creature.Master = caster; var pack = creature.Backpack; @@ -4006,9 +3995,11 @@ namespace Server.Mobiles return BardMaster; } - if ((_controlled || _summoned) && _master != null) + var master = GetMaster(); + + if (master != null) { - return _master; + return master; } return base.GetDamageMaster(damagee); diff --git a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs index 6da24edfe..917e928d3 100644 --- a/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs +++ b/Projects/UOContent/Mobiles/Familiars/BaseFamiliar.cs @@ -94,7 +94,7 @@ public abstract partial class BaseFamiliar : BaseCreature { base.GetContextMenuEntries(from, ref list); - if (from.Alive && Controlled && from == ControlMaster && from.InRange(this, 14)) + if (from.Alive && from == ControlMaster && from.InRange(this, 14)) { list.Add(new ReleaseEntry()); } diff --git a/Projects/UOContent/Mobiles/Familiars/DarkWolf.cs b/Projects/UOContent/Mobiles/Familiars/DarkWolf.cs index 32e321011..a39c54d44 100644 --- a/Projects/UOContent/Mobiles/Familiars/DarkWolf.cs +++ b/Projects/UOContent/Mobiles/Familiars/DarkWolf.cs @@ -52,7 +52,7 @@ public partial class DarkWolfFamiliar : BaseFamiliar m_NextRestore = Core.Now + TimeSpan.FromSeconds(2.0); - var caster = ControlMaster ?? SummonMaster; + var caster = GetMaster(); if (caster != null) { diff --git a/Projects/UOContent/Mobiles/Familiars/ShadowWisp.cs b/Projects/UOContent/Mobiles/Familiars/ShadowWisp.cs index 004d9cb9f..1af1dc204 100644 --- a/Projects/UOContent/Mobiles/Familiars/ShadowWisp.cs +++ b/Projects/UOContent/Mobiles/Familiars/ShadowWisp.cs @@ -62,7 +62,7 @@ public partial class ShadowWispFamiliar : BaseFamiliar private void Flare() { - var caster = ControlMaster ?? SummonMaster; + var caster = GetMaster(); if (caster == null) { diff --git a/Projects/UOContent/Mobiles/Monsters/Ants/SolenHelper.cs b/Projects/UOContent/Mobiles/Monsters/Ants/SolenHelper.cs index ad97905dd..5fb37aec6 100644 --- a/Projects/UOContent/Mobiles/Monsters/Ants/SolenHelper.cs +++ b/Projects/UOContent/Mobiles/Monsters/Ants/SolenHelper.cs @@ -21,14 +21,9 @@ namespace Server.Mobiles { if (m is BaseCreature bc) { - if (bc.Controlled && bc.ControlMaster is PlayerMobile) + if (bc.GetMaster() is PlayerMobile master) { - return CheckRedFriendship(bc.ControlMaster); - } - - if (bc.Summoned && bc.SummonMaster is PlayerMobile) - { - return CheckRedFriendship(bc.SummonMaster); + return CheckRedFriendship(master); } } @@ -39,14 +34,9 @@ namespace Server.Mobiles { if (m is BaseCreature bc) { - if (bc.Controlled && bc.ControlMaster is PlayerMobile) + if (bc.GetMaster() is PlayerMobile master) { - return CheckBlackFriendship(bc.ControlMaster); - } - - if (bc.Summoned && bc.SummonMaster is PlayerMobile) - { - return CheckBlackFriendship(bc.SummonMaster); + return CheckBlackFriendship(master); } } @@ -57,13 +47,9 @@ namespace Server.Mobiles { if (from is BaseCreature bc) { - if (bc.Controlled && bc.ControlMaster is PlayerMobile) + if (bc.GetMaster() is PlayerMobile master) { - OnRedDamage(bc.ControlMaster); - } - else if (bc.Summoned && bc.SummonMaster is PlayerMobile) - { - OnRedDamage(bc.SummonMaster); + OnRedDamage(master); } } @@ -83,13 +69,9 @@ namespace Server.Mobiles { if (from is BaseCreature bc) { - if (bc.Controlled && bc.ControlMaster is PlayerMobile) + if (bc.GetMaster() is PlayerMobile master) { - OnBlackDamage(bc.ControlMaster); - } - else if (bc.Summoned && bc.SummonMaster is PlayerMobile) - { - OnBlackDamage(bc.SummonMaster); + OnBlackDamage(master); } } diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs index a7afa1b9e..cc6eb8cd0 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/EnragedCreatures.cs @@ -92,15 +92,15 @@ namespace Server.Mobiles Karma = -1000; Tamable = false; - SummonMaster = summoner; + Master = summoner; } // The meer's FightMode.Evil against our negative karma would otherwise make us enemies. - public override bool IsEnemy(Mobile m) => m != SummonMaster && base.IsEnemy(m); + public override bool IsEnemy(Mobile m) => m != Master && base.IsEnemy(m); public override void OnThink() { - if (SummonMaster?.Deleted != false) + if (Master?.Deleted != false) { Delete(); } @@ -111,7 +111,7 @@ namespace Server.Mobiles */ else if (!Combat(this)) { - AIObject?.MoveTo(SummonMaster, 5); + AIObject?.MoveTo(Master, 5); } /* On OSI, if the summon attacks a mobile, the summoner meer also @@ -119,11 +119,11 @@ namespace Server.Mobiles is a player or controlled/summoned, and the summoner is not already engaged in combat. */ - else if (!Combat(SummonMaster)) + else if (!Combat(Master)) { if (Combatant.Player || Combatant is BaseCreature bc && bc.GetMaster() != null) { - SummonMaster.Combatant = Combatant; + Master.Combatant = Combatant; } } else diff --git a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs index e96e8cd1d..fe0a2e2d5 100644 --- a/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs +++ b/Projects/UOContent/Mobiles/Monsters/LBR/Meers/MeerMage.cs @@ -60,7 +60,7 @@ namespace Server.Mobiles public override bool InitialInnocent => true; // FightMode.Evil would otherwise target our own enraged creatures for their negative karma. - public override bool IsEnemy(Mobile m) => (m as BaseEnraged)?.SummonMaster != this && base.IsEnemy(m); + public override bool IsEnemy(Mobile m) => (m as BaseEnraged)?.Master != this && base.IsEnemy(m); public override void GenerateLoot() { @@ -88,7 +88,7 @@ namespace Server.Mobiles if (combatant is BaseCreature bc) { - if (bc.Controlled && bc.ControlMaster?.Deleted == false && bc.ControlMaster.Alive) + if (bc.ControlMaster is { Deleted: false, Alive: true }) { if (bc.ControlMaster.Map == Map && bc.ControlMaster.InRange(this, 12) && !UnderEffect(bc.ControlMaster)) diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs index 455418563..4d6dcf251 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/BladeSpirits.cs @@ -55,7 +55,7 @@ namespace Server.Mobiles public override bool BleedImmune => true; public override Poison PoisonImmune => Poison.Lethal; - public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true || + public override bool FollowsAcquireRules => Core.AOS || SummonMaster?.Player != true || (Map?.Rules & MapRules.HarmfulRestrictions) != 0; public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) => diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs index efa78c6ba..c68ecee8a 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/EnergyVortex.cs @@ -63,7 +63,7 @@ namespace Server.Mobiles public override bool BleedImmune => true; public override Poison PoisonImmune => Poison.Lethal; - public override bool FollowsAcquireRules => Core.AOS || !Summoned || SummonMaster?.Player != true || + public override bool FollowsAcquireRules => Core.AOS || SummonMaster?.Player != true || (Map?.Rules & MapRules.HarmfulRestrictions) != 0; public override double GetFightModeRanking(Mobile m, FightMode acqType, bool bPlayerOnly) => diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/Golem.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/Golem.cs index ff40211e8..21927419b 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/Golem.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/Golem.cs @@ -142,22 +142,17 @@ namespace Server.Mobiles public override void OnDamage(int amount, Mobile from, bool willKill) { - if (Controlled || Summoned) + if (GetMaster() is { Player: true } master && master.Map == Map && master.InRange(Location, 20)) { - var master = ControlMaster ?? SummonMaster; - - if (master?.Player == true && master.Map == Map && master.InRange(Location, 20)) + if (master.Mana >= amount) { - if (master.Mana >= amount) - { - master.Mana -= amount; - } - else - { - amount -= master.Mana; - master.Mana = 0; - master.Damage(amount); - } + master.Mana -= amount; + } + else + { + amount -= master.Mana; + master.Mana = 0; + master.Damage(amount); } } diff --git a/Projects/UOContent/Mobiles/Monsters/SE/FireBeetle.cs b/Projects/UOContent/Mobiles/Monsters/SE/FireBeetle.cs index f45f7a7d7..53c13fefe 100644 --- a/Projects/UOContent/Mobiles/Monsters/SE/FireBeetle.cs +++ b/Projects/UOContent/Mobiles/Monsters/SE/FireBeetle.cs @@ -59,7 +59,7 @@ namespace Server.Mobiles public override void OnHarmfulSpell(Mobile from) { - if (!Controlled && ControlMaster == null) + if (!Controlled) { CurrentSpeed = BoostedSpeed; } @@ -67,7 +67,7 @@ namespace Server.Mobiles public override void OnCombatantChange() { - if (Combatant == null && !Controlled && ControlMaster == null) + if (Combatant == null && !Controlled) { CurrentSpeed = PassiveSpeed; } diff --git a/Projects/UOContent/Multis/Houses/BaseHouse.cs b/Projects/UOContent/Multis/Houses/BaseHouse.cs index 212a60386..587f9907e 100644 --- a/Projects/UOContent/Multis/Houses/BaseHouse.cs +++ b/Projects/UOContent/Multis/Houses/BaseHouse.cs @@ -3660,12 +3660,7 @@ namespace Server.Multis return true; } - if (!(bc.Controlled || bc.Summoned)) - { - return false; - } - - m = bc.ControlMaster ?? bc.SummonMaster; + m = bc.GetMaster(); return m != null && (m.AccessLevel > AccessLevel.Player || IsFriend(m) || Access?.Contains(m) == true); } diff --git a/Projects/UOContent/Skills/Discordance.cs b/Projects/UOContent/Skills/Discordance.cs index 14c5a7ee7..15b7ed118 100644 --- a/Projects/UOContent/Skills/Discordance.cs +++ b/Projects/UOContent/Skills/Discordance.cs @@ -141,7 +141,7 @@ namespace Server.SkillHandlers { if (targ == from || targ is BaseCreature bc && (bc.BardImmune || !from.CanBeHarmful(bc, false)) && - !(bc.Controlled && bc.ControlMaster == from)) + bc.ControlMaster != from) { from.SendLocalizedMessage(1049535); // A song of discord would have no effect on that. } diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index e373bc6d8..537c56f1d 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -407,32 +407,8 @@ namespace Server.Spells }; } - public static Guild GetGuildFor(Mobile m) - { - var g = m.Guild as Guild; - - if (g == null && m is BaseCreature c) - { - m = c.ControlMaster; - - if (m != null) - { - g = m.Guild as Guild; - } - - if (g == null) - { - m = c.SummonMaster; - - if (m != null) - { - g = m.Guild as Guild; - } - } - } - - return g; - } + public static Guild GetGuildFor(Mobile m) => + m.Guild as Guild ?? (m as BaseCreature)?.GetMaster()?.Guild as Guild; public static bool ValidIndirectTarget(Mobile from, Mobile to) { @@ -473,29 +449,17 @@ namespace Server.Spells return false; } - if (bcTarg != null && (bcTarg.Controlled || bcTarg.Summoned)) + if (bcTarg?.GetMaster() is { } targMaster) { - if (bcTarg.ControlMaster == from || bcTarg.SummonMaster == from) - { - return false; - } - - if (p != null && (p.Contains(bcTarg.ControlMaster) || p.Contains(bcTarg.SummonMaster))) + if (targMaster == from || p?.Contains(targMaster) == true) { return false; } } - if (bcFrom != null && (bcFrom.Controlled || bcFrom.Summoned)) + if (bcFrom?.GetMaster() is { } fromMaster) { - if (bcFrom.ControlMaster == to || bcFrom.SummonMaster == to) - { - return false; - } - - p = Party.Get(to); - - if (p != null && (p.Contains(bcFrom.ControlMaster) || p.Contains(bcFrom.SummonMaster))) + if (fromMaster == to || Party.Get(to)?.Contains(fromMaster) == true) { return false; } diff --git a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs index 63788aa69..5ca42f997 100644 --- a/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs +++ b/Projects/UOContent/Spells/Ninjitsu/MirrorImage.cs @@ -158,7 +158,7 @@ namespace Server.Mobiles Warmode = true; Summoned = true; - SummonMaster = caster; + Master = caster; ControlOrder = OrderType.Follow; ControlTarget = caster; diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md index 9a7c08ab4..bd994b983 100644 --- a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md @@ -33,6 +33,7 @@ description: > - `AcquireOnApproach` (bool) -> `AcquireOnApproachDelay` (TimeSpan; `Zero` = old instant behavior) -> same doc § Target Acquisition - `BaseAI.OnAggressiveAction(aggressor)` is `bool`, not `void`: the AI owns the retaliation policy (`BaseCreature` no longer sets `Combatant` after it); an early `return;` that refused a fight -> `return false;`, otherwise `return true;`; swap-only overrides call `PreferCloserAggressor(aggressor)` instead of `base` -> same doc § Retaliation - `DamageEntries` is an inline `ref readonly ValueLinkList`, not a `List`: indexer/`Add`/`Remove`/`Clear` -> `foreach` / `.ByDescending()` (needs `using Server.Collections;`) and `ClearDamageEntries()`; `GetLootingRights` takes it by `in` -> same doc § Damage Entries +- `ControlMaster`/`SummonMaster` are views of one `Master`, gated by `Controlled`/`Summoned`; assignments still compile (they set `Master`). `ControlMaster ?? SummonMaster` and controlled-or-summoned pairs -> `GetMaster()`; a creature that sets `SummonMaster` without `Summoned` (RunUO's `BaseEnraged`) must read `Master`, because `SummonMaster` now reads null -> same doc § Masters ## Anti-Patterns - Using `_field--` instead of `Property--` (bypasses MarkDirty tracking) diff --git a/dev-docs/claude-skills/modernuo-content-patterns.md b/dev-docs/claude-skills/modernuo-content-patterns.md index 55aa32605..ae83888af 100644 --- a/dev-docs/claude-skills/modernuo-content-patterns.md +++ b/dev-docs/claude-skills/modernuo-content-patterns.md @@ -40,6 +40,12 @@ description: > trigger cooldown is the rate limit, `ChanceToTrigger` is per-sample jitter, and a zero-cooldown `Think`/`CombatAction` ability triggers every sampled think -- see `dev-docs/content-patterns.md` § OnThink: the excess-call contract +9. **One master, read through views** -- `ControlMaster` (owner, only while `Controlled`), + `SummonMaster` (summoner, only while `Summoned`), `GetMaster()` (either: who answers for the + creature), `Master` (the stored reference, flags ignored; both setters assign it). Tame with + `SetControlMaster`, summon with `BaseCreature.Summon`. Don't repeat the view's flag: + `Controlled && ControlMaster == x` -> `ControlMaster == x`, but `!=` does not reduce -- see + `dev-docs/content-patterns.md` § Masters ## New Item Template diff --git a/dev-docs/content-patterns.md b/dev-docs/content-patterns.md index e583364c6..b138ce9f0 100644 --- a/dev-docs/content-patterns.md +++ b/dev-docs/content-patterns.md @@ -267,6 +267,37 @@ public override int TreasureMapLevel => 3; // Drops treasure map public override double WeaponAbilityChance => 0.4; // Weapon ability chance ``` +### Masters: owner, summoner, responsible party + +A creature stores one master. Read it through the view that matches the question: + +| Read | Returns | Use it for | +|---|---|---| +| `ControlMaster` | the master while `Controlled`, else null | the owner: commands, bonding, friends, stabling | +| `SummonMaster` | the master while `Summoned`, else null | the summoner: dispel, summon acquire rules | +| `GetMaster()` | `ControlMaster ?? SummonMaster` | whoever answers for the creature: notoriety, kill credit, guild and party checks | +| `Master` | the stored reference, flags ignored | a creature's own AI following or defending a master it has no flag for | + +A master with neither flag is legal. A meer mage's enraged creatures carry their meer in `Master` +without being `Controlled` or `Summoned`, so `GetMaster()` is null and the meer never answers for them. + +To set a master, tame through `SetControlMaster(m)` (it raises `Controlled`, resets the order and +checks follower slots) and summon through `BaseCreature.Summon(...)`. Assign `Master` directly only +for a master with no flag. The `ControlMaster` and `SummonMaster` setters are aliases for `Master`: +they don't set `Controlled` or `Summoned`, so the value reads back as null until the flag is set. +Every assignment moves the creature's follower slots from the old master to the new one. + +The view already checks its flag, so don't repeat it: + +```csharp +// Redundant: ControlMaster is null unless Controlled +if (Controlled && ControlMaster == from) { ... } +if (ControlMaster == from) { ... } + +// Not equivalent: without the flag this is true for every wild creature +if (Controlled && ControlMaster != from) { ... } +``` + ### Creature Speeds (think vs move clocks) All "speed" values are **delays in seconds** (smaller = faster). A creature runs two clocks: diff --git a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md index 389c845e5..7bde0a898 100644 --- a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md +++ b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md @@ -540,6 +540,40 @@ An override that used to `return;` early to refuse a fight now returns `false`; `base.OnAggressiveAction` runs the full stock policy, including setting `Combatant`, so an AI that only wants the closer-aggressor swap calls `PreferCloserAggressor` instead. +## Masters: One Reference, Not Two + +RunUO kept `ControlMaster` and `SummonMaster` as two independent fields. ModernUO stores one `Master`. +`ControlMaster` and `SummonMaster` are views of it gated by `Controlled` and `Summoned`, and +`GetMaster()` is `ControlMaster ?? SummonMaster` (see `dev-docs/content-patterns.md` § Masters). + +`ControlMaster != null` implies `Controlled`, so a bare `ControlMaster` check needs no `Controlled` in front of it. + +Assignments compile unchanged, because both setters assign `Master`. Reads need a look: + +| RunUO | ModernUO | +|---|---| +| `bc.ControlMaster ?? bc.SummonMaster` | `bc.GetMaster()` | +| `bc.Controlled && bc.ControlMaster == m \|\| bc.Summoned && bc.SummonMaster == m` | `bc.GetMaster() == m` | +| `Controlled && ControlMaster == m` (`m` non-null) | `ControlMaster == m` | +| `SummonMaster` on a creature that sets it without `Summoned = true` (RunUO's `BaseEnraged`) | `Master` | + +Gotchas: + +- **A summon master without `Summoned` reads back null.** A RunUO creature that assigned + `SummonMaster = caster;` without `Summoned = true` and later read `SummonMaster` now reads null. + The write still compiles; only the read changes. Use `Master` for both, and override `IsEnemy` if the + creature must never target its master (see `BaseEnraged`). +- **One master per creature.** `SummonMaster = x` on a tamed pet replaces its owner, and + `ControlMaster = null` on an uncontrolled summon clears its caster. Code that tracked a different + owner and summoner keeps only the last assignment. +- **`ControlMaster = x;` is not a tame.** The setter only moves follower slots. It skips everything + `SetControlMaster(x)` does: raising `Controlled`, checking the follower cap, resetting the order + and `ControlTarget`, unlinking from the spawner, clearing the waypoint, home and guild. On its own it + leaves a creature that uses up `x`'s follower slots, can't be commanded and still reads as wild. + Use `SetControlMaster(x)`. +- **`SetControlMaster(null)` keeps an uncontrolled summon's caster.** It clears the master only + while the creature is `Controlled`. + ## Target Acquisition: `AcquireOnApproach` Is a Delay RunUO's `AcquireOnApproach` bool (paragon insta-aggro on approach) is now