fix: a deleted character's followers, and familiars that never unregistered (#2675)
## Summary **Followers of a deleted character.** `PlayerMobile.OnDelete` deleted stabled pets and left every active follower pointing at the deleted mobile. Pets stayed controlled by nobody, and after a restart they loaded as owned pets with no owner. Now, when a character is deleted: - bonded or dead pets and summons (controlled or not) are deleted; - unbonded pets are released: they go wild and despawn on the abandoned-pet timer; - escorts and hirelings are left alone, because they already notice a deleted master and walk off on their own. **Familiars and animated dead that never unregistered.** `BaseCreature` unregistered them in `OnAfterDelete`, after `OnDelete` had already cleared the master. Both calls did nothing, so `SummonFamiliarSpell.Table` and the animated-dead registry kept deleted creatures until the caster's next cast. The unregisters now run in `OnDelete`, before the master is cleared. ## Test plan - [x] `DeletedMasterTests` (6 cases, all failing on main): unbonded pet released; bonded and bonded-dead pets deleted; controlled and uncontrolled summons deleted; familiar unregistered on delete - [x] UOContent.Tests (1184 passed, 2 skipped) and Server.Tests (907) green
This commit is contained in:
parent
843a29e7dc
commit
95177fb647
3 changed files with 154 additions and 10 deletions
114
Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
Normal file
114
Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server;
|
||||
using Server.Mobiles;
|
||||
using Server.Spells.Necromancy;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Mobiles;
|
||||
|
||||
// A deleted master must not leave creatures pointing at it.
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class DeletedMasterTests : IDisposable
|
||||
{
|
||||
private readonly List<Mobile> _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;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeletingAPlayer_ReleasesAnUnbondedPet()
|
||||
{
|
||||
var owner = NewPlayer();
|
||||
var pet = NewCreature();
|
||||
pet.SetControlMaster(owner);
|
||||
|
||||
owner.Delete();
|
||||
|
||||
Assert.False(pet.Deleted);
|
||||
Assert.False(pet.Controlled);
|
||||
Assert.Null(pet.Master);
|
||||
Assert.True(pet.PendingDeleteTimer?.Running);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)] // bonded
|
||||
[InlineData(true)] // bonded and dead
|
||||
public void DeletingAPlayer_DeletesABondedPet(bool dead)
|
||||
{
|
||||
var owner = NewPlayer();
|
||||
var pet = NewCreature();
|
||||
pet.SetControlMaster(owner);
|
||||
pet.IsBonded = true;
|
||||
pet.IsDeadPet = dead;
|
||||
|
||||
owner.Delete();
|
||||
|
||||
Assert.True(pet.Deleted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)] // energy vortex
|
||||
[InlineData(true)] // controlled summon
|
||||
public void DeletingAPlayer_DeletesItsSummons(bool controlled)
|
||||
{
|
||||
var caster = NewPlayer();
|
||||
var summon = NewCreature();
|
||||
|
||||
if (controlled)
|
||||
{
|
||||
summon.SetControlMaster(caster);
|
||||
}
|
||||
|
||||
summon.Summoned = true;
|
||||
summon.Master = caster;
|
||||
|
||||
caster.Delete();
|
||||
|
||||
Assert.True(summon.Deleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeletingAFamiliar_UnregistersIt()
|
||||
{
|
||||
var caster = NewPlayer();
|
||||
var familiar = NewCreature();
|
||||
familiar.SetControlMaster(caster);
|
||||
familiar.Summoned = true;
|
||||
SummonFamiliarSpell.Table[caster] = familiar;
|
||||
|
||||
familiar.Delete();
|
||||
|
||||
Assert.False(SummonFamiliarSpell.Table.ContainsKey(caster));
|
||||
}
|
||||
}
|
||||
|
|
@ -2717,16 +2717,6 @@ namespace Server.Mobiles
|
|||
|
||||
FocusMob = null;
|
||||
|
||||
if (IsAnimatedDead)
|
||||
{
|
||||
AnimateDeadSpell.Unregister(SummonMaster, this);
|
||||
}
|
||||
|
||||
if (SummonMaster != null)
|
||||
{
|
||||
SummonFamiliarSpell.Unregister(SummonMaster, this);
|
||||
}
|
||||
|
||||
if (MLQuestSystem.Enabled)
|
||||
{
|
||||
MLQuestSystem.HandleDeletion(this);
|
||||
|
|
@ -3683,6 +3673,17 @@ namespace Server.Mobiles
|
|||
{
|
||||
CreatureEvents.CreatureDeletedEvent(this);
|
||||
|
||||
// Both registries are keyed by the master, which is cleared below.
|
||||
if (IsAnimatedDead)
|
||||
{
|
||||
AnimateDeadSpell.Unregister(SummonMaster, this);
|
||||
}
|
||||
|
||||
if (SummonMaster != null)
|
||||
{
|
||||
SummonFamiliarSpell.Unregister(SummonMaster, this);
|
||||
}
|
||||
|
||||
var m = ControlMaster;
|
||||
SetControlMaster(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -3835,6 +3835,35 @@ namespace Server.Mobiles
|
|||
|
||||
Stabled = null;
|
||||
}
|
||||
|
||||
if (_allFollowers?.Count > 0)
|
||||
{
|
||||
// Releasing or deleting a follower removes it from _allFollowers.
|
||||
using var followers = PooledRefQueue<BaseCreature>.Create(_allFollowers.Count);
|
||||
|
||||
foreach (var follower in _allFollowers)
|
||||
{
|
||||
// Escorts and hirelings notice a deleted master and walk off on their own.
|
||||
if (follower is BaseCreature bc and not (BaseEscortable or BaseHire))
|
||||
{
|
||||
followers.Enqueue(bc);
|
||||
}
|
||||
}
|
||||
|
||||
while (followers.Count > 0)
|
||||
{
|
||||
var bc = followers.Dequeue();
|
||||
if (bc.Summoned || bc.IsBonded || bc.IsDeadPet)
|
||||
{
|
||||
bc.Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
// An unbonded pet goes wild and despawns on the abandoned-pet timer.
|
||||
bc.ControlOrder = OrderType.Release;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int ComputeMovementSpeed(Direction dir, bool checkTurning = true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue