fix: player corpse notoriety ignores the owner's body at death (#2669)

CorpseNotoriety returned Murderer for a player corpse whenever the corpse body was a monster
body. RunUO's check was `body.IsMonster && IsSummoned(owner)`, which is never true for a player;
the second half was lost when the branch was simplified (#134). Players who died while
polymorphed or in a necromancer form left a corpse with the wrong notoriety.

Player corpses also check murderer and criminal status before guild standing again, matching
RunUO and MobileNotoriety.

Tests: UOContent.Tests 1162 passed (4 new CorpseNotorietyTests).
This commit is contained in:
Kamron Batman 2026-09-23 19:43:37 -07:00 • committed by GitHub
parent 08dab47413
commit 92ed9c5456
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 16 deletions

View file

@ -0,0 +1,40 @@
using Server;
using Server.Items;
using Server.Misc;
using Server.Mobiles;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class CorpseNotorietyTests
{
[Theory]
[InlineData(0, Notoriety.Innocent)] // Human body
[InlineData(746, Notoriety.Innocent)] // Horrific Beast, a monster body
[InlineData(0, Notoriety.Murderer, 5)]
[InlineData(746, Notoriety.Murderer, 5)]
public void PlayerCorpseNotorietyIgnoresTransformedBody(int bodyMod, int expected, int kills = 0)
{
var victim = new PlayerMobile(World.NewMobile);
victim.DefaultMobileInit();
victim.Kills = kills;
victim.BodyMod = bodyMod;
var looter = new PlayerMobile(World.NewMobile);
looter.DefaultMobileInit();
var corpse = new Corpse(victim, []);
try
{
Assert.Equal(expected, NotorietyHandlers.CorpseNotoriety(looter, corpse));
}
finally
{
corpse.Delete();
victim.Delete();
looter.Delete();
}
}
}

View file

@ -270,7 +270,24 @@ namespace Server.Misc
return Notoriety.CanBeAttacked;
}
Body body = target.Amount;
// BaseCreatures are deleted on death, so target.Owner is null after a server restart.
// The OwnerWasBaseCreature flag is the persisted snapshot that survives the live mobile.
var creature = target.Owner as BaseCreature;
var ownerWasCreature = target.OwnerWasBaseCreature || creature != null;
// A player's murderer and criminal status outrank guild standing, as on the live mobile.
if (!ownerWasCreature)
{
if (target.Murderer)
{
return Notoriety.Murderer;
}
if (target.Criminal && (target.Map?.Rules & MapRules.HarmfulRestrictions) == 0)
{
return Notoriety.Criminal;
}
}
var sourceGuild = GetGuildFor(source.Guild as Guild, source);
var targetGuild = GetGuildFor(target.Guild, target.Owner);
@ -292,11 +309,7 @@ namespace Server.Misc
}
}
// BaseCreatures are deleted on death, so target.Owner is null after a server restart.
// The OwnerWasBaseCreature flag is the persisted snapshot that survives the live mobile.
var creature = target.Owner as BaseCreature;
if (target.OwnerWasBaseCreature || creature != null)
if (ownerWasCreature)
{
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
{
@ -309,6 +322,7 @@ namespace Server.Misc
}
var actual = Notoriety.CanBeAttacked;
Body body = target.Amount;
if (target.Murderer || body.IsMonster && target.OwnerWasSummoned || target.OwnerWasAnimatedDead)
{
@ -333,16 +347,6 @@ namespace Server.Misc
return Notoriety.Innocent;
}
if (target.Murderer || body.IsMonster)
{
return Notoriety.Murderer;
}
if (target.Criminal && (target.Map?.Rules & MapRules.HarmfulRestrictions) == 0)
{
return Notoriety.Criminal;
}
if (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
{
for (var i = 0; i < list.Count; ++i)