refactor: one master reference behind ControlMaster and SummonMaster (#2670)

## Summary

`BaseCreature` stored the pet owner (`ControlMaster`) and the summoner (`SummonMaster`) in two fields. Every flow that manages pets kept them in step by hand: summon, transfer, stable, claim, ball of summoning and GM obey. They now share one `_master` field, and the two properties read it through the Controlled and Summoned flags:

```csharp
ControlMaster => Controlled ? master : null
SummonMaster  => Controlled && !Summoned ? null : master
```

Every existing reader keeps its meaning:
- `ControlMaster` is still "the owner".
- `SummonMaster` is still "the summoner, controlled or not".

Blade spirits, energy vortexes, clones, undead minions and enraged creatures (a summon master without `Summoned`) never become visible through `ControlMaster`, so the ungated readers (ML notoriety, aggressor entries and similar) behave as before.

- Setting `SummonMaster` on a plain pet does nothing, instead of quietly replacing the owner.
- `SetControlMaster` now keeps both masters in step, so the hand-written `SummonMaster` copies are removed from ball of summoning, GM obey, pet transfer, auto-stable and claim, the animal trainer's stable and claim, and jail stabling.

## Serialization

BaseCreature moves to v24 with one serialized master (slot 15); the old `SummonMaster` slot is dropped.
- `MigrateFrom(V23Content)` resolves absent fields through the same default-value methods the live reader uses.
- A save that carried both masters (a summon traded before ownership followed transfers) resolves to the owner. Legacy pre-codegen streams follow the same rule.

Requires `ModernUO.Serialization.Generator` 4.2.0 (#2671). `UOContent.Tests` was still on 4.1.0 and is bumped to match.

## Test plan

- [x] `MasterShapes_RoundTrip`: controlled pet, controlled summon, energy vortex, enraged creature
- [x] `SummonMaster_OnPlainPet_KeepsTheOwner`: owner and follower counts unchanged
- [x] `LegacyV22Stream_DifferingMasters_OwnerWins`
- [x] `V23Stream_MigratesToOneMaster_AndDefaultsAbsentFields`: both master slots -> owner; flags past bit 32; absent fields take class defaults
- [x] UOContent.Tests (1168 passed, 2 skipped) and Server.Tests (907) green
This commit is contained in:
Kamron Batman 2026-09-24 21:37:28 -07:00 • committed by GitHub
parent 77bd0d9a64
commit 05b6ca9d5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 723 additions and 133 deletions

View file

@ -182,22 +182,50 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.Equal(master, copy.LastOwner);
}
[Fact]
public void UncontrolledSummon_KeepsItsSummonMaster()
// One serialized master; the Controlled and Summoned flags decide which views expose it.
[Theory]
[InlineData(true, false)] // controlled pet
[InlineData(true, true)] // controlled summon
[InlineData(false, true)] // energy vortex: summoned, never controlled
[InlineData(false, false)] // EnragedCreature: a summon master without Summoned
public void MasterShapes_RoundTrip(bool controlled, bool summoned)
{
var bc = NewCreature();
var master = NewMaster();
// Energy vortex-style: summoned with a master, never controlled.
bc.Summoned = true;
bc.SummonMaster = master;
bc.Summoned = summoned;
if (controlled)
{
bc.SetControlMaster(master);
}
else
{
bc.SummonMaster = master;
}
var copy = Load(Snapshot(bc));
Assert.True(copy.Summoned);
Assert.False(copy.Controlled);
Assert.Equal(master, copy.SummonMaster);
Assert.Null(copy.ControlMaster);
Assert.Equal(controlled, copy.Controlled);
Assert.Equal(summoned, copy.Summoned);
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);
}
[Fact]
@ -418,8 +446,8 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.Equal(0.6, copy.CurrentMoveSpeed); // passive mode, inheriting
}
// ControlMaster and SummonMaster are independent references: a summon master can
// exist without Summoned (EnragedCreature), and a controlled summon carries both.
// A summon master can exist 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
@ -452,4 +480,79 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.InRange(copy.SummonEndValue, summonEnd - TimeSpan.FromSeconds(1), summonEnd + TimeSpan.FromSeconds(1));
}
}
// Before summon ownership followed transfers, a traded summon kept its caster as summon master.
[Fact]
public void LegacyV22Stream_DifferingMasters_OwnerWins()
{
var owner = NewMaster();
var caster = NewMaster();
var copy = LoadLegacyV22(true, owner, true, caster, Core.Now + TimeSpan.FromMinutes(5));
Assert.Equal(owner, copy.ControlMaster);
Assert.Equal(owner, copy.SummonMaster);
}
// v23 bit positions: every save-flagged field in schema order (DefaultAI is not flagged).
private const int V23Controlled = 13;
private const int V23ControlMaster = 14;
private const int V23Summoned = 20;
private const int V23SummonEnd = 21;
private const int V23SummonMaster = 22;
private const int V23RemoveStep = 50;
private const int V23CorpseNameOverride = 52;
// v23 wrote the owner and the summoner to separate slots; v24 keeps one master.
[Fact]
public void V23Stream_MigratesToOneMaster_AndDefaultsAbsentFields()
{
var owner = NewMaster();
var caster = NewMaster();
var summonEnd = Core.Now + TimeSpan.FromMinutes(5);
var donor = new MobileStub();
donor.DefaultMobileInit();
_created.Add(donor);
var writer = new BufferWriter(true);
donor.Serialize(writer);
writer.Write(23); // version
writer.Write(
1UL << V23Controlled | 1UL << V23ControlMaster | 1UL << V23Summoned | 1UL << V23SummonEnd |
1UL << V23SummonMaster | 1UL << V23RemoveStep | 1UL << V23CorpseNameOverride
);
writer.WriteEncodedInt((int)AIType.AI_Melee); // DefaultAI
writer.Write(owner); // ControlMaster
writer.WriteAnchoredTime(summonEnd); // SummonEnd
writer.Write(caster); // SummonMaster
writer.WriteEncodedInt(3); // RemoveStep
writer.Write("a migrated corpse"); // CorpseNameOverride
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
var copy = Load(buffer);
Assert.True(copy.Controlled);
Assert.True(copy.Summoned);
Assert.Equal(owner, copy.ControlMaster);
Assert.Equal(owner, copy.SummonMaster);
Assert.InRange(copy.SummonEndValue, summonEnd - TimeSpan.FromSeconds(1), summonEnd + TimeSpan.FromSeconds(1));
Assert.Equal(3, copy.RemoveStep);
Assert.Equal("a migrated corpse", copy.CorpseNameOverride);
// Absent fields take the class defaults, not default(T).
Assert.Equal(AIType.AI_Melee, copy.AI);
Assert.Equal(-1, copy.HitsMaxSeed);
Assert.Equal(1, copy.ControlSlots);
Assert.Equal(BaseCreature.MaxLoyalty, copy.Loyalty);
Assert.Equal(100, copy.PhysicalDamage);
Assert.Equal(10, copy.RangeHome);
Assert.Equal(FightMode.Closest, copy.FightMode);
Assert.Equal(0.3, copy.ActiveSpeed);
Assert.Equal(0.6, copy.PassiveSpeed);
Assert.NotNull(copy.Owners);
}
}

