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.
This commit is contained in:
parent
d65e5bb37b
commit
26f784f45d
13 changed files with 650 additions and 335 deletions
|
|
@ -109,6 +109,9 @@ public static partial class EventSink
|
||||||
public static event Action<NetState, int> DeleteRequest;
|
public static event Action<NetState, int> DeleteRequest;
|
||||||
public static void InvokeDeleteRequest(NetState state, int index) => DeleteRequest?.Invoke(state, index);
|
public static void InvokeDeleteRequest(NetState state, int index) => DeleteRequest?.Invoke(state, index);
|
||||||
|
|
||||||
|
public static event Action<Mobile> PlayerDeleted;
|
||||||
|
public static void InvokePlayerDeleted(Mobile m) => PlayerDeleted?.Invoke(m);
|
||||||
|
|
||||||
public static event Action ServerStarted;
|
public static event Action ServerStarted;
|
||||||
public static void InvokeServerStarted() => ServerStarted?.Invoke();
|
public static void InvokeServerStarted() => ServerStarted?.Invoke();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,16 @@ public partial class Mobile
|
||||||
StableMigrations[m] = stabled;
|
StableMigrations[m] = stabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrating murders to the murder system
|
||||||
|
public static Dictionary<Mobile, int> MurderMigrations { get; private set; }
|
||||||
|
|
||||||
|
public static void AddToMurderMigrations(Mobile m, int shortTermMurders)
|
||||||
|
{
|
||||||
|
if (shortTermMurders > 0)
|
||||||
|
{
|
||||||
|
MurderMigrations ??= new Dictionary<Mobile, int>();
|
||||||
|
MurderMigrations[m] = shortTermMurders;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
private int m_Hunger;
|
private int m_Hunger;
|
||||||
|
|
||||||
private bool m_InDeltaQueue;
|
private bool m_InDeltaQueue;
|
||||||
private int m_Kills, m_ShortTermMurders;
|
private int m_Kills;
|
||||||
private string m_Language;
|
private string m_Language;
|
||||||
private int m_LightLevel;
|
private int m_LightLevel;
|
||||||
private Point3D m_Location;
|
private Point3D m_Location;
|
||||||
|
|
@ -1626,19 +1626,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, 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)]
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
||||||
public virtual bool Criminal
|
public virtual bool Criminal
|
||||||
{
|
{
|
||||||
|
|
@ -2277,7 +2264,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
|
|
||||||
public virtual void Serialize(IGenericWriter writer)
|
public virtual void Serialize(IGenericWriter writer)
|
||||||
{
|
{
|
||||||
writer.Write(34); // version
|
writer.Write(35); // version
|
||||||
|
|
||||||
writer.WriteDeltaTime(LastStrGain);
|
writer.WriteDeltaTime(LastStrGain);
|
||||||
writer.WriteDeltaTime(LastIntGain);
|
writer.WriteDeltaTime(LastIntGain);
|
||||||
|
|
@ -2313,18 +2300,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
|
|
||||||
writer.Write(Corpse);
|
writer.Write(Corpse);
|
||||||
|
|
||||||
// writer.Write(CreationTime);
|
|
||||||
|
|
||||||
// if (Stabled == null)
|
|
||||||
// {
|
|
||||||
// writer.Write(0);
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// Stabled.Tidy();
|
|
||||||
// writer.Write(Stabled);
|
|
||||||
// }
|
|
||||||
|
|
||||||
writer.Write(CantWalk);
|
writer.Write(CantWalk);
|
||||||
|
|
||||||
VirtueInfo.Serialize(writer, Virtues);
|
VirtueInfo.Serialize(writer, Virtues);
|
||||||
|
|
@ -2332,11 +2307,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
writer.Write(Thirst);
|
writer.Write(Thirst);
|
||||||
writer.Write(BAC);
|
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(m_FollowersMax);
|
||||||
|
|
||||||
writer.Write(MagicDamageAbsorb);
|
writer.Write(MagicDamageAbsorb);
|
||||||
|
|
@ -6074,21 +6044,10 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
|
|
||||||
switch (version)
|
switch (version)
|
||||||
{
|
{
|
||||||
case 34:
|
case 35: // Moved short term murders to PlayerMurderSystem
|
||||||
{
|
case 34: // Moved Stabled to PlayerMobile
|
||||||
// Moved Stabled to PlayerMobile
|
case 33: // Removed created
|
||||||
goto case 33;
|
case 32: // Removed StuckMenu
|
||||||
}
|
|
||||||
case 33:
|
|
||||||
{
|
|
||||||
// Removed created
|
|
||||||
goto case 32;
|
|
||||||
}
|
|
||||||
case 32:
|
|
||||||
{
|
|
||||||
// Removed StuckMenu
|
|
||||||
goto case 31;
|
|
||||||
}
|
|
||||||
case 31:
|
case 31:
|
||||||
{
|
{
|
||||||
LastStrGain = reader.ReadDeltaTime();
|
LastStrGain = reader.ReadDeltaTime();
|
||||||
|
|
@ -6183,7 +6142,11 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
}
|
}
|
||||||
case 16:
|
case 16:
|
||||||
{
|
{
|
||||||
m_ShortTermMurders = reader.ReadInt();
|
if (version < 35)
|
||||||
|
{
|
||||||
|
// Migrated to PlayerMurderSystem
|
||||||
|
AddToMurderMigrations(this, reader.ReadInt());
|
||||||
|
}
|
||||||
|
|
||||||
if (version <= 24)
|
if (version <= 24)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,8 @@ public static class AccountHandler
|
||||||
acct.Comments.Add(new AccountComment("System", $"Character #{index + 1} {m} deleted by {state}"));
|
acct.Comments.Add(new AccountComment("System", $"Character #{index + 1} {m} deleted by {state}"));
|
||||||
|
|
||||||
m.Delete();
|
m.Delete();
|
||||||
|
|
||||||
|
EventSink.InvokePlayerDeleted(m);
|
||||||
state.SendCharacterListUpdate(acct);
|
state.SendCharacterListUpdate(acct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
Projects/UOContent/Engines/Player Murder System/MurderContext.cs
Normal file
107
Projects/UOContent/Engines/Player Murder System/MurderContext.cs
Normal file
|
|
@ -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<MurderContext>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<PlayerMobile, MurderContext> _murderContexts = new();
|
||||||
|
|
||||||
|
// Only the players that are online
|
||||||
|
private static readonly HashSet<MurderContext> _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<PlayerMobile>());
|
||||||
|
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<Mobile>.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Mobile> _killers;
|
||||||
|
private int _idx;
|
||||||
|
|
||||||
|
private ReportMurdererGump(List<Mobile> 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<Mobile> killers = null;
|
||||||
|
HashSet<Mobile> 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<Mobile>();
|
||||||
|
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<Mobile>();
|
||||||
|
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<Mobile>();
|
||||||
|
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<Mobile> _killers;
|
||||||
|
private readonly Mobile _victim;
|
||||||
|
|
||||||
|
public GumpTimer(Mobile victim, List<Mobile> killers) : base(TimeSpan.FromSeconds(4.0))
|
||||||
|
{
|
||||||
|
_victim = victim;
|
||||||
|
_killers = killers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnTick()
|
||||||
|
{
|
||||||
|
_victim.SendGump(new ReportMurdererGump(_killers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Mobile> m_Killers;
|
|
||||||
private int m_Idx;
|
|
||||||
|
|
||||||
private ReportMurdererGump(List<Mobile> 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<Mobile>();
|
|
||||||
var toGive = new List<Mobile>();
|
|
||||||
|
|
||||||
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<Mobile> m_Killers;
|
|
||||||
private readonly Mobile m_Victim;
|
|
||||||
|
|
||||||
public GumpTimer(Mobile victim, List<Mobile> killers) : base(TimeSpan.FromSeconds(4.0))
|
|
||||||
{
|
|
||||||
m_Victim = victim;
|
|
||||||
m_Killers = killers;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTick()
|
|
||||||
{
|
|
||||||
m_Victim.SendGump(new ReportMurdererGump(m_Killers));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -95,23 +95,11 @@ namespace Server.Gumps
|
||||||
AddRadio(30, 175, 9727, 9730, false, 0);
|
AddRadio(30, 175, 9727, 9730, false, 0);
|
||||||
AddHtmlLocalized(65, 178, 300, 25, 1060016, 0x7FFF); // I'd rather stay dead, you scoundrel!!!
|
AddHtmlLocalized(65, 178, 300, 25, 1060016, 0x7FFF); // I'd rather stay dead, you scoundrel!!!
|
||||||
|
|
||||||
AddHtmlLocalized(
|
// Wishing to rejoin the living, are you? I can restore your body... for a price of course...
|
||||||
30,
|
AddHtmlLocalized(30, 20, 360, 35, 1060017, 0x7FFF);
|
||||||
20,
|
|
||||||
360,
|
|
||||||
35,
|
|
||||||
1060017,
|
|
||||||
0x7FFF
|
|
||||||
); // Wishing to rejoin the living, are you? I can restore your body... for a price of course...
|
|
||||||
|
|
||||||
AddHtmlLocalized(
|
// Do you accept the fee, which will be withdrawn from your bank?
|
||||||
30,
|
AddHtmlLocalized(30, 105, 345, 40, 1060018, 0x5B2D);
|
||||||
105,
|
|
||||||
345,
|
|
||||||
40,
|
|
||||||
1060018,
|
|
||||||
0x5B2D
|
|
||||||
); // Do you accept the fee, which will be withdrawn from your bank?
|
|
||||||
|
|
||||||
AddImage(65, 72, 5605);
|
AddImage(65, 72, 5605);
|
||||||
|
|
||||||
|
|
@ -156,20 +144,16 @@ namespace Server.Gumps
|
||||||
{
|
{
|
||||||
if (Banker.Withdraw(from, m_Price))
|
if (Banker.Withdraw(from, m_Price))
|
||||||
{
|
{
|
||||||
from.SendLocalizedMessage(
|
// ~1_AMOUNT~ gold has been withdrawn from your bank box.
|
||||||
1060398,
|
from.SendLocalizedMessage(1060398, m_Price.ToString());
|
||||||
m_Price.ToString()
|
|
||||||
); // ~1_AMOUNT~ gold has been withdrawn from your bank box.
|
// You have ~1_AMOUNT~ gold in cash remaining in your bank box.
|
||||||
from.SendLocalizedMessage(
|
from.SendLocalizedMessage(1060022, Banker.GetBalance(from).ToString());
|
||||||
1060022,
|
|
||||||
Banker.GetBalance(from).ToString()
|
|
||||||
); // You have ~1_AMOUNT~ gold in cash remaining in your bank box.
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
from.SendLocalizedMessage(
|
// Unfortunately, you do not have enough cash in your bank to cover the cost of the healing.
|
||||||
1060020
|
from.SendLocalizedMessage(1060020);
|
||||||
); // Unfortunately, you do not have enough cash in your bank to cover the cost of the healing.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,12 +182,14 @@ namespace Server.Gumps
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_FromSacrifice && from is PlayerMobile mobile)
|
var player = from as PlayerMobile;
|
||||||
{
|
|
||||||
mobile.AvailableResurrects -= 1;
|
|
||||||
|
|
||||||
var pack = mobile.Backpack;
|
if (m_FromSacrifice && player != null)
|
||||||
var corpse = mobile.Corpse;
|
{
|
||||||
|
player.AvailableResurrects -= 1;
|
||||||
|
|
||||||
|
var pack = player.Backpack;
|
||||||
|
var corpse = player.Corpse;
|
||||||
|
|
||||||
if (pack != null && corpse != null)
|
if (pack != null && corpse != null)
|
||||||
{
|
{
|
||||||
|
|
@ -228,9 +214,9 @@ namespace Server.Gumps
|
||||||
Titles.AwardFame(from, -amount, true);
|
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)
|
if (loss < 0.85)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
24
Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v0.json
generated
Normal file
24
Projects/UOContent/Migrations/Server.Engines.PlayerMurderSystem.MurderContext.v0.json
generated
Normal file
|
|
@ -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": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -29,17 +29,17 @@ namespace Server.Misc
|
||||||
}
|
}
|
||||||
case 0x0032: // *i must consider my sins*
|
case 0x0032: // *i must consider my sins*
|
||||||
{
|
{
|
||||||
if (!Core.SE)
|
if (from is PlayerMobile player)
|
||||||
{
|
{
|
||||||
from.SendMessage($"Short Term Murders : {from.ShortTermMurders}");
|
if (!Core.SE)
|
||||||
from.SendMessage($"Long Term Murders : {from.Kills}");
|
{
|
||||||
}
|
from.SendMessage($"Short Term Murders : {player.ShortTermMurders}");
|
||||||
else
|
from.SendMessage($"Long Term Murders : {from.Kills}");
|
||||||
{
|
}
|
||||||
from.SendMessage(
|
else
|
||||||
0x3B2,
|
{
|
||||||
$"Short Term Murders: {from.ShortTermMurders} Long Term Murders: {from.Kills}"
|
from.SendLocalizedMessage(1114370, $"{player.ShortTermMurders}\t{from.Kills}");
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Server.Engines.Help;
|
||||||
using Server.Engines.MLQuests;
|
using Server.Engines.MLQuests;
|
||||||
using Server.Engines.MLQuests.Gumps;
|
using Server.Engines.MLQuests.Gumps;
|
||||||
using Server.Engines.PartySystem;
|
using Server.Engines.PartySystem;
|
||||||
|
using Server.Engines.PlayerMurderSystem;
|
||||||
using Server.Engines.Quests;
|
using Server.Engines.Quests;
|
||||||
using Server.Ethics;
|
using Server.Ethics;
|
||||||
using Server.Factions;
|
using Server.Factions;
|
||||||
|
|
@ -175,7 +176,6 @@ namespace Server.Mobiles
|
||||||
private DateTime m_LastYoungHeal = DateTime.MinValue;
|
private DateTime m_LastYoungHeal = DateTime.MinValue;
|
||||||
|
|
||||||
private DateTime m_LastYoungMessage = DateTime.MinValue;
|
private DateTime m_LastYoungMessage = DateTime.MinValue;
|
||||||
private TimeSpan m_LongTermElapse;
|
|
||||||
|
|
||||||
private MountBlock _mountBlock;
|
private MountBlock _mountBlock;
|
||||||
|
|
||||||
|
|
@ -191,7 +191,6 @@ namespace Server.Mobiles
|
||||||
private int m_NonAutoreinsuredItems;
|
private int m_NonAutoreinsuredItems;
|
||||||
|
|
||||||
private DateTime m_SavagePaintExpiration;
|
private DateTime m_SavagePaintExpiration;
|
||||||
private TimeSpan m_ShortTermElapse;
|
|
||||||
|
|
||||||
private DateTime[] m_StuckMenuUses;
|
private DateTime[] m_StuckMenuUses;
|
||||||
|
|
||||||
|
|
@ -201,13 +200,10 @@ namespace Server.Mobiles
|
||||||
{
|
{
|
||||||
VisibilityList = new List<Mobile>();
|
VisibilityList = new List<Mobile>();
|
||||||
PermaFlags = new List<Mobile>();
|
PermaFlags = new List<Mobile>();
|
||||||
RecentlyReported = new List<Mobile>();
|
|
||||||
|
|
||||||
BOBFilter = new BOBFilter();
|
BOBFilter = new BOBFilter();
|
||||||
|
|
||||||
m_GameTime = TimeSpan.Zero;
|
m_GameTime = TimeSpan.Zero;
|
||||||
m_ShortTermElapse = TimeSpan.FromHours(8.0);
|
|
||||||
m_LongTermElapse = TimeSpan.FromHours(40.0);
|
|
||||||
|
|
||||||
JusticeProtectors = new List<Mobile>();
|
JusticeProtectors = new List<Mobile>();
|
||||||
m_GuildRank = RankDefinition.Lowest;
|
m_GuildRank = RankDefinition.Lowest;
|
||||||
|
|
@ -730,6 +726,23 @@ namespace Server.Mobiles
|
||||||
[CommandProperty(AccessLevel.GameMaster)]
|
[CommandProperty(AccessLevel.GameMaster)]
|
||||||
public ChampionTitleInfo ChampionTitles { get; private set; }
|
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)]
|
[CommandProperty(AccessLevel.GameMaster)]
|
||||||
public int KnownRecipes => m_AcquiredRecipes?.Count ?? 0;
|
public int KnownRecipes => m_AcquiredRecipes?.Count ?? 0;
|
||||||
|
|
||||||
|
|
@ -2885,6 +2898,7 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
switch (version)
|
switch (version)
|
||||||
{
|
{
|
||||||
|
case 31: // Removed Short/Long Term Elapse
|
||||||
case 30:
|
case 30:
|
||||||
{
|
{
|
||||||
Stabled = reader.ReadEntitySet<Mobile>(true);
|
Stabled = reader.ReadEntitySet<Mobile>(true);
|
||||||
|
|
@ -3132,8 +3146,14 @@ namespace Server.Mobiles
|
||||||
}
|
}
|
||||||
case 1:
|
case 1:
|
||||||
{
|
{
|
||||||
m_LongTermElapse = reader.ReadTimeSpan();
|
if (version < 31)
|
||||||
m_ShortTermElapse = reader.ReadTimeSpan();
|
{
|
||||||
|
var longTermElapse = reader.ReadTimeSpan();
|
||||||
|
var shortTermElapse = reader.ReadTimeSpan();
|
||||||
|
|
||||||
|
PlayerMurderSystem.MigrateContext(this, shortTermElapse, longTermElapse);
|
||||||
|
}
|
||||||
|
|
||||||
m_GameTime = reader.ReadTimeSpan();
|
m_GameTime = reader.ReadTimeSpan();
|
||||||
goto case 0;
|
goto case 0;
|
||||||
}
|
}
|
||||||
|
|
@ -3181,7 +3201,6 @@ namespace Server.Mobiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckKillDecay();
|
|
||||||
CheckAtrophies();
|
CheckAtrophies();
|
||||||
|
|
||||||
if (Hidden) // Hiding is the only buff where it has an effect that's serialized.
|
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);
|
base.Serialize(writer);
|
||||||
|
|
||||||
writer.Write(30); // version
|
writer.Write(31); // version
|
||||||
|
|
||||||
if (Stabled == null)
|
if (Stabled == null)
|
||||||
{
|
{
|
||||||
|
|
@ -3332,19 +3351,16 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
writer.Write((int)Flags);
|
writer.Write((int)Flags);
|
||||||
|
|
||||||
writer.Write(m_LongTermElapse);
|
|
||||||
writer.Write(m_ShortTermElapse);
|
|
||||||
writer.Write(GameTime);
|
writer.Write(GameTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we need to run an after serialize?
|
// Do we need to run an after serialize?
|
||||||
public override bool ShouldExecuteAfterSerialize => ShouldKillDecay() || ShouldAtrophy();
|
public override bool ShouldExecuteAfterSerialize => ShouldAtrophy();
|
||||||
|
|
||||||
public override void AfterSerialize()
|
public override void AfterSerialize()
|
||||||
{
|
{
|
||||||
base.AfterSerialize();
|
base.AfterSerialize();
|
||||||
|
|
||||||
CheckKillDecay();
|
|
||||||
CheckAtrophies();
|
CheckAtrophies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3368,35 +3384,6 @@ namespace Server.Mobiles
|
||||||
ChampionTitleInfo.CheckAtrophy(this);
|
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)
|
public override bool CanSee(Mobile m)
|
||||||
{
|
{
|
||||||
if (m is CharacterStatue statue)
|
if (m is CharacterStatue statue)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
||||||
"version": "0.9.7"
|
"version": "0.9.8"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue