From 26f784f45d30e03a1ce0b9b1b681e5b5f37d7e1c Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 15 Jul 2023 22:42:49 -0700 Subject: [PATCH] fix: Overhauls murder system (#1419) ## MAJOR CHANGE (API BREAKING) Added a player murder system to facilitate reporting murders. This should make it easier to extend to create a bounty system or other related game content. Player murders will be saved in a folder called _PlayerMurders_. ### Motivation The motivation was two-fold, performance, and bug fixes. First, murders are one of two systems that do a pre-world-save check on _every mobile in the game_ to decay kills and set their expiring murders. This is taxing since it freezes the world and makes world saves take longer. Every mobile has ShortTermMurders even though it is a player concept. And next, 90%+ of players are not murderers but had an ever increasing MurderElapse time that was being tracked against GameTime. These properties were also serialized unnecessarily for all mobs. Second, when I tried to optimize/refactor the code, it was obvious that the system has bugs. ### Major API Changes - [X] Created a player murder system and moved `ShortTermMurders`, `ShortTermElapse`, and `LongTermElapse` to the system. - [X] Added convenience property `PlayerMobile.ShortTermMurders`. - [X] Added convenience properties `PlayerMobile.ShortTermMurderExpiration` and `PlayerMobile.LongTermMurderExpiration` - [X] Moved ReportMurdererGump.cs - [X] Adds an `EventSink.PlayerDeleted` event. ### Notes The system currently does not support NPCs. To support expiring murders on NPCs I highly recommend a different architecture for large servers (500k+ mobs including players). Specifically switching from looping through all MurderContext to a time-order link list. --- Projects/Server/Events/EventSink.cs | 3 + Projects/Server/Mobiles/Mobile.Migrations.cs | 12 + Projects/Server/Mobiles/Mobile.cs | 59 +---- .../UOContent/Accounting/AccountHandler.cs | 2 + .../Player Murder System/MurderContext.cs | 107 +++++++++ .../PlayerMurderSystem.cs | 222 ++++++++++++++++++ .../ReportMurdererGump.cs | 209 +++++++++++++++++ Projects/UOContent/Gumps/ReportMurderer.cs | 200 ---------------- Projects/UOContent/Gumps/ResurrectGump.cs | 54 ++--- ...s.PlayerMurderSystem.MurderContext.v0.json | 24 ++ Projects/UOContent/Misc/Keywords.cs | 20 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 71 +++--- version.json | 2 +- 13 files changed, 650 insertions(+), 335 deletions(-) create mode 100644 Projects/UOContent/Engines/Player Murder System/MurderContext.cs create mode 100644 Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs create mode 100644 Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs delete mode 100644 Projects/UOContent/Gumps/ReportMurderer.cs create mode 100644 Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v0.json diff --git a/Projects/Server/Events/EventSink.cs b/Projects/Server/Events/EventSink.cs index 37d9c7c25..3ffdcbd4d 100644 --- a/Projects/Server/Events/EventSink.cs +++ b/Projects/Server/Events/EventSink.cs @@ -109,6 +109,9 @@ public static partial class EventSink public static event Action DeleteRequest; public static void InvokeDeleteRequest(NetState state, int index) => DeleteRequest?.Invoke(state, index); + public static event Action PlayerDeleted; + public static void InvokePlayerDeleted(Mobile m) => PlayerDeleted?.Invoke(m); + public static event Action ServerStarted; public static void InvokeServerStarted() => ServerStarted?.Invoke(); diff --git a/Projects/Server/Mobiles/Mobile.Migrations.cs b/Projects/Server/Mobiles/Mobile.Migrations.cs index 849060e97..7087c399c 100644 --- a/Projects/Server/Mobiles/Mobile.Migrations.cs +++ b/Projects/Server/Mobiles/Mobile.Migrations.cs @@ -15,4 +15,16 @@ public partial class Mobile StableMigrations[m] = stabled; } } + + // Migrating murders to the murder system + public static Dictionary MurderMigrations { get; private set; } + + public static void AddToMurderMigrations(Mobile m, int shortTermMurders) + { + if (shortTermMurders > 0) + { + MurderMigrations ??= new Dictionary(); + MurderMigrations[m] = shortTermMurders; + } + } } diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index da5fa449f..8827a6fdf 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -281,7 +281,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro private int m_Hunger; private bool m_InDeltaQueue; - private int m_Kills, m_ShortTermMurders; + private int m_Kills; private string m_Language; private int m_LightLevel; private Point3D m_Location; @@ -1626,19 +1626,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro } } - [CommandProperty(AccessLevel.GameMaster)] - public int ShortTermMurders - { - get => m_ShortTermMurders; - set - { - if (m_ShortTermMurders != value) - { - m_ShortTermMurders = Math.Max(value, 0); - } - } - } - [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public virtual bool Criminal { @@ -2277,7 +2264,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro public virtual void Serialize(IGenericWriter writer) { - writer.Write(34); // version + writer.Write(35); // version writer.WriteDeltaTime(LastStrGain); writer.WriteDeltaTime(LastIntGain); @@ -2313,18 +2300,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro writer.Write(Corpse); - // writer.Write(CreationTime); - - // if (Stabled == null) - // { - // writer.Write(0); - // } - // else - // { - // Stabled.Tidy(); - // writer.Write(Stabled); - // } - writer.Write(CantWalk); VirtueInfo.Serialize(writer, Virtues); @@ -2332,11 +2307,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro writer.Write(Thirst); writer.Write(BAC); - writer.Write(m_ShortTermMurders); - // writer.Write( m_ShortTermElapse ); - // writer.Write( m_LongTermElapse ); - - // writer.Write( m_Followers ); writer.Write(m_FollowersMax); writer.Write(MagicDamageAbsorb); @@ -6074,21 +6044,10 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro switch (version) { - case 34: - { - // Moved Stabled to PlayerMobile - goto case 33; - } - case 33: - { - // Removed created - goto case 32; - } - case 32: - { - // Removed StuckMenu - goto case 31; - } + case 35: // Moved short term murders to PlayerMurderSystem + case 34: // Moved Stabled to PlayerMobile + case 33: // Removed created + case 32: // Removed StuckMenu case 31: { LastStrGain = reader.ReadDeltaTime(); @@ -6183,7 +6142,11 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro } case 16: { - m_ShortTermMurders = reader.ReadInt(); + if (version < 35) + { + // Migrated to PlayerMurderSystem + AddToMurderMigrations(this, reader.ReadInt()); + } if (version <= 24) { diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index f93022830..e86a9af79 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -255,6 +255,8 @@ public static class AccountHandler acct.Comments.Add(new AccountComment("System", $"Character #{index + 1} {m} deleted by {state}")); m.Delete(); + + EventSink.InvokePlayerDeleted(m); state.SendCharacterListUpdate(acct); return; } diff --git a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs new file mode 100644 index 000000000..0a912de55 --- /dev/null +++ b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using ModernUO.Serialization; +using Server.Mobiles; + +namespace Server.Engines.PlayerMurderSystem; + +[SerializationGenerator(0)] +public partial class MurderContext +{ + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private TimeSpan _shortTermElapse = TimeSpan.MaxValue; + + [SerializableField(1)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private TimeSpan _longTermElapse = TimeSpan.MaxValue; + + [SerializableProperty(2)] + [CommandProperty(AccessLevel.GameMaster)] + public int ShortTermMurders + { + get => _shortTermMurders; + set => _shortTermMurders = Math.Max(value, 0); + } + + [DirtyTrackingEntity] + public PlayerMobile _player; + + public PlayerMobile Player => _player; + + // Wall clock time for next short or long term expiration + internal DateTime _nextElapse; + + public MurderContext(PlayerMobile player) => _player = player; + + public void ResetKillTime(bool isShort = true, bool isLong = true) + { + var gameTime = _player.GameTime; + + if (isShort) + { + ShortTermElapse = gameTime + PlayerMurderSystem.ShortTermMurderDuration; + } + + if (isLong) + { + LongTermElapse = gameTime + PlayerMurderSystem.LongTermMurderDuration; + } + } + + public void DecayKills() + { + var gameTime = _player.GameTime; + + if (ShortTermElapse < gameTime) + { + ShortTermElapse += PlayerMurderSystem.ShortTermMurderDuration; + if (ShortTermMurders > 0) + { + --ShortTermMurders; + } + } + + if (LongTermElapse < gameTime) + { + LongTermElapse += PlayerMurderSystem.LongTermMurderDuration; + if (_player.Kills > 0) + { + --_player.Kills; + } + } + } + + public bool CheckStart() + { + _nextElapse = DateTime.MaxValue; + + var now = Core.Now; + var gameTime = _player.GameTime; + + if (ShortTermMurders > 0) + { + _nextElapse = now + (ShortTermElapse - gameTime); + } + + if (_player.Kills > 0) + { + var timeUntilLong = now + (LongTermElapse - gameTime); + if (_nextElapse > timeUntilLong) + { + _nextElapse = timeUntilLong; + } + } + + return _nextElapse != DateTime.MaxValue; + } + + public class EqualityComparer : IEqualityComparer + { + public static EqualityComparer Default { get; } = new (); + + public bool Equals(MurderContext x, MurderContext y) => x?._player == y?._player; + + public int GetHashCode(MurderContext context) => context._player?.GetHashCode() ?? 0; + } +} diff --git a/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs new file mode 100644 index 000000000..d02dbc844 --- /dev/null +++ b/Projects/UOContent/Engines/Player Murder System/PlayerMurderSystem.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Server.Collections; +using Server.Logging; +using Server.Mobiles; + +namespace Server.Engines.PlayerMurderSystem; + +public static class PlayerMurderSystem +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(PlayerMurderSystem)); + + // All of the players with murders + private static readonly Dictionary _murderContexts = new(); + + // Only the players that are online + private static readonly HashSet _contextTerms = new(MurderContext.EqualityComparer.Default); + + private static readonly Timer _murdererTimer = new MurdererTimer(); + + private static TimeSpan _shortTermMurderDuration; + + private static TimeSpan _longTermMurderDuration; + + public static TimeSpan ShortTermMurderDuration => _shortTermMurderDuration; + + public static TimeSpan LongTermMurderDuration => _longTermMurderDuration; + + public static void Configure() + { + GenericPersistence.Register("PlayerMurders", Serialize, Deserialize); + + _shortTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.shortTermMurderDuration", TimeSpan.FromHours(8)); + _longTermMurderDuration = ServerConfiguration.GetOrUpdateSetting("murderSystem.longTermMurderDuration", TimeSpan.FromHours(40)); + } + + public static void Initialize() + { + EventSink.Disconnected += OnDisconnected; + EventSink.Login += OnLogin; + EventSink.PlayerDeleted += OnPlayerDeleted; + + _murdererTimer.Start(); + } + + private static void OnPlayerDeleted(Mobile m) + { + if (m is PlayerMobile pm && _murderContexts.Remove(pm, out var context)) + { + _contextTerms.Remove(context); + } + } + + // Only used for migrations! + public static void MigrateContext(PlayerMobile player, TimeSpan shortTerm, TimeSpan longTerm) + { + if (!World.Loading) + { + logger.Error( + $"Attempted to call MigrateContext outside of world loading.{Environment.NewLine}{{StackTrace}}", + new StackTrace() + ); + return; + } + + if (GetOrCreateContext(player, out var context)) + { + // We make a big assumption that by the time this is called, the Mobile/PlayerMobile info is deserialized + if (Mobile.MurderMigrations?.TryGetValue(player, out var shortTermMurders) == true) + { + context.ShortTermMurders = shortTermMurders; + } + + context.ShortTermElapse = shortTerm; + context.LongTermElapse = longTerm; + UpdateMurderContext(context); + } + } + + private static void OnLogin(Mobile m) + { + if (m is not PlayerMobile pm || !GetContext(pm, out var context)) + { + return; + } + + if (context.CheckStart()) + { + _contextTerms.Add(context); + } + else + { + _murderContexts.Remove(pm); + _contextTerms.Remove(context); + } + } + + private static void OnDisconnected(Mobile m) + { + if (m is PlayerMobile pm && _murderContexts.Remove(pm, out var context)) + { + _contextTerms.Remove(context); + } + } + + private static void Deserialize(IGenericReader reader) + { + var version = reader.ReadEncodedInt(); + + var count = reader.ReadEncodedInt(); + for (var i = 0; i < count; ++i) + { + var context = new MurderContext(reader.ReadEntity()); + context.Deserialize(reader); + + _murderContexts.Add(context.Player, context); + } + } + + private static void Serialize(IGenericWriter writer) + { + writer.WriteEncodedInt(0); // version + + writer.WriteEncodedInt(_murderContexts.Count); + foreach (var (m, context) in _murderContexts) + { + writer.Write(m); + context.Serialize(writer); + } + } + + public static bool GetContext(PlayerMobile player, out MurderContext context) => + _murderContexts.TryGetValue(player, out context); + + public static bool GetOrCreateContext(PlayerMobile player, out MurderContext context) + { + if (!_murderContexts.TryGetValue(player, out context)) + { + context = _murderContexts[player] = new MurderContext(player); + } + + return true; + } + + public static void ManuallySetShortTermMurders(PlayerMobile player, int shortTermMurders, bool resetKillTime = true) + { + if (GetOrCreateContext(player, out var context)) + { + context.ShortTermMurders = shortTermMurders; + UpdateMurderContext(context, resetKillTime); + } + } + + public static void OnPlayerMurder(PlayerMobile player, bool resetKillTime = false) + { + if (GetOrCreateContext(player, out var context)) + { + context.ShortTermMurders++; + player.Kills++; + + UpdateMurderContext(context, resetKillTime); + } + } + + private static void UpdateMurderContext(MurderContext context, bool resetKillTime = false) + { + var player = context.Player; + // Either we are resetting their decay time, or they got their first kill + context.ResetKillTime( + context.ShortTermMurders > 0 && (!resetKillTime || context.ShortTermElapse == TimeSpan.MaxValue), + player.Kills > 0 && (!resetKillTime || context.LongTermElapse == TimeSpan.MaxValue) + ); + + if (context.CheckStart()) + { + if (player.NetState != null) + { + _contextTerms.Add(context); + } + } + else + { + _murderContexts.Remove(player); + _contextTerms.Remove(context); + } + } + + private class MurdererTimer : Timer + { + public MurdererTimer() : base(TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.0)) + { + } + + protected override void OnTick() + { + if (_contextTerms.Count == 0) + { + return; + } + + using var queue = PooledRefQueue.Create(); + + foreach (var context in _contextTerms) + { + context.DecayKills(); + if (!context.CheckStart()) + { + queue.Enqueue(context.Player); + } + } + + while (queue.Count > 0) + { + if (_murderContexts.Remove((PlayerMobile)queue.Dequeue(), out var context)) + { + _contextTerms.Remove(context); + } + } + } + } +} diff --git a/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs b/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs new file mode 100644 index 000000000..88f7e5524 --- /dev/null +++ b/Projects/UOContent/Engines/Player Murder System/ReportMurdererGump.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using Server.Gumps; +using Server.Misc; +using Server.Mobiles; +using Server.Network; +using Server.SkillHandlers; + +namespace Server.Engines.PlayerMurderSystem; + +public class ReportMurdererGump : Gump +{ + // Recently reported + private static TimeSpan _recentlyReportedDelay; + private static readonly HashSet<(Mobile, Mobile)> _recentlyReported = new(); + + private readonly List _killers; + private int _idx; + + private ReportMurdererGump(List killers, int idx = 0) : base(0, 0) + { + _killers = killers; + _idx = idx; + BuildGump(); + } + + public static void Initialize() + { + _recentlyReportedDelay = ServerConfiguration.GetOrUpdateSetting("murderSystem.recentlyReportedDelay", TimeSpan.FromMinutes(10)); + EventSink.PlayerDeath += OnPlayerDeath; + } + + public static void OnPlayerDeath(Mobile m) + { + List killers = null; + HashSet toGive = null; + + // Guards won't take reports of the death of a thief! + bool notInThievesGuild = m is not PlayerMobile { NpcGuild: NpcGuild.ThievesGuild }; + + foreach (var ai in m.Aggressors) + { + if (ai.Attacker.Player && ai.CanReportMurder && !ai.Reported) + { + if (!Core.SE || !_recentlyReported.Contains((m, ai.Attacker))) + { + if (notInThievesGuild) + { + killers ??= new List(); + killers.Add(ai.Attacker); + } + + ai.Reported = true; + ai.CanReportMurder = false; + } + } + + if (ai.Attacker.Player && Core.Now - ai.LastCombatTime < TimeSpan.FromSeconds(30.0)) + { + toGive ??= new HashSet(); + toGive.Add(ai.Attacker); + } + } + + foreach (var ai in m.Aggressed) + { + if (ai.Defender.Player && Core.Now - ai.LastCombatTime < TimeSpan.FromSeconds(30.0)) + { + toGive ??= new HashSet(); + toGive.Add(ai.Defender); + } + } + + if (toGive?.Count > 0) + { + foreach (var g in toGive) + { + var n = Notoriety.Compute(g, m); + + var ourKarma = g.Karma; + var innocent = n == Notoriety.Innocent; + var criminal = n is Notoriety.Criminal or Notoriety.Murderer; + + var fameAward = m.Fame / 200; + var karmaAward = 0; + + if (innocent) + { + karmaAward = ourKarma > -2500 ? -850 : -110 - m.Karma / 100; + } + else if (criminal) + { + karmaAward = 50; + } + + Titles.AwardFame(g, fameAward, false); + Titles.AwardKarma(g, karmaAward, true); + } + } + + if (notInThievesGuild && killers?.Count > 0) + { + new GumpTimer(m, killers).Start(); + } + } + + private void BuildGump() + { + AddBackground(265, 205, 320, 290, 5054); + Closable = false; + Resizable = false; + + AddPage(0); + + AddImageTiled(225, 175, 50, 45, 0xCE); // Top left corner + AddImageTiled(267, 175, 315, 44, 0xC9); // Top bar + AddImageTiled(582, 175, 43, 45, 0xCF); // Top right corner + AddImageTiled(225, 219, 44, 270, 0xCA); // Left side + AddImageTiled(582, 219, 44, 270, 0xCB); // Right side + AddImageTiled(225, 489, 44, 43, 0xCC); // Lower left corner + AddImageTiled(267, 489, 315, 43, 0xE9); // Lower Bar + AddImageTiled(582, 489, 43, 43, 0xCD); // Lower right corner + + AddPage(1); + + AddHtml(260, 234, 300, 140, _killers[_idx].Name); // Player's Name + AddHtmlLocalized(260, 254, 300, 140, 1049066); // Would you like to report... + + AddButton(260, 300, 0xFA5, 0xFA7, 1); + AddHtmlLocalized(300, 300, 300, 50, 1046362); // Yes + + AddButton(360, 300, 0xFA5, 0xFA7, 2); + AddHtmlLocalized(400, 300, 300, 50, 1046363); // No + } + + public override void OnResponse(NetState state, RelayInfo info) + { + var from = (PlayerMobile)state.Mobile; + + switch (info.ButtonID) + { + case 1: + { + var killer = _killers[_idx]; + if (killer?.Deleted == false) + { + if (Core.SE) + { + if (_recentlyReported.Add((from, killer))) + { + Timer.DelayCall( + _recentlyReportedDelay, + static (f, k) => _recentlyReported.Remove((f, k)), + from, + killer + ); + } + } + + if (killer is PlayerMobile pk) + { + // Increment their short term murders, their kills, and reset the murder decay time + PlayerMurderSystem.OnPlayerMurder(pk, true); + + pk.SendLocalizedMessage(1049067); // You have been reported for murder! + + if (pk.Kills == 5) + { + pk.SendLocalizedMessage(502134); // You are now known as a murderer! + } + else if (Stealing.SuspendOnMurder && pk.Kills == 1 && pk.NpcGuild == NpcGuild.ThievesGuild) + { + pk.SendLocalizedMessage(501562); // You have been suspended by the Thieves Guild. + } + } + } + + break; + } + case 2: + { + break; + } + } + + _idx++; + if (_idx < _killers.Count) + { + from.SendGump(new ReportMurdererGump(_killers, _idx)); + } + } + + private class GumpTimer : Timer + { + private readonly List _killers; + private readonly Mobile _victim; + + public GumpTimer(Mobile victim, List killers) : base(TimeSpan.FromSeconds(4.0)) + { + _victim = victim; + _killers = killers; + } + + protected override void OnTick() + { + _victim.SendGump(new ReportMurdererGump(_killers)); + } + } +} diff --git a/Projects/UOContent/Gumps/ReportMurderer.cs b/Projects/UOContent/Gumps/ReportMurderer.cs deleted file mode 100644 index 6394a76bd..000000000 --- a/Projects/UOContent/Gumps/ReportMurderer.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.Collections.Generic; -using Server.Misc; -using Server.Mobiles; -using Server.Network; -using Server.SkillHandlers; - -namespace Server.Gumps -{ - public class ReportMurdererGump : Gump - { - private readonly List m_Killers; - private int m_Idx; - - private ReportMurdererGump(List killers, int idx = 0) : base(0, 0) - { - m_Killers = killers; - m_Idx = idx; - BuildGump(); - } - - public static void Initialize() - { - EventSink.PlayerDeath += EventSink_PlayerDeath; - } - - public static void EventSink_PlayerDeath(Mobile m) - { - var killers = new List(); - var toGive = new List(); - - foreach (var ai in m.Aggressors) - { - if (ai.Attacker.Player && ai.CanReportMurder && !ai.Reported) - { - if (!Core.SE || !((PlayerMobile)m).RecentlyReported.Contains(ai.Attacker)) - { - killers.Add(ai.Attacker); - ai.Reported = true; - ai.CanReportMurder = false; - } - } - - if (ai.Attacker.Player && Core.Now - ai.LastCombatTime < TimeSpan.FromSeconds(30.0) && - !toGive.Contains(ai.Attacker)) - { - toGive.Add(ai.Attacker); - } - } - - foreach (var ai in m.Aggressed) - { - if (ai.Defender.Player && Core.Now - ai.LastCombatTime < TimeSpan.FromSeconds(30.0) && - !toGive.Contains(ai.Defender)) - { - toGive.Add(ai.Defender); - } - } - - foreach (var g in toGive) - { - var n = Notoriety.Compute(g, m); - - var ourKarma = g.Karma; - var innocent = n == Notoriety.Innocent; - var criminal = n is Notoriety.Criminal or Notoriety.Murderer; - - var fameAward = m.Fame / 200; - var karmaAward = 0; - - if (innocent) - { - karmaAward = ourKarma > -2500 ? -850 : -110 - m.Karma / 100; - } - else if (criminal) - { - karmaAward = 50; - } - - Titles.AwardFame(g, fameAward, false); - Titles.AwardKarma(g, karmaAward, true); - } - - if (m is PlayerMobile mobile && mobile.NpcGuild == NpcGuild.ThievesGuild) - { - return; - } - - if (killers.Count > 0) - { - new GumpTimer(m, killers).Start(); - } - } - - private void BuildGump() - { - AddBackground(265, 205, 320, 290, 5054); - Closable = false; - Resizable = false; - - AddPage(0); - - AddImageTiled(225, 175, 50, 45, 0xCE); // Top left corner - AddImageTiled(267, 175, 315, 44, 0xC9); // Top bar - AddImageTiled(582, 175, 43, 45, 0xCF); // Top right corner - AddImageTiled(225, 219, 44, 270, 0xCA); // Left side - AddImageTiled(582, 219, 44, 270, 0xCB); // Right side - AddImageTiled(225, 489, 44, 43, 0xCC); // Lower left corner - AddImageTiled(267, 489, 315, 43, 0xE9); // Lower Bar - AddImageTiled(582, 489, 43, 43, 0xCD); // Lower right corner - - AddPage(1); - - AddHtml(260, 234, 300, 140, m_Killers[m_Idx].Name); // Player's Name - AddHtmlLocalized(260, 254, 300, 140, 1049066); // Would you like to report... - - AddButton(260, 300, 0xFA5, 0xFA7, 1); - AddHtmlLocalized(300, 300, 300, 50, 1046362); // Yes - - AddButton(360, 300, 0xFA5, 0xFA7, 2); - AddHtmlLocalized(400, 300, 300, 50, 1046363); // No - } - - public static void ReportedListExpiry_Callback(PlayerMobile from, Mobile killer) - { - if (from.RecentlyReported.Contains(killer)) - { - from.RecentlyReported.Remove(killer); - } - } - - public override void OnResponse(NetState state, RelayInfo info) - { - var from = (PlayerMobile)state.Mobile; - - switch (info.ButtonID) - { - case 1: - { - var killer = m_Killers[m_Idx]; - if (killer?.Deleted == false) - { - killer.Kills++; - killer.ShortTermMurders++; - - if (Core.SE) - { - from.RecentlyReported.Add(killer); - Timer.StartTimer(TimeSpan.FromMinutes(10), () => ReportedListExpiry_Callback(from, killer)); - } - - if (killer is PlayerMobile pk) - { - pk.ResetKillTime(); - pk.SendLocalizedMessage(1049067); // You have been reported for murder! - - if (pk.Kills == 5) - { - pk.SendLocalizedMessage(502134); // You are now known as a murderer! - } - else if (Stealing.SuspendOnMurder && pk.Kills == 1 && pk.NpcGuild == NpcGuild.ThievesGuild) - { - pk.SendLocalizedMessage(501562); // You have been suspended by the Thieves Guild. - } - } - } - - break; - } - case 2: - { - break; - } - } - - m_Idx++; - if (m_Idx < m_Killers.Count) - { - from.SendGump(new ReportMurdererGump( m_Killers, m_Idx)); - } - } - - private class GumpTimer : Timer - { - private readonly List m_Killers; - private readonly Mobile m_Victim; - - public GumpTimer(Mobile victim, List killers) : base(TimeSpan.FromSeconds(4.0)) - { - m_Victim = victim; - m_Killers = killers; - } - - protected override void OnTick() - { - m_Victim.SendGump(new ReportMurdererGump(m_Killers)); - } - } - } -} diff --git a/Projects/UOContent/Gumps/ResurrectGump.cs b/Projects/UOContent/Gumps/ResurrectGump.cs index 7a659e44b..1a5cecb71 100644 --- a/Projects/UOContent/Gumps/ResurrectGump.cs +++ b/Projects/UOContent/Gumps/ResurrectGump.cs @@ -95,23 +95,11 @@ namespace Server.Gumps AddRadio(30, 175, 9727, 9730, false, 0); AddHtmlLocalized(65, 178, 300, 25, 1060016, 0x7FFF); // I'd rather stay dead, you scoundrel!!! - AddHtmlLocalized( - 30, - 20, - 360, - 35, - 1060017, - 0x7FFF - ); // Wishing to rejoin the living, are you? I can restore your body... for a price of course... + // Wishing to rejoin the living, are you? I can restore your body... for a price of course... + AddHtmlLocalized(30, 20, 360, 35, 1060017, 0x7FFF); - AddHtmlLocalized( - 30, - 105, - 345, - 40, - 1060018, - 0x5B2D - ); // Do you accept the fee, which will be withdrawn from your bank? + // Do you accept the fee, which will be withdrawn from your bank? + AddHtmlLocalized(30, 105, 345, 40, 1060018, 0x5B2D); AddImage(65, 72, 5605); @@ -156,20 +144,16 @@ namespace Server.Gumps { if (Banker.Withdraw(from, m_Price)) { - from.SendLocalizedMessage( - 1060398, - m_Price.ToString() - ); // ~1_AMOUNT~ gold has been withdrawn from your bank box. - from.SendLocalizedMessage( - 1060022, - Banker.GetBalance(from).ToString() - ); // You have ~1_AMOUNT~ gold in cash remaining in your bank box. + // ~1_AMOUNT~ gold has been withdrawn from your bank box. + from.SendLocalizedMessage(1060398, m_Price.ToString()); + + // You have ~1_AMOUNT~ gold in cash remaining in your bank box. + from.SendLocalizedMessage(1060022, Banker.GetBalance(from).ToString()); } else { - from.SendLocalizedMessage( - 1060020 - ); // Unfortunately, you do not have enough cash in your bank to cover the cost of the healing. + // Unfortunately, you do not have enough cash in your bank to cover the cost of the healing. + from.SendLocalizedMessage(1060020); return; } } @@ -198,12 +182,14 @@ namespace Server.Gumps }; } - if (m_FromSacrifice && from is PlayerMobile mobile) - { - mobile.AvailableResurrects -= 1; + var player = from as PlayerMobile; - var pack = mobile.Backpack; - var corpse = mobile.Corpse; + if (m_FromSacrifice && player != null) + { + player.AvailableResurrects -= 1; + + var pack = player.Backpack; + var corpse = player.Corpse; if (pack != null && corpse != null) { @@ -228,9 +214,9 @@ namespace Server.Gumps Titles.AwardFame(from, -amount, true); } - if (!Core.AOS && from.ShortTermMurders >= 5) + if (!Core.AOS && player?.ShortTermMurders >= 5) { - var loss = (100.0 - (4.0 + from.ShortTermMurders / 5.0)) / 100.0; // 5 to 15% loss + var loss = (100.0 - (4.0 + player.ShortTermMurders / 5.0)) / 100.0; // 5 to 15% loss if (loss < 0.85) { diff --git a/Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v0.json b/Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v0.json new file mode 100644 index 000000000..3ed9220c1 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v0.json @@ -0,0 +1,24 @@ +{ + "version": 0, + "type": "Server.Engines.PlayerMurderSystem.MurderContext", + "properties": [ + { + "name": "ShortTermElapse", + "type": "System.TimeSpan", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "LongTermElapse", + "type": "System.TimeSpan", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "ShortTermMurders", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Misc/Keywords.cs b/Projects/UOContent/Misc/Keywords.cs index fa1ceedf9..541cf9e0c 100644 --- a/Projects/UOContent/Misc/Keywords.cs +++ b/Projects/UOContent/Misc/Keywords.cs @@ -29,17 +29,17 @@ namespace Server.Misc } case 0x0032: // *i must consider my sins* { - if (!Core.SE) + if (from is PlayerMobile player) { - from.SendMessage($"Short Term Murders : {from.ShortTermMurders}"); - from.SendMessage($"Long Term Murders : {from.Kills}"); - } - else - { - from.SendMessage( - 0x3B2, - $"Short Term Murders: {from.ShortTermMurders} Long Term Murders: {from.Kills}" - ); + if (!Core.SE) + { + from.SendMessage($"Short Term Murders : {player.ShortTermMurders}"); + from.SendMessage($"Long Term Murders : {from.Kills}"); + } + else + { + from.SendLocalizedMessage(1114370, $"{player.ShortTermMurders}\t{from.Kills}"); + } } break; diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 7660a98d7..095fb4850 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -10,6 +10,7 @@ using Server.Engines.Help; using Server.Engines.MLQuests; using Server.Engines.MLQuests.Gumps; using Server.Engines.PartySystem; +using Server.Engines.PlayerMurderSystem; using Server.Engines.Quests; using Server.Ethics; using Server.Factions; @@ -175,7 +176,6 @@ namespace Server.Mobiles private DateTime m_LastYoungHeal = DateTime.MinValue; private DateTime m_LastYoungMessage = DateTime.MinValue; - private TimeSpan m_LongTermElapse; private MountBlock _mountBlock; @@ -191,7 +191,6 @@ namespace Server.Mobiles private int m_NonAutoreinsuredItems; private DateTime m_SavagePaintExpiration; - private TimeSpan m_ShortTermElapse; private DateTime[] m_StuckMenuUses; @@ -201,13 +200,10 @@ namespace Server.Mobiles { VisibilityList = new List(); PermaFlags = new List(); - RecentlyReported = new List(); BOBFilter = new BOBFilter(); m_GameTime = TimeSpan.Zero; - m_ShortTermElapse = TimeSpan.FromHours(8.0); - m_LongTermElapse = TimeSpan.FromHours(40.0); JusticeProtectors = new List(); m_GuildRank = RankDefinition.Lowest; @@ -730,6 +726,23 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public ChampionTitleInfo ChampionTitles { get; private set; } + [CommandProperty(AccessLevel.GameMaster)] + public int ShortTermMurders + { + get => PlayerMurderSystem.GetOrCreateContext(this, out var context) ? context.ShortTermMurders : 0; + set => PlayerMurderSystem.ManuallySetShortTermMurders(this, value); + } + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime ShortTermMurderExpiration => PlayerMurderSystem.GetOrCreateContext(this, out var context) + ? Core.Now + (context.ShortTermElapse - GameTime) + : DateTime.MinValue; + + [CommandProperty(AccessLevel.GameMaster)] + public DateTime LongTermMurderExpiration => PlayerMurderSystem.GetOrCreateContext(this, out var context) + ? Core.Now + (context.LongTermElapse - GameTime) + : DateTime.MinValue; + [CommandProperty(AccessLevel.GameMaster)] public int KnownRecipes => m_AcquiredRecipes?.Count ?? 0; @@ -2885,6 +2898,7 @@ namespace Server.Mobiles switch (version) { + case 31: // Removed Short/Long Term Elapse case 30: { Stabled = reader.ReadEntitySet(true); @@ -3132,8 +3146,14 @@ namespace Server.Mobiles } case 1: { - m_LongTermElapse = reader.ReadTimeSpan(); - m_ShortTermElapse = reader.ReadTimeSpan(); + if (version < 31) + { + var longTermElapse = reader.ReadTimeSpan(); + var shortTermElapse = reader.ReadTimeSpan(); + + PlayerMurderSystem.MigrateContext(this, shortTermElapse, longTermElapse); + } + m_GameTime = reader.ReadTimeSpan(); goto case 0; } @@ -3181,7 +3201,6 @@ namespace Server.Mobiles } } - CheckKillDecay(); CheckAtrophies(); if (Hidden) // Hiding is the only buff where it has an effect that's serialized. @@ -3194,7 +3213,7 @@ namespace Server.Mobiles { base.Serialize(writer); - writer.Write(30); // version + writer.Write(31); // version if (Stabled == null) { @@ -3332,19 +3351,16 @@ namespace Server.Mobiles writer.Write((int)Flags); - writer.Write(m_LongTermElapse); - writer.Write(m_ShortTermElapse); writer.Write(GameTime); } // Do we need to run an after serialize? - public override bool ShouldExecuteAfterSerialize => ShouldKillDecay() || ShouldAtrophy(); + public override bool ShouldExecuteAfterSerialize => ShouldAtrophy(); public override void AfterSerialize() { base.AfterSerialize(); - CheckKillDecay(); CheckAtrophies(); } @@ -3368,35 +3384,6 @@ namespace Server.Mobiles ChampionTitleInfo.CheckAtrophy(this); } - public bool ShouldKillDecay() => m_ShortTermElapse < GameTime || m_LongTermElapse < GameTime; - - public void CheckKillDecay() - { - if (m_ShortTermElapse < GameTime) - { - m_ShortTermElapse += TimeSpan.FromHours(8); - if (ShortTermMurders > 0) - { - --ShortTermMurders; - } - } - - if (m_LongTermElapse < GameTime) - { - m_LongTermElapse += TimeSpan.FromHours(40); - if (Kills > 0) - { - --Kills; - } - } - } - - public void ResetKillTime() - { - m_ShortTermElapse = GameTime + TimeSpan.FromHours(8); - m_LongTermElapse = GameTime + TimeSpan.FromHours(40); - } - public override bool CanSee(Mobile m) { if (m is CharacterStatue statue) diff --git a/version.json b/version.json index 72b0d928f..0ab5664cc 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.9.7" + "version": "0.9.8" }