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:
Kamron Batman 2026-09-24 22:45:24 -07:00 • committed by GitHub
parent 37970a4811
commit 843a29e7dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 346 additions and 213 deletions

View file

@ -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: