## 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.
222 lines
6.5 KiB
C#
222 lines
6.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|