View file

@ -11,7 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.SkippableFact" Version="1.5.85" />
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.1.0" PrivateAssets="all" />
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.2.0" PrivateAssets="all" />
<ProjectReference Include="..\Server\Server.csproj" />
<ProjectReference Include="..\UOContent\UOContent.csproj" />
<ProjectReference Include="..\Server.Tests\Server.Tests.csproj" />

View file

@ -228,11 +228,6 @@ public partial class BallOfSummoning : Item, TranslocationItem
{
pet.SetControlMaster(from);
if (pet.SummonMaster != null)
{
pet.SummonMaster = from;
}
pet.ControlTarget = from;
pet.ControlOrder = OrderType.Follow;

View file

@ -0,0 +1,465 @@
{
"version": 24,
"type": "Server.Mobiles.BaseCreature",
"properties": [
{
"name": "DefaultAI",
"type": "Server.Mobiles.AIType",
"rule": "EnumMigrationRule"
},
{
"name": "CurrentAI",
"type": "Server.Mobiles.AIType",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "RangePerception",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "RangeFight",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "RangeHome",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "Team",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "FightMode",
"type": "Server.Mobiles.FightMode",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "ActiveSpeed",
"type": "double",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "PassiveSpeed",
"type": "double",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "CurrentSpeed",
"type": "double",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "ActiveMoveSpeed",
"type": "double",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "PassiveMoveSpeed",
"type": "double",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Home",
"type": "Server.Point3D",
"usesSaveFlag": true,
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"Point3D"
]
},
{
"name": "HomeMap",
"type": "Server.Map",
"usesSaveFlag": true,
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"Map"
]
},
{
"name": "Controlled",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Master",
"type": "Server.Mobile",
"usesSaveFlag": true,
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "ControlTarget",
"type": "Server.Mobile",
"usesSaveFlag": true,
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "ControlDest",
"type": "Server.Point3D",
"usesSaveFlag": true,
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"Point3D"
]
},
{
"name": "ControlOrder",
"type": "Server.Mobiles.OrderType",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "MinTameSkill",
"type": "double",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Tamable",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Summoned",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "SummonEnd",
"type": "System.DateTime",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"AnchoredTime"
]
},
{
"name": "ControlSlots",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "Loyalty",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "CurrentWayPoint",
"type": "Server.Items.WayPoint",
"usesSaveFlag": true,
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "HitsMaxSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "StamMaxSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "ManaMaxSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "DamageMin",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "DamageMax",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "PhysicalResistanceSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "FireResistSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "ColdResistSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "PoisonResistSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "EnergyResistSeed",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "PhysicalDamage",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "FireDamage",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "ColdDamage",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "PoisonDamage",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "EnergyDamage",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "Owners",
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
"usesSaveFlag": true,
"rule": "ListMigrationRule",
"ruleArguments": [
"@Tidy",
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "IsDeadPet",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "IsBonded",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "BondingBegin",
"type": "System.DateTime",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "OwnerAbandonTime",
"type": "System.DateTime",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "HasGeneratedLoot",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "IsParagon",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Friends",
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
"usesSaveFlag": true,
"rule": "ListMigrationRule",
"ruleArguments": [
"@Tidy",
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "RemoveIfUntamed",
"type": "bool",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "RemoveStep",
"type": "int",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "PendingDeleteTimer",
"type": "Server.Timer",
"usesSaveFlag": true,
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
},
{
"name": "CorpseNameOverride",
"type": "string",
"usesSaveFlag": true,
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -451,11 +451,6 @@ public abstract partial class BaseAI
Mobile.SetControlMaster(e.Mobile);
if (Mobile.SummonMaster != null)
{
Mobile.SummonMaster = e.Mobile;
}
return true;
}
}

View file

@ -156,11 +156,6 @@ internal sealed partial class TransferItem : Item
private void TransferPetOwnership(Mobile from, Mobile to)
{
if (_creature.SummonMaster != null)
{
_creature.SummonMaster = to;
}
_creature.ClearPetFriends();
_creature.IssueOrder(OrderType.Follow, null, to);
_creature.BondingBegin = DateTime.MinValue;

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using ModernUO.CodeGeneratedEvents;
using ModernUO.Serialization;
using Server.Collections;
using Server.ContextMenus;
@ -135,7 +134,7 @@ namespace Server.Mobiles
public int CompareTo(DamageStore ds) => (ds?.m_Damage ?? 0).CompareTo(m_Damage);
}
[SerializationGenerator(23, false)]
[SerializationGenerator(24, false)]
public abstract partial class BaseCreature : Mobile, IHonorTarget, IQuestGiver
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseCreature));
@ -446,10 +445,12 @@ namespace Server.Mobiles
InvalidateProperties();
}
// Follower bookkeeping brackets the assignment, so the property is hand-written.
private Mobile _controlMaster;
// ControlMaster and SummonMaster are views of this one reference, gated by Controlled and Summoned.
[SerializableField(15, getter: "private", setter: "private")]
[SaveFlag(nameof(ShouldSerializeMaster))]
private Mobile _master;
private bool ShouldSerializeControlMaster() => _controlMaster != null;
private bool ShouldSerializeMaster() => _master != null;
[SerializableField(16)]
[SaveFlag(nameof(ShouldSerializeControlTarget))]
@ -503,13 +504,8 @@ namespace Server.Mobiles
private bool ShouldSerializeSummonEnd() => _summoned;
// Follower bookkeeping brackets the assignment, so the property is hand-written.
private Mobile _summonMaster;
private bool ShouldSerializeSummonMaster() => _summonMaster != null;
[EncodedInt]
[SerializableField(24)]
[SerializableField(23)]
[SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))]
[SerializedCommandProperty(AccessLevel.Administrator)]
private int _controlSlots = 1;
@ -519,7 +515,7 @@ namespace Server.Mobiles
private int ControlSlotsDefaultValue() => 1;
[EncodedInt]
[SerializableField(25, allowFieldChange: nameof(ClampLoyalty))]
[SerializableField(24, allowFieldChange: nameof(ClampLoyalty))]
[SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _loyalty;
@ -534,7 +530,7 @@ namespace Server.Mobiles
return true;
}
[SerializableField(26)]
[SerializableField(25)]
[SaveFlag(nameof(ShouldSerializeCurrentWayPoint))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private WayPoint _currentWayPoint;
@ -542,7 +538,7 @@ namespace Server.Mobiles
private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null;
[EncodedInt]
[SerializableField(27)]
[SerializableField(26)]
[SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _hitsMaxSeed = -1;
@ -552,7 +548,7 @@ namespace Server.Mobiles
private int HitsMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(28)]
[SerializableField(27)]
[SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _stamMaxSeed = -1;
@ -562,7 +558,7 @@ namespace Server.Mobiles
private int StamMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(29)]
[SerializableField(28)]
[SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _manaMaxSeed = -1;
@ -572,7 +568,7 @@ namespace Server.Mobiles
private int ManaMaxSeedDefaultValue() => -1;
[EncodedInt]
[SerializableField(30)]
[SerializableField(29)]
[SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMin = -1;
@ -582,7 +578,7 @@ namespace Server.Mobiles
private int DamageMinDefaultValue() => -1;
[EncodedInt]
[SerializableField(31)]
[SerializableField(30)]
[SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _damageMax = -1;
@ -592,7 +588,7 @@ namespace Server.Mobiles
private int DamageMaxDefaultValue() => -1;
[EncodedInt]
[SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(31, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalResistanceSeed;
@ -602,7 +598,7 @@ namespace Server.Mobiles
private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances();
[EncodedInt]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeFireResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireResistSeed;
@ -610,7 +606,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0;
[EncodedInt]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeColdResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldResistSeed;
@ -618,7 +614,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0;
[EncodedInt]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializePoisonResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonResistSeed;
@ -626,7 +622,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0;
[EncodedInt]
[SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))]
[SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))]
[SaveFlag(nameof(ShouldSerializeEnergyResistSeed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyResistSeed;
@ -634,7 +630,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0;
[EncodedInt]
[SerializableField(37)]
[SerializableField(36)]
[SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _physicalDamage = 100;
@ -644,7 +640,7 @@ namespace Server.Mobiles
private int PhysicalDamageDefaultValue() => 100;
[EncodedInt]
[SerializableField(38)]
[SerializableField(37)]
[SaveFlag(nameof(ShouldSerializeFireDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _fireDamage;
@ -652,7 +648,7 @@ namespace Server.Mobiles
private bool ShouldSerializeFireDamage() => _fireDamage != 0;
[EncodedInt]
[SerializableField(39)]
[SerializableField(38)]
[SaveFlag(nameof(ShouldSerializeColdDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _coldDamage;
@ -660,7 +656,7 @@ namespace Server.Mobiles
private bool ShouldSerializeColdDamage() => _coldDamage != 0;
[EncodedInt]
[SerializableField(40)]
[SerializableField(39)]
[SaveFlag(nameof(ShouldSerializePoisonDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _poisonDamage;
@ -668,7 +664,7 @@ namespace Server.Mobiles
private bool ShouldSerializePoisonDamage() => _poisonDamage != 0;
[EncodedInt]
[SerializableField(41)]
[SerializableField(40)]
[SaveFlag(nameof(ShouldSerializeEnergyDamage))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _energyDamage;
@ -676,7 +672,7 @@ namespace Server.Mobiles
private bool ShouldSerializeEnergyDamage() => _energyDamage != 0;
[Tidy]
[SerializableField(42, setter: "private")]
[SerializableField(41, setter: "private")]
[SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))]
private List<Mobile> _owners;
@ -688,13 +684,13 @@ namespace Server.Mobiles
private List<Mobile> OwnersDefaultValue() => new();
[SerializableField(43)]
[SerializableField(42)]
[SaveFlag(nameof(ShouldSerializeIsDeadPet))]
private bool _isDeadPet;
private bool ShouldSerializeIsDeadPet() => _isDeadPet;
[SerializableField(44, fieldChanged: nameof(OnBondedChange))]
[SerializableField(43, fieldChanged: nameof(OnBondedChange))]
[SaveFlag(nameof(ShouldSerializeIsBonded))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _isBonded;
@ -703,21 +699,21 @@ namespace Server.Mobiles
private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties();
[SerializableField(45)]
[SerializableField(44)]
[SaveFlag(nameof(ShouldSerializeBondingBegin))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _bondingBegin;
private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue;
[SerializableField(46)]
[SerializableField(45)]
[SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private DateTime _ownerAbandonTime;
private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue;
[SerializableField(47)]
[SerializableField(46)]
[SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))]
private bool _hasGeneratedLoot;
@ -729,7 +725,7 @@ namespace Server.Mobiles
private bool ShouldSerializeIsParagon() => _isParagon;
[Tidy]
[SerializableField(49, setter: "private")]
[SerializableField(48, setter: "private")]
[SaveFlag(nameof(ShouldSerializeFriends))]
private List<Mobile> _friends;
@ -739,7 +735,7 @@ namespace Server.Mobiles
return _friends?.Count > 0;
}
[SerializableField(50)]
[SerializableField(49)]
[SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _removeIfUntamed;
@ -747,21 +743,21 @@ namespace Server.Mobiles
private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed;
[EncodedInt]
[SerializableField(51)]
[SerializableField(50)]
[SaveFlag(nameof(ShouldSerializeRemoveStep))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _removeStep;
private bool ShouldSerializeRemoveStep() => _removeStep != 0;
[SerializableField(52, setter: "private")]
[SerializableField(51, setter: "private")]
[SaveFlag(nameof(ShouldSerializePendingDeleteTimer))]
[DeserializeTimer(nameof(DeserializePendingDeleteTimer))]
private Timer _pendingDeleteTimer;
// Stabled and controlled pets never resume a delete countdown.
private bool ShouldSerializePendingDeleteTimer() =>
_pendingDeleteTimer?.Running == true && !IsStabled && !(_controlled && _controlMaster != null);
_pendingDeleteTimer?.Running == true && !IsStabled && ControlMaster == null;
private void DeserializePendingDeleteTimer(TimeSpan delay)
{
@ -769,7 +765,7 @@ namespace Server.Mobiles
_pendingDeleteTimer.Start();
}
[SerializableField(53)]
[SerializableField(52)]
[SaveFlag(nameof(ShouldSerializeCorpseNameOverride))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _corpseNameOverride;
@ -847,7 +843,6 @@ namespace Server.Mobiles
Debug = false;
_controlled = false;
_controlMaster = null;
ControlTarget = null;
_controlOrder = OrderType.None;
@ -931,7 +926,7 @@ namespace Server.Mobiles
public virtual double WeaponAbilityChance => 0.4;
[SerializableProperty(48, useField: nameof(_isParagon))]
[SerializableProperty(47, useField: nameof(_isParagon))]
[SaveFlag(nameof(ShouldSerializeIsParagon))]
[CommandProperty(AccessLevel.GameMaster)]
public bool IsParagon
@ -1028,8 +1023,8 @@ namespace Server.Mobiles
}
public virtual bool IsNecroFamiliar =>
Summoned && _controlMaster != null &&
SummonFamiliarSpell.Table.TryGetValue(_controlMaster, out var bc) && bc == this;
Summoned && ControlMaster != null &&
SummonFamiliarSpell.Table.TryGetValue(ControlMaster, out var bc) && bc == this;
public virtual bool DeleteCorpseOnDeath => !Core.AOS && _summoned;
@ -1157,54 +1152,53 @@ namespace Server.Mobiles
}
}
[SerializableProperty(15, useField: nameof(_controlMaster))]
[SaveFlag(nameof(ShouldSerializeControlMaster))]
// Assigned before Controlled is raised (SetControlMaster), so the setter cannot be gated.
[CommandProperty(AccessLevel.GameMaster)]
public Mobile ControlMaster
{
get => _controlMaster;
get => _controlled ? _master : null;
set
{
if (_controlMaster == value || this == value)
if (ControlMaster == value || this == value)
{
return;
}
RemoveFollowers();
_controlMaster = value;
AddFollowers();
if (_controlMaster != null)
SetMaster(value);
if (value != null)
{
StopDeleteTimer();
}
Delta(MobileDelta.Noto);
this.MarkDirty();
}
}
[SerializableProperty(23, useField: nameof(_summonMaster))]
[SaveFlag(nameof(ShouldSerializeSummonMaster))]
// 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.
[CommandProperty(AccessLevel.GameMaster)]
public Mobile SummonMaster
{
get => _summonMaster;
get => _controlled && !_summoned ? null : _master;
set
{
if (_summonMaster == value || this == value)
if (SummonMaster == value || this == value || _controlled && !_summoned)
{
return;
}
RemoveFollowers();
_summonMaster = value;
AddFollowers();
Delta(MobileDelta.Noto);
this.MarkDirty();
SetMaster(value);
}
}
private void SetMaster(Mobile value)
{
RemoveFollowers();
Master = value;
AddFollowers();
Delta(MobileDelta.Noto);
}
// Fires on every assignment, not only changes: a reissued order is a command (retarget, re-anchor).
// A raw assignment is system-issued; player commands go through IssueOrder.
[SerializableProperty(18, useField: nameof(_controlOrder))]
@ -1271,7 +1265,7 @@ namespace Server.Mobiles
}
InvalidateProperties();
_controlMaster?.InvalidateProperties();
ControlMaster?.InvalidateProperties();
this.MarkDirty();
}
@ -2195,9 +2189,75 @@ namespace Server.Mobiles
}
}
// Absent fields resolve through the same default-value methods the live reader uses.
private void MigrateFrom(V23Content content)
{
_defaultAI = content.DefaultAI;
_currentAI = content.CurrentAI ?? CurrentAIDefaultValue();
_rangePerception = content.RangePerception ?? RangePerceptionDefaultValue();
_rangeFight = content.RangeFight ?? RangeFightDefaultValue();
_rangeHome = content.RangeHome ?? RangeHomeDefaultValue();
_team = content.Team ?? 0;
_fightMode = content.FightMode ?? FightModeDefaultValue();
_activeSpeed = content.ActiveSpeed ?? ActiveSpeedDefaultValue();
_passiveSpeed = content.PassiveSpeed ?? PassiveSpeedDefaultValue();
_currentSpeed = content.CurrentSpeed ?? CurrentSpeedDefaultValue();
_activeMoveSpeed = content.ActiveMoveSpeed ?? ActiveMoveSpeedDefaultValue();
_passiveMoveSpeed = content.PassiveMoveSpeed ?? PassiveMoveSpeedDefaultValue();
_home = content.Home ?? Point3D.Zero;
_homeMap = content.HomeMap;
_controlled = content.Controlled;
// A traded summon could still carry its caster as summon master; the owner wins.
_master = content.ControlMaster ?? content.SummonMaster;
_controlTarget = content.ControlTarget;
_controlDest = content.ControlDest ?? Point3D.Zero;
_controlOrder = content.ControlOrder ?? OrderType.None;
_minTameSkill = content.MinTameSkill ?? 0;
_tamable = content.Tamable;
_summoned = content.Summoned;
_summonEnd = content.SummonEnd ?? DateTime.MinValue;
_controlSlots = content.ControlSlots ?? ControlSlotsDefaultValue();
_loyalty = content.Loyalty ?? LoyaltyDefaultValue();
_currentWayPoint = content.CurrentWayPoint;
_hitsMaxSeed = content.HitsMaxSeed ?? HitsMaxSeedDefaultValue();
_stamMaxSeed = content.StamMaxSeed ?? StamMaxSeedDefaultValue();
_manaMaxSeed = content.ManaMaxSeed ?? ManaMaxSeedDefaultValue();
_damageMin = content.DamageMin ?? DamageMinDefaultValue();
_damageMax = content.DamageMax ?? DamageMaxDefaultValue();
_physicalResistanceSeed = content.PhysicalResistanceSeed ?? 0;
_fireResistSeed = content.FireResistSeed ?? 0;
_coldResistSeed = content.ColdResistSeed ?? 0;
_poisonResistSeed = content.PoisonResistSeed ?? 0;
_energyResistSeed = content.EnergyResistSeed ?? 0;
_physicalDamage = content.PhysicalDamage ?? PhysicalDamageDefaultValue();
_fireDamage = content.FireDamage ?? 0;
_coldDamage = content.ColdDamage ?? 0;
_poisonDamage = content.PoisonDamage ?? 0;
_energyDamage = content.EnergyDamage ?? 0;
_owners = content.Owners ?? OwnersDefaultValue();
_isDeadPet = content.IsDeadPet;
_isBonded = content.IsBonded;
_bondingBegin = content.BondingBegin ?? DateTime.MinValue;
_ownerAbandonTime = content.OwnerAbandonTime ?? DateTime.MinValue;
_hasGeneratedLoot = content.HasGeneratedLoot;
_isParagon = content.IsParagon;
_friends = content.Friends;
_removeIfUntamed = content.RemoveIfUntamed;
_removeStep = content.RemoveStep ?? 0;
if (content.PendingDeleteTimerDelay != TimeSpan.MinValue)
{
DeserializePendingDeleteTimer(content.PendingDeleteTimerDelay);
}
_corpseNameOverride = content.CorpseNameOverride;
}
// Pre-codegen loads only (versions 0-22); post-codegen bumps use MigrateFrom.
private void Deserialize(IGenericReader reader, int version)
{
Mobile controlMaster = null;
_currentAI = (AIType)reader.ReadInt();
_defaultAI = (AIType)reader.ReadInt();
@ -2245,7 +2305,7 @@ namespace Server.Mobiles
_fightMode = (FightMode)reader.ReadInt();
_controlled = reader.ReadBool();
_controlMaster = reader.ReadEntity<Mobile>();
controlMaster = reader.ReadEntity<Mobile>();
_controlTarget = reader.ReadEntity<Mobile>();
_controlDest = reader.ReadPoint3D();
_controlOrder = (OrderType)reader.ReadInt();
@ -2273,7 +2333,6 @@ namespace Server.Mobiles
_fightMode = FightMode.Closest;
_controlled = false;
_controlMaster = null;
_controlTarget = null;
_controlOrder = OrderType.None;
}
@ -2294,9 +2353,12 @@ namespace Server.Mobiles
if (version >= 5)
{
_summonMaster = reader.ReadEntity<Mobile>();
_master = reader.ReadEntity<Mobile>();
}
// A transferred summon kept its caster as summon master; the owner wins.
_master = controlMaster ?? _master;
if (version >= 6)
{
_hitsMaxSeed = reader.ReadInt();
@ -2457,7 +2519,7 @@ namespace Server.Mobiles
if (IsAnimatedDead)
{
AnimateDeadSpell.Register(_summonMaster, this);
AnimateDeadSpell.Register(SummonMaster, this);
}
}
@ -2566,7 +2628,7 @@ namespace Server.Mobiles
public void RemoveFollowers()
{
var master = _controlMaster ?? _summonMaster;
var master = _master;
if (master != null)
{
master.Followers -= Math.Min(ControlSlots, master.Followers);
@ -2580,7 +2642,7 @@ namespace Server.Mobiles
public void AddFollowers()
{
var master = _controlMaster ?? _summonMaster;
var master = _master;
if (master != null)
{
master.Followers += ControlSlots;
@ -2661,7 +2723,7 @@ namespace Server.Mobiles
if (IsAnimatedDead)
{
AnimateDeadSpell.Unregister(_summonMaster, this);
AnimateDeadSpell.Unregister(SummonMaster, this);
}
if (Summoned && SummonMaster != null)
@ -2842,7 +2904,7 @@ namespace Server.Mobiles
}
public override bool IsHarmfulCriminal(Mobile target) =>
(!Controlled || target != _controlMaster) && (!Summoned || target != _summonMaster) &&
target != ControlMaster && (!Summoned || target != SummonMaster) &&
(target is not BaseCreature { InitialInnocent: true } creature || creature.Controlled) &&
(target is not PlayerMobile mobile || mobile.PermaFlags.Count <= 0) && base.IsHarmfulCriminal(target);
@ -2850,16 +2912,9 @@ namespace Server.Mobiles
{
base.CriminalAction(message);
if (Controlled || Summoned)
if ((Controlled || Summoned) && _master?.Player == true)
{
if (_controlMaster?.Player == true)
{
_controlMaster.CriminalAction(false);
}
else if (_summonMaster?.Player == true)
{
_summonMaster.CriminalAction(false);
}
_master.CriminalAction(false);
}
}
@ -2867,7 +2922,7 @@ namespace Server.Mobiles
{
base.DoHarmful(target, indirect);
if (target == this || target == _controlMaster || target == _summonMaster || !Controlled && !Summoned)
if (target == this || target == _master || !Controlled && !Summoned)
{
return;
}
@ -3644,7 +3699,7 @@ namespace Server.Mobiles
{
CreatureEvents.CreatureDeletedEvent(this);
var m = _controlMaster;
var m = ControlMaster;
SetControlMaster(null);
SummonMaster = null;
@ -3951,14 +4006,9 @@ namespace Server.Mobiles
return BardMaster;
}
if (_controlled && _controlMaster != null)
if ((_controlled || _summoned) && _master != null)
{
return _controlMaster;
}
if (_summoned && _summonMaster != null)
{
return _summonMaster;
return _master;
}
return base.GetDamageMaster(damagee);
@ -4549,7 +4599,7 @@ namespace Server.Mobiles
if (IsBondable && !IsBonded)
{
var master = _controlMaster;
var master = ControlMaster;
if (master != null && master == from) // So friends can't start the bonding process
{

View file

@ -3553,7 +3553,6 @@ namespace Server.Mobiles
pet.Internalize();
pet.SetControlMaster(null);
pet.SummonMaster = null;
pet.IsStabled = true;
pet.StabledBy = this;
@ -3602,11 +3601,6 @@ namespace Server.Mobiles
{
pet.SetControlMaster(this);
if (pet.SummonMaster != null)
{
pet.SummonMaster = this;
}
pet.ControlTarget = this;
pet.ControlOrder = OrderType.Follow;

View file

@ -255,7 +255,6 @@ namespace Server.Mobiles
pet.Internalize();
pet.SetControlMaster(null);
pet.SummonMaster = null;
pet.IsStabled = true;
pet.StabledBy = from;
@ -357,11 +356,6 @@ namespace Server.Mobiles
{
pet.SetControlMaster(from);
if (pet.SummonMaster != null)
{
pet.SummonMaster = from;
}
pet.ControlTarget = from;
pet.ControlOrder = OrderType.Follow;

View file

@ -144,7 +144,6 @@ public class JailSystem : GenericPersistence
bc.Internalize();
bc.SetControlMaster(null);
bc.SummonMaster = null;
bc.IsStabled = true;
bc.StabledBy = from;