fix: Preserve corpse notoriety across server restart (#2426)
BaseCreatures are deleted on death (Mobile.OnDeath calls Delete for non-players), so after save/restart the corpse's _owner reference resolves to null. CorpseNotoriety gated its entire creature branch on `target.Owner is BaseCreature`, falling through to player-corpse logic once the reference vanished. That made monster corpses turn red (body.IsMonster -> Murderer) and innocent NPC corpses turn grey (null is not PlayerMobile -> CanBeAttacked) on the next restart. Snapshots the relevant owner state into CorpseFlag at corpse creation: OwnerWasBaseCreature, OwnerWasSummoned, OwnerWasAnimatedDead. Folds the standalone _murderer bool into CorpseFlag.Murderer for consistency with Criminal. CorpseNotoriety now consults the flags so the creature branch stays correct without a live mobile reference. Bumps Corpse serialization to v16 with a MigrateFrom(V15Content) that maps the old Murderer bool onto the new flag. Pre-fix corpses already on disk decay within 7 minutes; their first post-restart color may be wrong, which is acceptable. Also documents that the schema generator must be run after every version bump (`dotnet tool run ModernUOSchemaGenerator -- ModernUO.slnx`) since `dotnet build` does not emit migration JSON files.
This commit is contained in:
parent
a74c7f9d4e
commit
29e4ecdb1b
5 changed files with 251 additions and 29 deletions
|
|
@ -62,10 +62,31 @@ public enum CorpseFlag
|
|||
/// <summary>
|
||||
/// Has this corpse been self looted?
|
||||
/// </summary>
|
||||
SelfLooted = 0x00000080
|
||||
SelfLooted = 0x00000080,
|
||||
|
||||
/// <summary>
|
||||
/// Was the owner a murderer when he died?
|
||||
/// </summary>
|
||||
Murderer = 0x00000100,
|
||||
|
||||
/// <summary>
|
||||
/// Was the owner a BaseCreature? Snapshot at death so notoriety still resolves after the
|
||||
/// owner Mobile is deleted (BaseCreatures are deleted on death; the reference is null after restart).
|
||||
/// </summary>
|
||||
OwnerWasBaseCreature = 0x00000200,
|
||||
|
||||
/// <summary>
|
||||
/// Was the owner a summoned creature?
|
||||
/// </summary>
|
||||
OwnerWasSummoned = 0x00000400,
|
||||
|
||||
/// <summary>
|
||||
/// Was the owner an animated dead creature?
|
||||
/// </summary>
|
||||
OwnerWasAnimatedDead = 0x00000800
|
||||
}
|
||||
|
||||
[SerializationGenerator(15, false)]
|
||||
[SerializationGenerator(16, false)]
|
||||
public partial class Corpse : Container, ICarvable
|
||||
{
|
||||
public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0);
|
||||
|
|
@ -125,21 +146,17 @@ public partial class Corpse : Container, ICarvable
|
|||
[SerializableField(11, setter: "private")]
|
||||
private Guild _guild;
|
||||
|
||||
[SerializableField(12)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private bool _murderer;
|
||||
|
||||
[SerializableField(13, setter: "private")]
|
||||
[SerializableField(12, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private List<Item> _equipItems;
|
||||
|
||||
[CanBeNull]
|
||||
[SerializableField(14, setter: "private")]
|
||||
[SerializableField(13, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private VirtualHairInfo _hair;
|
||||
|
||||
[CanBeNull]
|
||||
[SerializableField(15, setter: "private")]
|
||||
[SerializableField(14, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private VirtualHairInfo _facialHair;
|
||||
|
||||
|
|
@ -171,9 +188,20 @@ public partial class Corpse : Container, ICarvable
|
|||
|
||||
_accessLevel = owner.AccessLevel;
|
||||
_guild = owner.Guild as Guild;
|
||||
_murderer = owner.Murderer;
|
||||
SetFlag(CorpseFlag.Murderer, owner.Murderer);
|
||||
SetFlag(CorpseFlag.Criminal, owner.Criminal);
|
||||
|
||||
var ownerBaseCreature = owner as BaseCreature;
|
||||
|
||||
// Snapshot owner type & state. BaseCreatures are deleted after death, so the owner mobile
|
||||
// reference becomes null after restart - notoriety must be derivable without it.
|
||||
if (ownerBaseCreature != null)
|
||||
{
|
||||
SetFlag(CorpseFlag.OwnerWasBaseCreature, true);
|
||||
SetFlag(CorpseFlag.OwnerWasSummoned, ownerBaseCreature.Summoned);
|
||||
SetFlag(CorpseFlag.OwnerWasAnimatedDead, ownerBaseCreature.IsAnimatedDead);
|
||||
}
|
||||
|
||||
if (hair?.ItemId > 0)
|
||||
{
|
||||
_hair = new VirtualHairInfo(hair.ItemId, hair.Hue);
|
||||
|
|
@ -192,8 +220,6 @@ public partial class Corpse : Container, ICarvable
|
|||
|
||||
_aggressors = new List<Mobile>(owner.Aggressors.Count + owner.Aggressed.Count);
|
||||
|
||||
var isBaseCreature = owner is BaseCreature;
|
||||
|
||||
var lastTime = TimeSpan.MaxValue;
|
||||
|
||||
for (var i = 0; i < owner.Aggressors.Count; ++i)
|
||||
|
|
@ -206,7 +232,7 @@ public partial class Corpse : Container, ICarvable
|
|||
lastTime = Core.Now - info.LastCombatTime;
|
||||
}
|
||||
|
||||
if (!isBaseCreature && !info.CriminalAggression)
|
||||
if (ownerBaseCreature == null && !info.CriminalAggression)
|
||||
{
|
||||
_aggressors.Add(info.Attacker);
|
||||
}
|
||||
|
|
@ -222,23 +248,21 @@ public partial class Corpse : Container, ICarvable
|
|||
lastTime = Core.Now - info.LastCombatTime;
|
||||
}
|
||||
|
||||
if (!isBaseCreature)
|
||||
if (ownerBaseCreature == null)
|
||||
{
|
||||
_aggressors.Add(info.Defender);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBaseCreature)
|
||||
if (ownerBaseCreature != null)
|
||||
{
|
||||
var bc = (BaseCreature)owner;
|
||||
|
||||
var master = bc.GetMaster();
|
||||
var master = ownerBaseCreature.GetMaster();
|
||||
if (master != null)
|
||||
{
|
||||
_aggressors.Add(master);
|
||||
}
|
||||
|
||||
var rights = BaseCreature.GetLootingRights(bc.DamageEntries, bc.HitsMax);
|
||||
var rights = BaseCreature.GetLootingRights(ownerBaseCreature.DamageEntries, ownerBaseCreature.HitsMax);
|
||||
for (var i = 0; i < rights.Count; ++i)
|
||||
{
|
||||
var ds = rights[i];
|
||||
|
|
@ -255,11 +279,40 @@ public partial class Corpse : Container, ICarvable
|
|||
DevourCorpse();
|
||||
}
|
||||
|
||||
// Replaced int Kills snapshot with bool Murderer snapshot
|
||||
private void MigrateFrom(V14Content content)
|
||||
// Folded Murderer bool field into CorpseFlag.Murderer
|
||||
private void MigrateFrom(V15Content content)
|
||||
{
|
||||
_restoreEquip = content.RestoreEquip;
|
||||
_flags = content.Flags;
|
||||
if (content.Murderer)
|
||||
{
|
||||
_flags |= CorpseFlag.Murderer;
|
||||
}
|
||||
_timeOfDeath = content.TimeOfDeath;
|
||||
_restoreTable = content.RestoreTable;
|
||||
_decayTimer = new InternalTimer(this, content.DecayTimerDelay);
|
||||
_decayTimer.Start();
|
||||
_looters = content.Looters;
|
||||
_killer = content.Killer;
|
||||
_aggressors = content.Aggressors;
|
||||
_owner = content.Owner;
|
||||
_corpseName = content.CorpseName;
|
||||
_accessLevel = content.AccessLevel;
|
||||
_guild = content.Guild;
|
||||
_equipItems = content.EquipItems;
|
||||
_hair = content.Hair;
|
||||
_facialHair = content.FacialHair;
|
||||
}
|
||||
|
||||
// Replaced int Kills snapshot with bool Murderer snapshot
|
||||
private void MigrateFrom(V14Content content)
|
||||
{
|
||||
_restoreEquip = content.RestoreEquip;
|
||||
_flags = content.Flags;
|
||||
if (content.Kills >= 5)
|
||||
{
|
||||
_flags |= CorpseFlag.Murderer;
|
||||
}
|
||||
_timeOfDeath = content.TimeOfDeath;
|
||||
_restoreTable = content.RestoreTable;
|
||||
_decayTimer = new InternalTimer(this, content.DecayTimerDelay);
|
||||
|
|
@ -271,7 +324,6 @@ public partial class Corpse : Container, ICarvable
|
|||
_corpseName = content.CorpseName;
|
||||
_accessLevel = content.AccessLevel;
|
||||
_guild = content.Guild;
|
||||
_murderer = content.Kills >= 5;
|
||||
_equipItems = content.EquipItems;
|
||||
_hair = content.Hair;
|
||||
_facialHair = content.FacialHair;
|
||||
|
|
@ -282,6 +334,10 @@ public partial class Corpse : Container, ICarvable
|
|||
{
|
||||
_restoreEquip = content.RestoreEquip;
|
||||
_flags = content.Flags;
|
||||
if (content.Kills >= 5)
|
||||
{
|
||||
_flags |= CorpseFlag.Murderer;
|
||||
}
|
||||
_timeOfDeath = content.TimeOfDeath;
|
||||
_restoreTable = content.RestoreTable;
|
||||
_decayTimer = new InternalTimer(this, content.DecayTimerDelay);
|
||||
|
|
@ -293,7 +349,6 @@ public partial class Corpse : Container, ICarvable
|
|||
_corpseName = content.CorpseName;
|
||||
_accessLevel = content.AccessLevel;
|
||||
_guild = content.Guild;
|
||||
_murderer = content.Kills >= 5;
|
||||
_equipItems = content.EquipItems;
|
||||
}
|
||||
|
||||
|
|
@ -356,6 +411,22 @@ public partial class Corpse : Container, ICarvable
|
|||
set => SetFlag(CorpseFlag.Criminal, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool Murderer
|
||||
{
|
||||
get => GetFlag(CorpseFlag.Murderer);
|
||||
set => SetFlag(CorpseFlag.Murderer, value);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool OwnerWasBaseCreature => GetFlag(CorpseFlag.OwnerWasBaseCreature);
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool OwnerWasSummoned => GetFlag(CorpseFlag.OwnerWasSummoned);
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool OwnerWasAnimatedDead => GetFlag(CorpseFlag.OwnerWasAnimatedDead);
|
||||
|
||||
public override bool DisplaysContent => false;
|
||||
|
||||
public void Carve(Mobile from, Item item)
|
||||
|
|
@ -666,7 +737,10 @@ public partial class Corpse : Container, ICarvable
|
|||
|
||||
_accessLevel = (AccessLevel)reader.ReadInt();
|
||||
reader.ReadInt(); // guild reserve
|
||||
_murderer = reader.ReadInt() >= 5;
|
||||
if (reader.ReadInt() >= 5)
|
||||
{
|
||||
_flags |= CorpseFlag.Murderer;
|
||||
}
|
||||
|
||||
_equipItems = reader.ReadEntityList<Item>();
|
||||
}
|
||||
|
|
|
|||
123
Projects/UOContent/Migrations/Server.Items.Corpse.v16.json
generated
Normal file
123
Projects/UOContent/Migrations/Server.Items.Corpse.v16.json
generated
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
{
|
||||
"version": 16,
|
||||
"type": "Server.Items.Corpse",
|
||||
"properties": [
|
||||
{
|
||||
"name": "RestoreEquip",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flags",
|
||||
"type": "Server.Items.CorpseFlag",
|
||||
"rule": "EnumMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "TimeOfDeath",
|
||||
"type": "System.DateTime",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"DeltaTime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RestoreTable",
|
||||
"type": "System.Collections.Generic.Dictionary\u003CServer.Item, Server.Point3D\u003E",
|
||||
"rule": "DictionaryMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule",
|
||||
"0",
|
||||
"Server.Point3D",
|
||||
"PrimitiveUOTypeMigrationRule",
|
||||
"1",
|
||||
"Point3D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DecayTimer",
|
||||
"type": "Server.Timer",
|
||||
"rule": "TimerMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@TimerDrift"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Looters",
|
||||
"type": "System.Collections.Generic.HashSet\u003CServer.Mobile\u003E",
|
||||
"rule": "HashSetMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Mobile",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Killer",
|
||||
"type": "Server.Mobile",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Aggressors",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Mobile",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Owner",
|
||||
"type": "Server.Mobile",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "CorpseName",
|
||||
"type": "string",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AccessLevel",
|
||||
"type": "Server.AccessLevel",
|
||||
"rule": "EnumMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Guild",
|
||||
"type": "Server.Guilds.Guild",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "EquipItems",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
"@CanBeNull"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FacialHair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
"@CanBeNull"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -297,22 +297,25 @@ namespace Server.Misc
|
|||
}
|
||||
}
|
||||
|
||||
if (target.Owner is BaseCreature creature)
|
||||
// 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 (srcFaction != null && trgFaction != null && srcFaction != trgFaction && source.Map == Faction.Facet)
|
||||
{
|
||||
return Notoriety.Enemy;
|
||||
}
|
||||
|
||||
if (CheckHouseFlag(source, creature, target.Location, target.Map))
|
||||
if (creature != null && CheckHouseFlag(source, creature, target.Location, target.Map))
|
||||
{
|
||||
return Notoriety.CanBeAttacked;
|
||||
}
|
||||
|
||||
var actual = Notoriety.CanBeAttacked;
|
||||
|
||||
if (target.Murderer || body.IsMonster && IsSummoned(creature) ||
|
||||
creature.IsAnimatedDead)
|
||||
if (target.Murderer || body.IsMonster && target.OwnerWasSummoned || target.OwnerWasAnimatedDead)
|
||||
{
|
||||
actual = Notoriety.Murderer;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue