The in-memory penalty was only cleared when its end time exactly matched the database row's, which never happens for one given in game (sub-second vs to-the-second), so it lasted until reconnect. Match by type within 5 seconds, and stop applying a penalty as soon as it's marked passed.
323 lines
No EOL
13 KiB
C#
323 lines
No EOL
13 KiB
C#
using CS2_SimpleAdminApi;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace CS2_SimpleAdmin.Managers;
|
|
|
|
public static class PlayerPenaltyManager
|
|
{
|
|
private static readonly ConcurrentDictionary<int, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>> Penalties =
|
|
new();
|
|
|
|
// zio: the reason and online-time bookkeeping for each penalty, kept beside Penalties so the
|
|
// tuple type the API exposes doesn't change.
|
|
private static readonly ConcurrentDictionary<(int Slot, PenaltyType Type, DateTime EndDateTime), (string? Reason, DateTime AddedAt, int MinutesLeft)> Details =
|
|
new();
|
|
|
|
/// <summary>
|
|
/// Adds a penalty and remembers its reason, for telling the player why they're penalized.
|
|
/// </summary>
|
|
/// <param name="minutesLeft">Minutes left when added: the duration, less any already served
|
|
/// online when TimeMode is 0.</param>
|
|
public static void AddPenalty(int slot, PenaltyType penaltyType, DateTime endDateTime, int durationInMinutes, string? reason, int? minutesLeft = null)
|
|
{
|
|
AddPenalty(slot, penaltyType, endDateTime, durationInMinutes);
|
|
Details[(slot, penaltyType, endDateTime)] = (reason, Time.ActualDateTime(), minutesLeft ?? durationInMinutes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns how long the penalty IsPenalized found has left (null if permanent) and its reason.
|
|
/// </summary>
|
|
public static (TimeSpan? Remaining, string? Reason) GetPenaltyDetails(int slot, PenaltyType penaltyType, DateTime endDateTime)
|
|
{
|
|
var penalty = GetPlayerPenalties(slot, penaltyType).FirstOrDefault(p => p.EndDateTime == endDateTime);
|
|
var hasDetails = Details.TryGetValue((slot, penaltyType, endDateTime), out var details);
|
|
if (penalty.Duration == 0)
|
|
return (null, details.Reason);
|
|
|
|
var now = Time.ActualDateTime();
|
|
// TimeMode 0 only counts time online, and the player has been online since it was added.
|
|
var remaining = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0 && hasDetails
|
|
? TimeSpan.FromMinutes(details.MinutesLeft) - (now - details.AddedAt)
|
|
: endDateTime - now;
|
|
return (remaining < TimeSpan.Zero ? TimeSpan.Zero : remaining, details.Reason);
|
|
}
|
|
|
|
private static void RemoveDetails(Func<(int Slot, PenaltyType Type, DateTime EndDateTime), bool> match)
|
|
{
|
|
foreach (var key in Details.Keys.Where(match).ToList())
|
|
Details.TryRemove(key, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a penalty for a specific player slot and penalty type.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot where the penalty should be applied.</param>
|
|
/// <param name="penaltyType">The type of penalty to apply (e.g. gag, mute, silence).</param>
|
|
/// <param name="endDateTime">The validity expiration date/time of the penalty.</param>
|
|
/// <param name="durationInMinutes">The duration of the penalty in minutes (0 for permanent).</param>
|
|
public static void AddPenalty(int slot, PenaltyType penaltyType, DateTime endDateTime, int durationInMinutes)
|
|
{
|
|
Penalties.AddOrUpdate(slot,
|
|
(_) =>
|
|
{
|
|
var dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
|
|
{
|
|
[penaltyType] = [(endDateTime, durationInMinutes, false)]
|
|
};
|
|
return dict;
|
|
},
|
|
(_, existingDict) =>
|
|
{
|
|
if (!existingDict.TryGetValue(penaltyType, out var value))
|
|
{
|
|
value = new List<(DateTime, int, bool)>();
|
|
existingDict[penaltyType] = value;
|
|
}
|
|
|
|
value.Add((endDateTime, durationInMinutes, false));
|
|
return existingDict;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether a player is currently penalized with the given penalty type.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot to check.</param>
|
|
/// <param name="penaltyType">The penalty type to check.</param>
|
|
/// <param name="endDateTime">The out-parameter returning the end datetime of the penalty if active.</param>
|
|
/// <returns>True if the player has an active penalty, false otherwise.</returns>
|
|
public static bool IsPenalized(int slot, PenaltyType penaltyType, out DateTime? endDateTime)
|
|
{
|
|
endDateTime = null;
|
|
|
|
if (!Penalties.TryGetValue(slot, out var penaltyDict) ||
|
|
!penaltyDict.TryGetValue(penaltyType, out var penaltiesList)) return false;
|
|
|
|
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
|
|
{
|
|
// zio: a penalty marked passed is over, even before RemoveExpiredPenalties drops it.
|
|
var active = penaltiesList.FirstOrDefault(p => !p.Passed || p.Duration == 0);
|
|
if (active == default) return false;
|
|
|
|
endDateTime = active.EndDateTime;
|
|
return true;
|
|
}
|
|
|
|
var now = Time.ActualDateTime();
|
|
|
|
// Check if any active penalties exist
|
|
foreach (var penalty in penaltiesList.ToList())
|
|
{
|
|
// Check if the penalty is still active
|
|
if (penalty.Duration > 0 && now >= penalty.EndDateTime)
|
|
{
|
|
penaltiesList.Remove(penalty); // Remove expired penalty
|
|
if (penaltiesList.Count == 0)
|
|
{
|
|
penaltyDict.Remove(penaltyType); // Remove penalty type if no more penalties exist
|
|
}
|
|
}
|
|
else if (penalty.Duration == 0 || now < penalty.EndDateTime)
|
|
{
|
|
// Set endDateTime to the end time of this active penalty
|
|
endDateTime = penalty.EndDateTime;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Return false if no active penalties are found
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all penalties for a player of a specific penalty type.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
/// <param name="penaltyType">The penalty type to retrieve.</param>
|
|
/// <returns>A list of penalties if found, otherwise an empty list.</returns>
|
|
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, PenaltyType penaltyType)
|
|
{
|
|
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
|
|
penaltyDict.TryGetValue(penaltyType, out var penaltiesList))
|
|
{
|
|
return penaltiesList;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all penalties for a player across multiple penalty types.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
/// <param name="penaltyType">A list of penalty types to retrieve.</param>
|
|
/// <returns>A combined list of penalties of all requested types.</returns>
|
|
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, List<PenaltyType> penaltyType)
|
|
{
|
|
List<(DateTime EndDateTime, int Duration, bool Passed)> result = [];
|
|
|
|
if (Penalties.TryGetValue(slot, out var penaltyDict))
|
|
{
|
|
foreach (var type in penaltyType)
|
|
{
|
|
if (penaltyDict.TryGetValue(type, out var penaltiesList))
|
|
{
|
|
result.AddRange(penaltiesList);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all penalties for a player across all penalty types.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
/// <returns>A dictionary with penalty types as keys and lists of penalties as values.</returns>
|
|
public static Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetAllPlayerPenalties(int slot)
|
|
{
|
|
// Check if the player has any penalties in the dictionary
|
|
return Penalties.TryGetValue(slot, out var penaltyDict) ?
|
|
// Return all penalty types and their respective penalties for the player
|
|
penaltyDict :
|
|
// If the player has no penalties, return an empty dictionary
|
|
new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a given slot has any penalties assigned.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
/// <returns>True if the player has any penalties, false otherwise.</returns>
|
|
public static bool IsSlotInPenalties(int slot)
|
|
{
|
|
return Penalties.ContainsKey(slot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all penalties assigned to a specific player slot.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
public static void RemoveAllPenalties(int slot)
|
|
{
|
|
if (Penalties.ContainsKey(slot))
|
|
{
|
|
Penalties.TryRemove(slot, out _);
|
|
}
|
|
RemoveDetails(k => k.Slot == slot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all penalties for all players.
|
|
/// </summary>
|
|
public static void RemoveAllPenalties()
|
|
{
|
|
Penalties.Clear();
|
|
Details.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all penalties of a specific type from a player.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
/// <param name="penaltyType">The penalty type to remove.</param>
|
|
public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType)
|
|
{
|
|
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
|
|
penaltyDict.ContainsKey(penaltyType))
|
|
{
|
|
penaltyDict.Remove(penaltyType);
|
|
}
|
|
RemoveDetails(k => k.Slot == slot && k.Type == penaltyType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks penalties with a specific end datetime as "passed" for a player.
|
|
/// </summary>
|
|
/// <param name="slot">The player slot.</param>
|
|
/// <param name="dateTime">The end datetime of penalties to mark as passed.</param>
|
|
public static void RemovePenaltiesByDateTime(int slot, DateTime dateTime)
|
|
{
|
|
if (!Penalties.TryGetValue(slot, out var penaltyDict)) return;
|
|
|
|
foreach (var penaltiesList in penaltyDict.Values)
|
|
{
|
|
for (var i = 0; i < penaltiesList.Count; i++)
|
|
{
|
|
if (penaltiesList[i].EndDateTime != dateTime) continue;
|
|
// Create a copy of the penalty
|
|
var penalty = penaltiesList[i];
|
|
|
|
// Update the end datetime of the copied penalty to the current datetime
|
|
penalty.Passed = true;
|
|
|
|
// Replace the original penalty with the modified one
|
|
penaltiesList[i] = penalty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// zio: marks a penalty of one type as passed when its end is within a few seconds of the
|
|
/// database's. A penalty added in game keeps the end time from the command, which has
|
|
/// sub-second precision and is taken a moment before the one the database stores (to the
|
|
/// second), so an exact match never finds it and the penalty outlives its database row.
|
|
/// </summary>
|
|
public static void RemovePenaltiesByDateTime(int slot, PenaltyType penaltyType, DateTime dateTime)
|
|
{
|
|
if (!Penalties.TryGetValue(slot, out var penaltyDict) ||
|
|
!penaltyDict.TryGetValue(penaltyType, out var penaltiesList)) return;
|
|
|
|
for (var i = 0; i < penaltiesList.Count; i++)
|
|
{
|
|
if ((penaltiesList[i].EndDateTime - dateTime).Duration() > TimeSpan.FromSeconds(5)) continue;
|
|
var penalty = penaltiesList[i];
|
|
penalty.Passed = true;
|
|
penaltiesList[i] = penalty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes or expires penalties automatically across all players based on their duration or "passed" flag.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If <c>TimeMode == 0</c>, penalties are considered passed manually and are removed if flagged as such.
|
|
/// Otherwise, expired penalties are removed based on the current datetime compared with their end time.
|
|
/// </remarks>
|
|
public static void RemoveExpiredPenalties()
|
|
{
|
|
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
|
|
{
|
|
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
|
{
|
|
// Remove expired penalties for the player
|
|
foreach (var penaltiesList in penaltyDict.Values)
|
|
{
|
|
penaltiesList.RemoveAll(p => p is { Duration: > 0, Passed: true });
|
|
}
|
|
|
|
// Remove player slot if no penalties left
|
|
if (penaltyDict.Count == 0)
|
|
{
|
|
Penalties.TryRemove(playerSlot, out _);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var now = Time.ActualDateTime();
|
|
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
|
{
|
|
foreach (var penaltiesList in penaltyDict.Values)
|
|
{
|
|
penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime);
|
|
}
|
|
|
|
if (penaltyDict.Count == 0)
|
|
{
|
|
Penalties.TryRemove(playerSlot, out _);
|
|
}
|
|
}
|
|
}
|
|
} |