ModernUO/Projects/UOContent.Tests/Tests/Mobiles/DeletedMasterTests.cs
Kamron Batman 95177fb647
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
2026-09-24 23:01:40 -07:00

114 lines
2.6 KiB
C#

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));
}
}