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" }