Files
CS2-SimpleAdmin/CS2-SimpleAdmin/Helper.cs
Dawid Bepierszcz 5701455de0 Refactor database layer and add module/plugin improvements
Reworked the database layer to support both MySQL and SQLite via new provider classes and migration scripts for each backend. Updated the build workflow to support building and packaging additional modules, including StealthModule and BanSoundModule, and improved artifact handling. Refactored command registration to allow dynamic registration/unregistration and improved API event handling. Updated dependencies, project structure, and configuration checks for better reliability and extensibility. Added new language files, updated versioning, and removed obsolete files.

**⚠️ Warning: SQLite support is currently experimental.
Using this version requires reconfiguration of your database settings!
Plugin now uses UTC time. Please adjust your configurations accordingly!
**
2025-10-03 01:37:03 +02:00

1067 lines
41 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.ValveConstants.Protobuf;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CS2_SimpleAdmin.Managers;
using MenuManager;
using ZLinq;
namespace CS2_SimpleAdmin;
internal static class Helper
{
private static readonly string AssemblyName = Assembly.GetExecutingAssembly().GetName().Name ?? "";
private static readonly string CfgPath = $"{Server.GameDirectory}/csgo/addons/counterstrikesharp/configs/plugins/{AssemblyName}/{AssemblyName}.json";
private delegate nint CNetworkSystemUpdatePublicIp(nint a1);
private static CNetworkSystemUpdatePublicIp? _networkSystemUpdatePublicIp;
public static bool IsDebugBuild
{
get
{
#if DEBUG
return true;
#else
return false;
#endif
}
}
public static List<CCSPlayerController> GetPlayerFromName(string name)
{
return GetValidPlayers().FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase));
}
public static CCSPlayerController? GetPlayerFromSteamid64(ulong steamid)
{
return GetValidPlayers().FirstOrDefault(x => x.SteamID == steamid);
}
public static List<CCSPlayerController> GetPlayerFromIp(string ipAddress)
{
return CS2_SimpleAdmin.CachedPlayers.FindAll(x => x.IpAddress != null && x.IpAddress.Split(":")[0].Equals(ipAddress));
}
public static List<CCSPlayerController> GetValidPlayers()
{
return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().ToList();
}
public static List<CCSPlayerController> GetValidPlayersWithBots()
{
return CS2_SimpleAdmin.CachedPlayers.Concat(CS2_SimpleAdmin.BotPlayers).AsValueEnumerable().ToList();
}
// public static bool IsValidSteamId64(string input)
// {
// const string pattern = @"^\d{17}$";
// return Regex.IsMatch(input, pattern);
// }
public static bool ValidateSteamId(string input, out SteamID? steamId)
{
steamId = null;
if (string.IsNullOrEmpty(input))
{
return false;
}
if (!SteamID.TryParse(input, out var parsedSteamId)) return false;
steamId = parsedSteamId;
return true;
}
public static bool IsValidIp(string input)
{
const string pattern = @"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
return Regex.IsMatch(input, pattern);
}
public static void GivePlayerFlags(SteamID? steamid, List<string>? flags = null, uint immunity = 0)
{
try
{
if (steamid == null || (flags == null && immunity == 0))
{
return;
}
if (flags == null) return;
foreach (var flag in flags.Where(flag => !string.IsNullOrEmpty(flag)))
{
if (flag.StartsWith($"@"))
{
//Console.WriteLine($"Adding permission {flag} to SteamID {steamid}");
AdminManager.AddPlayerPermissions(steamid, flag);
}
else if (flag.StartsWith($"#"))
{
//Console.WriteLine($"Adding SteamID {steamid} to group {flag}");
AdminManager.AddPlayerToGroup(steamid, flag);
}
}
AdminManager.SetPlayerImmunity(steamid, immunity);
}
catch
{
}
}
public static void KickPlayer(int userId, NetworkDisconnectionReason reason = NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED, int delay = 0)
{
var player = Utilities.GetPlayerFromUserid(userId);
if (player == null || !player.IsValid || player.IsHLTV)
return;
if (player.UserId.HasValue && CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.SteamID, out var value))
value.WaitingForKick = true;
// player.CommitSuicide(true, true);
player.VoiceFlags = VoiceFlags.Muted;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null && playerPawn.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
playerPawn.Freeze();
playerPawn.Colorize(255, 0, 0);
var weaponServices = playerPawn.WeaponServices;
if (weaponServices == null)
return;
foreach (var _weap in weaponServices.MyWeapons)
{
var weapon = _weap.Value;;
if (weapon == null || !weapon.IsValid)
continue;
if (weapon.DesignerName.Contains("c4") || weapon.DesignerName.Contains("healthshot"))
continue;
weapon.NextPrimaryAttackTick = Server.TickCount + 999;
weapon.NextSecondaryAttackTick = Server.TickCount + 999;
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick");
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick");
}
}
if (delay > 0)
{
CS2_SimpleAdmin.Instance.AddTimer(delay, () =>
{
if (!player.IsValid || player.IsHLTV)
return;
// Server.ExecuteCommand($"kickid {player.UserId}");
playerPawn?.Colorize();
player.Disconnect(reason);
});
}
else
{
// Server.ExecuteCommand($"kickid {player.UserId}");
playerPawn?.Colorize();
player.Disconnect(reason);
}
if (CS2_SimpleAdmin.UnlockedCommands && reason == NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)
Server.ExecuteCommand($"banid 1 {new SteamID(player.SteamID).SteamId3}");
// if (!string.IsNullOrEmpty(reason))
// {
// var escapeChars = reason.IndexOfAny([';', '|']);
//
// if (escapeChars != -1)
// {
// reason = reason[..escapeChars];
// }
// }
//
// Server.ExecuteCommand($"kickid {userId} {reason}");
}
public static void KickPlayer(CCSPlayerController player, NetworkDisconnectionReason reason = NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED, int delay = 0)
{
if (!player.IsValid || player.IsHLTV)
return;
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.SteamID, out var value))
{
if (value.WaitingForKick)
return;
value.WaitingForKick = true;
}
player.VoiceFlags = VoiceFlags.Muted;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null && playerPawn.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
playerPawn.Freeze();
playerPawn.Colorize(255, 0, 0);
var weaponServices = playerPawn.WeaponServices;
if (weaponServices == null)
return;
foreach (var _weap in weaponServices.MyWeapons)
{
var weapon = _weap.Value;
;
if (weapon == null || !weapon.IsValid)
continue;
if (weapon.DesignerName.Contains("c4") || weapon.DesignerName.Contains("healthshot"))
continue;
weapon.NextPrimaryAttackTick = Server.TickCount + 999;
weapon.NextSecondaryAttackTick = Server.TickCount + 999;
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick");
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick");
}
}
if (delay > 0)
{
CS2_SimpleAdmin.Instance.AddTimer(delay, () =>
{
if (!player.IsValid || player.IsHLTV)
return;
// if (!string.IsNullOrEmpty(reason))
// {
// var escapeChars = reason.IndexOfAny([';', '|']);
//
// if (escapeChars != -1)
// {
// reason = reason[..escapeChars];
// }
// }
//
// Server.ExecuteCommand($"kickid {player.UserId}");
player.Disconnect(reason);
});
}
else
{
// Server.ExecuteCommand($"kickid {player.UserId}");
player.Disconnect(reason);
}
if (CS2_SimpleAdmin.UnlockedCommands && reason == NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)
Server.ExecuteCommand($"banid 1 {new SteamID(player.SteamID).SteamId3}");
// if (!string.IsNullOrEmpty(reason))
// {
// var escapeChars = reason.IndexOfAny([';', '|']);
//
// if (escapeChars != -1)
// {
// reason = reason[..escapeChars];
// }
// }
//
// Server.ExecuteCommand($"kickid {userId} {reason}");
}
public static int ParsePenaltyTime(string time)
{
time = time.ToLower();
if (string.IsNullOrWhiteSpace(time) || !time.Any(char.IsDigit))
{
// CS2_SimpleAdmin._logger?.LogError("Time string cannot be null or empty.");
return -1;
}
if (time.Equals($"0"))
return 0;
var timeUnits = new Dictionary<string, int>
{
{ "m", 1 }, // Minute
{ "h", 60 }, // Hour
{ "d", 1440 }, // Day (24 * 60)
{ "w", 10080 }, // Week (7 * 24 * 60)
{ "mo", 43200 }, // Month (30 * 24 * 60)
{ "y", 525600 } // Year (365 * 24 * 60)
};
// Check if the input is purely numeric (e.g., "10" for 10 minutes)
if (int.TryParse(time, out var numericMinutes))
{
return numericMinutes;
}
int totalMinutes = 0;
var regex = new Regex(@"(\d+)([a-z]+)");
var matches = regex.Matches(time);
foreach (Match match in matches)
{
var value = int.Parse(match.Groups[1].Value); // Numeric part
var unit = match.Groups[2].Value; // Unit part
if (timeUnits.TryGetValue(unit, out var minutesPerUnit))
{
totalMinutes += value * minutesPerUnit;
}
else
{
throw new ArgumentException($"Invalid time unit '{unit}' in time string.", nameof(time));
}
}
return totalMinutes > 0 ? totalMinutes : -1;
}
public static void PrintToCenterAll(string message)
{
Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false }).ToList().ForEach(controller =>
{
controller.PrintToCenter(message);
});
}
internal static void HandleVotes(CCSPlayerController player, ChatMenuOption option)
{
if (!CS2_SimpleAdmin.VoteInProgress)
return;
option.Disabled = true;
CS2_SimpleAdmin.VoteAnswers[option.Text]++;
}
internal static void LogCommand(CCSPlayerController? caller, CommandInfo command)
{
if (CS2_SimpleAdmin._localizer == null)
return;
var playerName = caller?.PlayerName ?? CS2_SimpleAdmin._localizer["sa_console"];
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer["sa_unknown"];
CS2_SimpleAdmin.Instance.Logger.LogInformation($"{CS2_SimpleAdmin._localizer[
"sa_discord_log_command",
playerName, command.GetCommandString]}".Replace("HOSTNAME", hostname).Replace("**", ""));
SendDiscordLogMessage(caller, command, CS2_SimpleAdmin._localizer);
}
internal static void LogCommand(CCSPlayerController? caller, string command)
{
if (CS2_SimpleAdmin._localizer == null)
return;
var playerName = caller?.PlayerName ?? CS2_SimpleAdmin._localizer["sa_console"];
var hostnameCvar = ConVar.Find("hostname");
var hostname = hostnameCvar?.StringValue ?? CS2_SimpleAdmin._localizer["sa_unknown"];
CS2_SimpleAdmin.Instance.Logger.LogInformation($"{CS2_SimpleAdmin._localizer["sa_discord_log_command",
playerName, command]}".Replace("HOSTNAME", hostname).Replace("**", ""));
SendDiscordLogMessage(caller, command, CS2_SimpleAdmin._localizer);
}
/*public static IEnumerable<Embed> GenerateEmbedsDiscord(string title, string description, string thumbnailUrl, Color color, string[] fieldNames, string[] fieldValues, bool[] inlineFlags)
{
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var address = $"{ConVar.Find("ip")?.StringValue}:{ConVar.Find("hostport")!.GetPrimitiveValue<int>()}";
description = description.Replace("{hostname}", hostname);
description = description.Replace("{address}", address);
var embed = new EmbedBuilder
{
Title = title,
Description = description,
ThumbnailUrl = thumbnailUrl,
Color = color,
};
for (var i = 0; i < fieldNames.Length; i++)
{
fieldValues[i] = fieldValues[i].Replace("{hostname}", hostname ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown");
fieldValues[i] = fieldValues[i].Replace("{address}", address ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown");
embed.AddField(fieldNames[i], fieldValues[i], inlineFlags[i]);
if ((i + 1) % 2 == 0 && i < fieldNames.Length - 1)
{
embed.AddField("\u200b", "\u200b");
}
}
return new List<Embed> { embed.Build() };
}*/
private static void SendDiscordLogMessage(CCSPlayerController? caller, CommandInfo command, IStringLocalizer? localizer)
{
if (CS2_SimpleAdmin.DiscordWebhookClientLog == null || localizer == null) return;
var communityUrl = caller != null ? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">" : "<https://steamcommunity.com/profiles/0>";
var callerName = caller != null ? caller.PlayerName : CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console";
_ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(Helper.GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString]));
}
private static void SendDiscordLogMessage(CCSPlayerController? caller, string command, IStringLocalizer? localizer)
{
if (CS2_SimpleAdmin.DiscordWebhookClientLog == null || localizer == null) return;
var communityUrl = caller != null ? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">" : "<https://steamcommunity.com/profiles/0>";
var callerName = caller != null ? caller.PlayerName : CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console";
_ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command]));
}
public static void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs)
{
string[] publishActions = ["ban", "gag", "silence", "mute"];
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return;
if (CS2_SimpleAdmin._localizer == null) return;
if (string.IsNullOrWhiteSpace(callerName))
callerName = CS2_SimpleAdmin._localizer["sa_console"];
var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray();
if (dontPublish == false && publishActions.Any(messageKey.Contains))
{
CS2_SimpleAdmin.SimpleAdminApi?.OnAdminShowActivityEvent(messageKey, callerName, dontPublish, messageArgs);
}
// // Replace placeholder based on showActivityType
// for (var i = 0; i < formattedMessageArgs.Length; i++)
// {
// var arg = formattedMessageArgs[i]; // Convert argument to string if not null
// // Replace "CALLER" placeholder in the argument string
// formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
// {
// 1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]),
// 2 => arg.Replace("CALLER", callerName ?? "Console"),
// _ => arg
// };
// }
var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false });
if (!validPlayers.Any())
return;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3)
{
validPlayers = validPlayers.Where(c =>
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban"));
}
foreach (var controller in validPlayers.ToList())
{
var currentMessageArgs = (string[])formattedMessageArgs.Clone();
// Replace "CALLER" placeholder based on showActivityType and whether the recipient is an admin
for (var i = 0; i < currentMessageArgs.Length; i++)
{
var arg = currentMessageArgs[i];
currentMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => arg.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
_ => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
};
}
// Send the localized message to each player
controller.SendLocalizedMessage(CS2_SimpleAdmin._localizer, messageKey, currentMessageArgs.Cast<object>().ToArray());
}
}
public static void DisplayCenterMessage(
CCSPlayerController player,
string messageKey,
string? callerName = null,
params object[] messageArgs)
{
if (CS2_SimpleAdmin._localizer == null) return;
var localizedMessageKey = $"{messageKey}";
var formattedMessageArgs = messageArgs.Select(arg => arg?.ToString() ?? string.Empty).ToArray();
for (var i = 0; i < formattedMessageArgs.Length; i++)
{
var arg = formattedMessageArgs[i];
formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]),
_ => arg
};
}
using (new WithTemporaryCulture(player.GetLanguage()))
{
player.PrintToCenter(CS2_SimpleAdmin._localizer[localizedMessageKey, formattedMessageArgs.Cast<object>().ToArray()]);
}
}
private static string ConvertMinutesToTime(int minutes)
{
var time = TimeSpan.FromMinutes(minutes);
return time.Days > 0 ? $"{time.Days}d {time.Hours}h {time.Minutes}m" : time.Hours > 0 ? $"{time.Hours}h {time.Minutes}m" : $"{time.Minutes}m";
}
public static void SendDiscordPenaltyMessage(CCSPlayerController? caller, CCSPlayerController? target, string reason, int duration, PenaltyType penalty, IStringLocalizer? localizer)
{
if (localizer == null) return;
var penaltySetting = penalty switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyBanSettings,
PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyMuteSettings,
PenaltyType.Gag => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyGagSettings,
PenaltyType.Silence => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltySilenceSettings,
PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyWarnSettings,
_ => throw new ArgumentOutOfRangeException(nameof(penalty), penalty, null)
};
var webhookUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("Webhook"))?.Value;
if (string.IsNullOrEmpty(webhookUrl)) return;
const string defaultCommunityUrl = "<https://steamcommunity.com/profiles/0>";
var callerCommunityUrl = caller != null ? $"<{new SteamID(caller.SteamID).ToCommunityUrl()}>" : defaultCommunityUrl;
var targetCommunityUrl = target != null ? $"<{new SteamID(target.SteamID).ToCommunityUrl()}>" : defaultCommunityUrl;
var callerName = caller?.PlayerName ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console";
var targetName = target?.PlayerName ?? localizer["sa_unknown"];
var targetSteamId = target != null ? new SteamID(target.SteamID).SteamId64.ToString() : localizer["sa_unknown"];
var futureTime = Time.ActualDateTime().AddMinutes(duration);
var futureUnixTimestamp = new DateTimeOffset(futureTime).ToUnixTimeSeconds();
string time;
if (penaltySetting.FirstOrDefault(s => s.Name.Equals("Time"))?.Value == "{relative}")
time = duration != 0 ? $"<t:{futureUnixTimestamp}:R>" : localizer["sa_permanent"];
else
time = duration != 0 ? ConvertMinutesToTime(duration) : localizer["sa_permanent"];
string[] fieldNames = [
localizer["sa_player"],
localizer["sa_steamid"],
localizer["sa_duration"],
localizer["sa_reason"],
localizer["sa_admin"]];
string[] fieldValues =
[
$"[{targetName}]({targetCommunityUrl})", $"||{targetSteamId}||", time, reason,
$"[{callerName}]({callerCommunityUrl})"
];
bool[] inlineFlags = [true, true, true, false, false];
var hostname = ConVar.Find("hostname")?.StringValue ?? localizer["sa_unknown"];
var colorValue = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value;
var colorHex = string.IsNullOrWhiteSpace(colorValue) ? "#FFFFFF" : colorValue.Trim();
var embed = new Embed
{
Color = DiscordManager.ColorFromHex(colorHex),
Title = penalty switch
{
PenaltyType.Ban => localizer["sa_discord_penalty_ban"],
PenaltyType.Mute => localizer["sa_discord_penalty_mute"],
PenaltyType.Gag => localizer["sa_discord_penalty_gag"],
PenaltyType.Silence => localizer["sa_discord_penalty_silence"],
PenaltyType.Warn => localizer["sa_discord_penalty_warn"],
_ => throw new ArgumentOutOfRangeException(nameof(penalty), penalty, null)
},
Description = $"{hostname}",
ThumbnailUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("ThumbnailUrl"))?.Value,
ImageUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("ImageUrl"))?.Value,
Footer = new Footer
{
Text = penaltySetting.FirstOrDefault(s => s.Name.Equals("Footer"))?.Value
},
Timestamp = Time.ActualDateTime().ToUniversalTime().ToString("o"),
};
for (var i = 0; i < fieldNames.Length; i++)
{
embed.AddField(fieldNames[i], fieldValues[i], inlineFlags[i]);
}
Task.Run(async () =>
{
try
{
await new DiscordManager(webhookUrl).SendEmbedAsync(embed);
}
catch (Exception ex)
{
// Log or handle the exception
CS2_SimpleAdmin._logger?.LogError("Unable to send discord webhook: {exception}", ex.Message);
}
});
}
public static void SendDiscordPenaltyMessage(CCSPlayerController? caller, string steamId, string reason, int duration, PenaltyType penalty, IStringLocalizer? localizer)
{
if (localizer == null) return;
var penaltySetting = penalty switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyBanSettings,
PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyMuteSettings,
PenaltyType.Gag => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyGagSettings,
PenaltyType.Silence => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltySilenceSettings,
PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyWarnSettings,
_ => throw new ArgumentOutOfRangeException(nameof(penalty), penalty, null)
};
var webhookUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("Webhook"))?.Value;
if (string.IsNullOrEmpty(webhookUrl)) return;
const string defaultCommunityUrl = "<https://steamcommunity.com/profiles/0>";
var callerCommunityUrl = caller != null ? $"<{new SteamID(caller.SteamID).ToCommunityUrl()}>" : defaultCommunityUrl;
var targetCommunityUrl = $"<{new SteamID(ulong.Parse(steamId)).ToCommunityUrl()}>";
var callerName = caller?.PlayerName ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console";
var targetName = steamId;
var targetSteamId = steamId;
var futureTime = Time.ActualDateTime().AddMinutes(duration);
var futureUnixTimestamp = new DateTimeOffset(futureTime).ToUnixTimeSeconds();
string time;
if (penaltySetting.FirstOrDefault(s => s.Name.Equals("Time"))?.Value == "{relative}")
time = duration != 0 ? $"<t:{futureUnixTimestamp}:R>" : localizer["sa_permanent"];
else
time = duration != 0 ? ConvertMinutesToTime(duration) : localizer["sa_permanent"];
string[] fieldNames = [
localizer["sa_player"],
localizer["sa_steamid"],
localizer["sa_duration"],
localizer["sa_reason"],
localizer["sa_admin"]];
string[] fieldValues =
[
$"[{targetName}]({targetCommunityUrl})", $"||{targetSteamId}||", time, reason,
$"[{callerName}]({callerCommunityUrl})"
];
bool[] inlineFlags = [true, true, true, false, false];
var hostname = ConVar.Find("hostname")?.StringValue ?? localizer["sa_unknown"];
var colorValue = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value;
var colorHex = string.IsNullOrWhiteSpace(colorValue) ? "#FFFFFF" : colorValue.Trim();
var embed = new Embed
{
Color = DiscordManager.ColorFromHex(colorHex),
Title = penalty switch
{
PenaltyType.Ban => localizer["sa_discord_penalty_ban"],
PenaltyType.Mute => localizer["sa_discord_penalty_mute"],
PenaltyType.Gag => localizer["sa_discord_penalty_gag"],
PenaltyType.Silence => localizer["sa_discord_penalty_silence"],
PenaltyType.Warn => localizer["sa_discord_penalty_warn"],
_ => throw new ArgumentOutOfRangeException(nameof(penalty), penalty, null)
},
Description = $"{hostname}",
ThumbnailUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("ThumbnailUrl"))?.Value,
ImageUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("ImageUrl"))?.Value,
Footer = new Footer
{
Text = penaltySetting.FirstOrDefault(s => s.Name.Equals("Footer"))?.Value
},
Timestamp = Time.ActualDateTime().ToUniversalTime().ToString("o"),
};
for (var i = 0; i < fieldNames.Length; i++)
{
embed.AddField(fieldNames[i], fieldValues[i], inlineFlags[i]);
}
Task.Run(async () =>
{
try
{
await new DiscordManager(webhookUrl).SendEmbedAsync(embed);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
});
}
private static string GenerateMessageDiscord(string message)
{
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var address = $"{ConVar.Find("ip")?.StringValue}:{ConVar.Find("hostport")!.GetPrimitiveValue<int>()}";
message = message.Replace("HOSTNAME", hostname);
message = message.Replace("ADDRESS", address);
return message;
}
public static string[] SeparateLines(string message)
{
return message.Split(["\r\n", "\r", "\n"], StringSplitOptions.None);
}
public static string CenterMessage(string message) =>
$"{message}";
public static string GetServerIp()
{
var networkSystem = NativeAPI.GetValveInterface(0, "NetworkSystemVersion001");
unsafe
{
if (_networkSystemUpdatePublicIp == null)
{
var funcPtr = *(nint*)(*(nint*)(networkSystem) + 256);
_networkSystemUpdatePublicIp = Marshal.GetDelegateForFunctionPointer<CNetworkSystemUpdatePublicIp>(funcPtr);
}
/*
struct netadr_t
{
uint32_t type
uint8_t ip[4]
uint16_t port
}
*/
// + 4 to skip type, because the size of uint32_t is 4 bytes
var ipBytes = (byte*)(_networkSystemUpdatePublicIp(networkSystem) + 4);
// port is always 0, use the one from convar "hostport"
return $"{ipBytes[0]}.{ipBytes[1]}.{ipBytes[2]}.{ipBytes[3]}";
}
}
public static void UpdateConfig<T>(T config) where T : BasePluginConfig, new()
{
// get newest config version
var newCfgVersion = new T().Version;
// loaded config is up to date
if (config.Version == newCfgVersion)
return;
// update the version
config.Version = newCfgVersion;
// serialize the updated config back to json
var updatedJsonContent = JsonSerializer.Serialize(config,
new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
File.WriteAllText(CfgPath, updatedJsonContent);
}
public static void TryLogCommandOnDiscord(CCSPlayerController? caller, string commandString)
{
if (CS2_SimpleAdmin.DiscordWebhookClientLog == null || CS2_SimpleAdmin._localizer == null)
return;
if (caller != null && caller.IsValid == false)
caller = null;
var callerName = caller == null ? CS2_SimpleAdmin._localizer["sa_console"] : caller.PlayerName;
var communityUrl = caller != null
? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">"
: "<https://steamcommunity.com/profiles/0>";
_ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(GenerateMessageDiscord(
CS2_SimpleAdmin._localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})",
commandString]));
}
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null)
{
var menuType = CS2_SimpleAdmin.Instance.Config.MenuConfigs.MenuType.ToLower();
var menu = menuType switch
{
_ when menuType.Equals("selectable", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenu(title),
_ when menuType.Equals("dynamic", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ButtonMenu),
_ when menuType.Equals("center", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.CenterMenu),
_ when menuType.Equals("chat", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ChatMenu),
_ when menuType.Equals("console", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ConsoleMenu),
_ => CS2_SimpleAdmin.MenuApi?.GetMenu(title)
};
return menu;
}
internal static IPluginManager? GetPluginManager()
{
// Access the singleton instance of Application
var applicationInstance = Application.Instance;
// Use Reflection to access the private _pluginManager field
var pluginManagerField = typeof(Application).GetField("_pluginManager", BindingFlags.NonPublic | BindingFlags.Instance);
var pluginManager = pluginManagerField?.GetValue(applicationInstance) as IPluginManager;
return pluginManager;
}
}
public static class PluginInfo
{
internal static async Task CheckVersion(string localVersion, ILogger logger)
{
ShowAd(localVersion);
const string versionUrl = "https://raw.githubusercontent.com/daffyyyy/CS2-SimpleAdmin/main/CS2-SimpleAdmin/VERSION";
var client = CS2_SimpleAdmin.HttpClient;
client.Timeout = TimeSpan.FromSeconds(3);
try
{
var response = await client.GetAsync(versionUrl);
if (response.IsSuccessStatusCode)
{
var remoteVersion = await response.Content.ReadAsStringAsync();
remoteVersion = remoteVersion.Trim();
var comparisonResult = string.CompareOrdinal(localVersion, remoteVersion);
switch (comparisonResult)
{
case < 0:
logger.LogWarning("Plugin is outdated! Check https://github.com/daffyyyy/CS2-SimpleAdmin");
break;
case > 0:
logger.LogInformation("Probably dev version detected");
break;
default:
logger.LogInformation("Plugin is up to date");
break;
}
}
else
{
logger.LogWarning($"Failed to check version. Status Code: {response.StatusCode}");
}
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "Failed to connect to the version server.");
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while checking version.");
}
}
private static void ShowAd(string moduleVersion)
{
Console.WriteLine(" ");
Console.WriteLine(" _______ ___ __ __ _______ ___ _______ _______ ______ __ __ ___ __ _ ");
Console.WriteLine("| || | | |_| || || | | || _ || | | |_| || | | | | |");
Console.WriteLine("| _____|| | | || _ || | | ___|| |_| || _ || || | | |_| |");
Console.WriteLine("| |_____ | | | || |_| || | | |___ | || | | || || | | |");
Console.WriteLine("|_____ || | | || ___|| |___ | ___|| || |_| || || | | _ |");
Console.WriteLine(" _____| || | | ||_|| || | | || |___ | _ || || ||_|| || | | | | |");
Console.WriteLine("|_______||___| |_| |_||___| |_______||_______||__| |__||______| |_| |_||___| |_| |__|");
Console.WriteLine(" >> Version: " + moduleVersion);
Console.WriteLine(" >> GitHub: https://github.com/daffyyyy/CS2-SimpleAdmin");
Console.WriteLine(" ");
}
}
public class SchemaString<TSchemaClass>(TSchemaClass instance, string member)
: NativeObject(Schema.GetSchemaValue<nint>(instance.Handle, typeof(TSchemaClass).Name, member))
where TSchemaClass : NativeObject
{
public unsafe void Set(string str)
{
var bytes = GetStringBytes(str);
for (var i = 0; i < bytes.Length; i++)
{
Unsafe.Write((void*)(Handle.ToInt64() + i), bytes[i]);
}
Unsafe.Write((void*)(Handle.ToInt64() + bytes.Length), 0);
}
private static byte[] GetStringBytes(string str)
{
return Encoding.UTF8.GetBytes(str);
}
}
public static class Time
{
public static DateTime ActualDateTime()
{
string timezoneId = CS2_SimpleAdmin.Instance.Config.Timezone;
DateTime utcNow = DateTime.UtcNow;
try
{
TimeZoneInfo timezone = TimeZoneInfo.FindSystemTimeZoneById(timezoneId);
DateTime userTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, timezone);
return userTime;
}
catch (TimeZoneNotFoundException)
{
CS2_SimpleAdmin._logger?.LogWarning($"Time zone '{timezoneId}' not found. Returning UTC time.");
return utcNow;
}
catch (InvalidTimeZoneException)
{
CS2_SimpleAdmin._logger?.LogWarning($"Time zone '{timezoneId}' is invalid. Returning UTC time.");
return utcNow;
}
}
}
public static class WeaponHelper
{
private static readonly Lazy<Dictionary<string, CsItem>> WeaponsEnumCache = new(BuildEnumMemberMap);
private static Dictionary<string, CsItem> BuildEnumMemberMap()
{
var dictionary = new Dictionary<string, CsItem>();
foreach (var field in typeof(CsItem).GetFields(BindingFlags.Public | BindingFlags.Static))
{
var attribute = field.GetCustomAttribute<EnumMemberAttribute>();
if (attribute?.Value == null) continue;
if (field.GetValue(null) is not CsItem csItem)
continue;
var enumValue = field.GetValue(null);
dictionary.TryAdd(attribute.Value, csItem);
}
return dictionary;
}
public static CsItem? GetEnumFromWeaponName(string weaponName)
{
if (WeaponsEnumCache.Value.TryGetValue(weaponName, out var csItem))
{
return csItem;
}
return null;
}
public static List<(string EnumMemberValue, CsItem EnumValue)> GetWeaponsByPartialName(string input)
{
// Normalize input for case-insensitive comparison
var normalizedInput = input.ToLowerInvariant();
// Find all matching weapons based on the input
var matchingWeapons = WeaponsEnumCache.Value
.Where(kvp => kvp.Key.Contains(normalizedInput, StringComparison.InvariantCultureIgnoreCase))
.Select(kvp => (EnumMemberValue: kvp.Key, EnumValue: kvp.Value))
.ToList();
// Check for an exact match first
var exactMatch = matchingWeapons
.FirstOrDefault(m => m.EnumMemberValue.Equals(input, StringComparison.OrdinalIgnoreCase));
if (exactMatch.EnumMemberValue != null)
{
// Return a list containing only the exact match
return [exactMatch];
}
// If no exact match, get all matches that start with the input
var filteredWeapons = matchingWeapons
.Where(m => m.EnumMemberValue.StartsWith(normalizedInput, StringComparison.InvariantCultureIgnoreCase))
.ToList();
return filteredWeapons; // Return all relevant matches for the partial input
}
}
public static class IpHelper
{
public static uint IpToUint(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP address cannot be null or empty.", nameof(ipAddress));
if (!System.Net.IPAddress.TryParse(ipAddress, out var ip))
throw new FormatException($"Invalid IP address format: {ipAddress}");
// Ensure it's IPv4 (IPv6 will throw)
if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
throw new FormatException("Only IPv4 addresses are supported.");
byte[] bytes = ip.GetAddressBytes();
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes); // Ensure big-endian (network order)
return BitConverter.ToUInt32(bytes, 0);
}
public static bool TryConvertIpToUint(string ipString, out uint ipUint)
{
ipUint = 0;
if (string.IsNullOrWhiteSpace(ipString))
return false;
if (!System.Net.IPAddress.TryParse(ipString, out var ipAddress))
return false;
var bytes = ipAddress.GetAddressBytes();
if (bytes.Length != 4)
return false;
ipUint = IpToUint(ipString);
return true;
}
public static string UintToIp(uint ipAddress)
{
var bytes = BitConverter.GetBytes(ipAddress).Reverse().ToArray();
return new System.Net.IPAddress(bytes).ToString();
}
}