refactor: master views over BaseCreature's one master reference (#2674)
## 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
This commit is contained in:
parent
37970a4811
commit
843a29e7dc
41 changed files with 346 additions and 213 deletions
|
|
@ -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.
|
||||
/// <summary>The owner. Setting it sets <see cref="Master"/>.</summary>
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public Mobile ControlMaster
|
||||
{
|
||||
get => _controlled ? _master : null;
|
||||
set => Master = value;
|
||||
}
|
||||
|
||||
/// <summary>The summoner, controlled or not. Setting it sets <see cref="Master"/>.</summary>
|
||||
[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<JailRegion>() ||
|
||||
from == ControlMaster && !from.Region.IsPartOf<JailRegion>() ||
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue