Files
squad-rain-ops-mini/Plugins/PersonalBuffPlugin.cs

1105 lines
41 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using RainOpsMini.Helpers;
using RainOpsMini.Services;
namespace RainOpsMini.Plugins
{
/// <summary>
/// 个人BUFF插件配置
/// </summary>
public class PersonalBuffConfig
{
/// <summary>
/// 是否启用插件
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// 触发关键字 (逗号分隔,支持拼音首字母)
/// </summary>
public string LikeKey { get; set; } = "buff,grbuff,战斗模式";
/// <summary>
/// 需要扣除的积分数量
/// </summary>
public int Integral { get; set; } = 100;
/// <summary>
/// 击杀提醒是否开启
/// </summary>
public bool EnableDie { get; set; } = true;
/// <summary>
/// 受伤提醒是否开启
/// </summary>
public bool EnableWound { get; set; } = true;
/// <summary>
/// 救治提醒是否开启
/// </summary>
public bool EnableRevive { get; set; } = true;
/// <summary>
/// 攻击伤害提醒是否开启
/// </summary>
public bool EnableAttack { get; set; } = true;
/// <summary>
/// 激活模式: Map (整张地图有效) 或 Time (时间有效)
/// </summary>
public string Type { get; set; } = "Time";
/// <summary>
/// 激活时长(分钟)仅当Type为Time时有效
/// </summary>
public int Minute { get; set; } = 120;
/// <summary>
/// 切换静默模式指令
/// </summary>
public string ToggleSilentCommand { get; set; } = "静默BUFF";
/// <summary>
/// 激活成功提示模板
/// </summary>
public string MsgTemplate_ActiveSuccess { get; set; } = "BUFF激活成功消耗{Points}积分,{ExpiryMsg}。";
/// <summary>
/// 重复激活提示模板
/// </summary>
public string MsgTemplate_AlreadyActive { get; set; } = "您的BUFF正在生效中剩余时间{TimeLeft}。";
/// <summary>
/// 击伤攻击者提示模板
/// </summary>
public string MsgTemplate_Wound_Attacker { get; set; } = "你击伤了 {Victim} (伤害: {Damage}, 武器: {Weapon})";
/// <summary>
/// 击伤受害者提示模板
/// </summary>
public string MsgTemplate_Wound_Victim { get; set; } = "{Attacker} 击伤了你 (伤害: {Damage}, 武器: {Weapon})";
/// <summary>
/// 击杀攻击者提示模板
/// </summary>
public string MsgTemplate_Die_Attacker { get; set; } = "你击杀了 {Victim} (武器: {Weapon})";
/// <summary>
/// 击杀受害者提示模板
/// </summary>
public string MsgTemplate_Die_Victim { get; set; } = "{Attacker} 击杀了你 (武器: {Weapon})";
/// <summary>
/// 救人者提示模板
/// </summary>
public string MsgTemplate_Revive_Reviver { get; set; } = "你救起了 {Revived}";
/// <summary>
/// 被救者提示模板
/// </summary>
public string MsgTemplate_Revive_Revived { get; set; } = "{Reviver} 救起了你";
/// <summary>
/// 造成伤害提示模板
/// </summary>
public string MsgTemplate_Attack { get; set; } = "造成伤害: {Damage}";
/// <summary>
/// VIP豁免列表
/// </summary>
public List<BUFFVipInfo> VipList { get; set; } = new List<BUFFVipInfo>();
}
/// <summary>
/// VIP信息类
/// </summary>
public class BUFFVipInfo
{
/// <summary>
/// 玩家ID
/// </summary>
public string SteamId { get; set; }
/// <summary>
/// 昵称(可不填)
/// </summary>
public string Name { get; set; }
/// <summary>
/// 开通原因(可不填)
/// </summary>
public string Reason { get; set; }
/// <summary>
/// 过期时间(可不填)
/// </summary>
public DateTime ExpireTime { get; set; }
}
/// <summary>
/// BUFF状态类
/// </summary>
public class BuffState
{
public DateTime ExpireTime { get; set; }
public bool IsSilent { get; set; }
public bool IsVip { get; set; }
}
/// <summary>
/// 个人BUFF插件
/// </summary>
public class PersonalBuffPlugin : IPlugin, IDisposable
{
public string Name => "个人BUFF系统";
public string Description => "允许玩家消耗积分开启战斗日志通知(BUFF),支持击杀、受伤、攻击、救治提示";
public PluginCategory Category => PluginCategory.PointsFunction;
public bool IsEnabled => _config.Enabled;
private PersonalBuffConfig _config = new PersonalBuffConfig();
// SteamID -> BuffState
private Dictionary<string, BuffState> _activeBuffs = new Dictionary<string, BuffState>();
private string _currentMap = "";
private readonly object _lock = new object();
private readonly string _storagePath;
private System.Timers.Timer _vipTimer;
/// <summary>
/// 构造函数,初始化存储路径并加载数据,注册事件
/// </summary>
public PersonalBuffPlugin()
{
_storagePath = Path.Combine(Environment.CurrentDirectory, "PluginData", "PersonalBuffData.json");
LoadBuffs();
// 初始化VIP检查定时器 (每60秒检查一次)
_vipTimer = new System.Timers.Timer(60000);
_vipTimer.Elapsed += (s, e) => CheckVips();
ApplyConfig();
}
/// <summary>
/// 从本地JSON文件加载活跃的BUFF数据
/// </summary>
private void LoadBuffs()
{
try
{
if (File.Exists(_storagePath))
{
string json = File.ReadAllText(_storagePath);
// 尝试先读取为 JsonDocument 以判断格式
using (JsonDocument doc = JsonDocument.Parse(json))
{
JsonElement root = doc.RootElement;
string map = "";
if (root.TryGetProperty("CurrentMap", out JsonElement mapEl))
{
map = mapEl.GetString() ?? "";
}
var newBuffs = new Dictionary<string, BuffState>();
if (root.TryGetProperty("ActiveBuffs", out JsonElement buffsEl))
{
foreach (JsonProperty property in buffsEl.EnumerateObject())
{
string steamId = property.Name;
JsonElement value = property.Value;
if (value.ValueKind == JsonValueKind.String)
{
// 旧格式: Value 是 DateTime 字符串
if (DateTime.TryParse(value.GetString(), out DateTime expiry))
{
newBuffs[steamId] = new BuffState { ExpireTime = expiry, IsSilent = false, IsVip = false };
}
}
else if (value.ValueKind == JsonValueKind.Object)
{
// 新格式: Value 是 BuffState 对象
var state = JsonSerializer.Deserialize<BuffState>(value.GetRawText());
if (state != null)
{
newBuffs[steamId] = state;
}
}
}
}
lock (_lock)
{
_activeBuffs = newBuffs;
_currentMap = map;
}
}
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] LoadBuffs Error: {ex.Message}");
}
}
/// <summary>
/// 定期检查VIP玩家并自动激活BUFF
/// </summary>
private async void CheckVips()
{
try
{
if (!_config.Enabled || _config.VipList == null || _config.VipList.Count == 0) return;
var players = Program.RconCache?.Players;
if (players == null || players.Count == 0) return;
foreach (var player in players)
{
var vip = _config.VipList.FirstOrDefault(v => v.SteamId == player.SteamId);
if (vip != null)
{
if (DateTime.Now < vip.ExpireTime)
{
// 检查是否已经激活BUFF
bool needActivate = false;
lock (_lock)
{
if (!_activeBuffs.ContainsKey(player.SteamId))
{
needActivate = true;
}
else
{
// 如果已激活但不是VIP模式或者是VIP但时间快到了小于5分钟刷新时间
var state = _activeBuffs[player.SteamId];
if (!state.IsVip || state.ExpireTime < DateTime.Now.AddMinutes(5))
{
needActivate = true;
}
}
}
if (needActivate)
{
DateTime newExpiry = DateTime.Now.AddHours(1); // VIP自动续期每次1小时
string expiryMsg = "VIP自动激活";
lock (_lock)
{
_activeBuffs[player.SteamId] = new BuffState
{
ExpireTime = newExpiry,
IsSilent = false, // 默认不静默,用户可自行切换
IsVip = true
};
}
SaveBuffs();
RainOpsLog.Log($"[BUFF] VIP Auto Activate: {player.Name} ({player.SteamId}), Reason: {vip.Reason}");
// 发送通知
string msg = FormatMessage(_config.MsgTemplate_ActiveSuccess, new Dictionary<string, string> {
{ "Points", "0 (VIP)" }, { "ExpiryMsg", expiryMsg }
});
await SendWarn(player.SteamId, msg);
}
}
}
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] CheckVips Error: {ex.Message}");
}
}
/// <summary>
/// 将当前活跃的BUFF数据保存到本地JSON文件
/// </summary>
private void SaveBuffs()
{
try
{
string dir = Path.GetDirectoryName(_storagePath);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
BuffStorageData data;
lock (_lock)
{
data = new BuffStorageData
{
CurrentMap = _currentMap,
ActiveBuffs = _activeBuffs
};
}
string json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_storagePath, json);
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] SaveBuffs Error: {ex.Message}");
}
}
/// <summary>
/// BUFF数据存储结构类
/// </summary>
private class BuffStorageData
{
public string CurrentMap { get; set; }
public Dictionary<string, BuffState> ActiveBuffs { get; set; }
}
/// <summary>
/// 获取当前配置
/// </summary>
/// <returns></returns>
public object GetConfig() => _config;
/// <summary>
/// 更新配置反序列化JSON并处理大小写
/// </summary>
/// <param name="config"></param>
public void UpdateConfig(JsonElement config)
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
try
{
_config = JsonSerializer.Deserialize<PersonalBuffConfig>(config.GetRawText(), options) ?? new PersonalBuffConfig();
ApplyConfig();
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] UpdateConfig Error: {ex.Message}");
}
}
/// <summary>
/// 应用配置,根据启用状态管理事件订阅和定时器
/// </summary>
private void ApplyConfig()
{
Program.OnRconMessageReceived -= OnRconMessage;
SquadLogHelper.squadLogCleaning.OnLogReceived -= OnLogMessage;
_vipTimer.Stop();
if (_config.Enabled)
{
Program.OnRconMessageReceived += OnRconMessage;
SquadLogHelper.squadLogCleaning.OnLogReceived += OnLogMessage;
_vipTimer.Start();
}
}
/// <summary>
/// 重置配置为默认值
/// </summary>
public void ResetConfig()
{
_config = new PersonalBuffConfig();
}
/// <summary>
/// 释放资源,取消事件订阅
/// </summary>
public void Dispose()
{
Program.OnRconMessageReceived -= OnRconMessage;
// 取消订阅日志事件
SquadLogHelper.squadLogCleaning.OnLogReceived -= OnLogMessage;
_vipTimer?.Stop();
_vipTimer?.Dispose();
}
/// <summary>
/// 处理游戏日志消息
/// </summary>
/// <param name="msg"></param>
private async void OnLogMessage(string msg)
{
if (!_config.Enabled) return;
try
{
// 解析战斗日志 (仅当有活跃BUFF时)
if (_activeBuffs.Count > 0 && (_config.EnableDie || _config.EnableWound || _config.EnableRevive || _config.EnableAttack))
{
await ProcessGameLog(msg);
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] Log Error: {ex.Message}");
}
}
/// <summary>
/// 处理RCON消息的主入口
/// </summary>
/// <param name="msg">RCON消息内容</param>
private async void OnRconMessage(string msg)
{
if (!_config.Enabled) return;
try
{
// 1. 检查地图变更
CheckMapStatus();
// 2. 解析聊天指令
await ProcessChatCommand(msg);
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] Error: {ex.Message}");
}
}
/// <summary>
/// 检查地图状态处理换图时的BUFF清理
/// </summary>
private void CheckMapStatus()
{
var serverInfo = Program.RconCache?.ServerInfo;
if (serverInfo == null) return;
string map = serverInfo.MapName_s ?? "";
if (map != _currentMap)
{
lock (_lock)
{
_currentMap = map;
// 如果是Map模式换图时清除所有BUFF
// 如果是Time模式清理已过期的BUFF (虽然OnRconMessage也会用到但换图清理一下也好)
if (_config.Type.Equals("Map", StringComparison.OrdinalIgnoreCase))
{
_activeBuffs.Clear();
SaveBuffs();
}
}
}
// 清理过期BUFF (通用)
CleanupExpiredBuffs();
}
/// <summary>
/// 清理已过期的Time模式BUFF
/// </summary>
private void CleanupExpiredBuffs()
{
bool changed = false;
lock (_lock)
{
var now = DateTime.Now;
var expired = _activeBuffs.Where(kv => kv.Value.ExpireTime < now).Select(kv => kv.Key).ToList();
if (expired.Count > 0)
{
foreach (var id in expired)
{
_activeBuffs.Remove(id);
}
changed = true;
}
}
if (changed)
{
SaveBuffs();
}
}
/// <summary>
/// 切换静默模式
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
private async Task ToggleSilent(string steamId)
{
bool currentState = false;
bool hasBuff = false;
lock (_lock)
{
if (_activeBuffs.TryGetValue(steamId, out var state))
{
hasBuff = true;
currentState = state.IsSilent;
state.IsSilent = !currentState;
}
}
if (hasBuff)
{
SaveBuffs();
string status = !currentState ? "已开启" : "已关闭";
await SendWarn(steamId, $"静默模式{status}。");
}
else
{
await SendWarn(steamId, "您未激活BUFF无法设置静默模式。");
}
}
/// <summary>
/// 解析聊天消息,检测是否为触发指令
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
private async Task ProcessChatCommand(string msg)
{
// RainOpsLog.Log($"[BUFF] Debug Chat: {msg}");
// 简单解析聊天
var match = Regex.Match(msg, @"ChatMessage: \[(?<steamId>\d+)\] (?<name>.+) : (?<content>.+)");
if (!match.Success)
{
match = Regex.Match(msg, @"\[Chat.+?\] \[Online IDs:EOS: .+? steam: (?<steamId>\d+)\]\s+(?<name>.+?) : (?<content>.+)");
}
if (match.Success)
{
string steamId = match.Groups["steamId"].Value;
string content = match.Groups["content"].Value.Trim();
// RainOpsLog.Log($"[BUFF] Chat Match: {steamId} said '{content}'");
if (!string.IsNullOrEmpty(_config.ToggleSilentCommand) &&
content.Equals(_config.ToggleSilentCommand, StringComparison.OrdinalIgnoreCase))
{
await ToggleSilent(steamId);
return;
}
if (IsTriggerCommand(content))
{
RainOpsLog.Log($"[BUFF] Trigger Command Detected from {steamId}: {content}");
await ActivateBuff(steamId);
}
}
}
/// <summary>
/// 判断内容是否匹配触发关键字(支持模糊匹配、拼音首字母)
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
private bool IsTriggerCommand(string content)
{
if (string.IsNullOrWhiteSpace(_config.LikeKey)) return false;
var keys = _config.LikeKey.Split(new[] { ',', '' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var key in keys)
{
if (string.IsNullOrWhiteSpace(key)) continue;
if (content.Equals(key, StringComparison.OrdinalIgnoreCase)) return true;
string initials = PinyinHelper.GetInitials(key);
if (!string.IsNullOrEmpty(initials) && content.Equals(initials, StringComparison.OrdinalIgnoreCase)) return true;
string pinyin = PinyinHelper.GetPinyin(key);
if (!string.IsNullOrEmpty(pinyin) && content.Equals(pinyin, StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}
/// <summary>
/// 激活玩家BUFF执行扣分并设置过期时间
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
private async Task ActivateBuff(string steamId)
{
// 检查是否已经激活
BuffState state = null;
lock (_lock)
{
if (_activeBuffs.TryGetValue(steamId, out var s))
{
if (s.ExpireTime > DateTime.Now)
{
state = s;
}
}
}
if (state != null)
{
// 计算剩余时间
TimeSpan left = state.ExpireTime - DateTime.Now;
string timeLeftStr;
if (state.ExpireTime == DateTime.MaxValue)
timeLeftStr = "本局结束前";
else if (left.TotalHours >= 1)
timeLeftStr = $"{(int)left.TotalHours}小时{left.Minutes}分";
else
timeLeftStr = $"{left.Minutes}分{left.Seconds}秒";
string msg = FormatMessage(_config.MsgTemplate_AlreadyActive, new Dictionary<string, string> {
{ "TimeLeft", timeLeftStr }
});
await SendWarn(steamId, msg);
return;
}
// 扣分
int points = await PointsService.GetPointsAsync(steamId);
if (points >= _config.Integral)
{
if (await PointsService.AdjustPointsAsync(steamId, -_config.Integral) != 0)
{
// 激活成功
DateTime expiry;
string expiryMsg;
if (_config.Type.Equals("Map", StringComparison.OrdinalIgnoreCase))
{
expiry = DateTime.MaxValue; // Map模式直到换图
expiryMsg = "本局地图结束前有效";
}
else
{
expiry = DateTime.Now.AddMinutes(_config.Minute);
expiryMsg = $"{_config.Minute}分钟内有效";
}
lock (_lock)
{
_activeBuffs[steamId] = new BuffState { ExpireTime = expiry, IsSilent = false, IsVip = false };
}
SaveBuffs();
string msg = FormatMessage(_config.MsgTemplate_ActiveSuccess, new Dictionary<string, string> {
{ "Points", _config.Integral.ToString() },
{ "ExpiryMsg", expiryMsg }
});
await SendWarn(steamId, msg);
}
else
{
await SendWarn(steamId, "扣分失败,请联系管理员。");
}
}
else
{
await SendWarn(steamId, $"积分不足!需要{_config.Integral}积分,当前{points}积分。");
}
}
/// <summary>
/// 解析游戏日志,分发各类战斗事件
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
private async Task ProcessGameLog(string msg)
{
// 过滤旧日志 (超过3秒)
// 格式: [2024.02.11-12.00.00:123]
var match = Regex.Match(msg, @"^\[(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}:\d{3})\]");
if (match.Success)
{
// 尝试解析时间
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy.MM.dd-HH.mm.ss:fff", null, System.Globalization.DateTimeStyles.None, out DateTime logTime))
{
// 日志时间通常是UTC转换为UTC+8北京时间以匹配系统时间
logTime = logTime.AddHours(8);
// 假设日志时间与系统时间一致(或时区匹配),若差异过大则忽略
if (Math.Abs((DateTime.Now - logTime).TotalSeconds) > 3)
{
// RainOpsLog.Log($"[BUFF] Log Time Ignored: LogTime(UTC+8)={logTime}, SysTime={DateTime.Now}");
return;
}
}
}
// Wound
if (_config.EnableWound && msg.Contains("Wound()"))
{
string cleanMsg = msg.Replace("ASQSoldier::", "");
RainOpsLog.Log($"[BUFF] Processing Wound: {cleanMsg}");
await HandleWound(cleanMsg);
}
// Die
else if (_config.EnableDie && msg.Contains("Die()"))
{
string cleanMsg = msg.Replace("ASQSoldier::", "");
RainOpsLog.Log($"[BUFF] Processing Die: {cleanMsg}");
await HandleDie(cleanMsg);
}
// Revived
else if (_config.EnableRevive && msg.Contains("has revived"))
{
RainOpsLog.Log($"[BUFF] Processing Revived: {msg}");
await HandleRevived(msg);
}
// Attack
else if (_config.EnableAttack && msg.Contains("ActualDamage="))
{
// RainOpsLog.Log($"[BUFF] Processing Attack: {msg}");
await HandleAttack(msg);
}
}
/// <summary>
/// 处理受伤(Wound)日志,通知双方
/// </summary>
/// <param name="log"></param>
/// <returns></returns>
private async Task HandleWound(string log)
{
// 优化正则:使用 \s* 允许空格波动,且忽略 Controller ID 具体文本
string pattern = @"\[(.*?)\]\[(.*?)\]LogSquadTrace: \[DedicatedServer\]Wound\(\): Player:\s*(.*?)\s*KillingDamage=(.*?) from (.*?) \(Online IDs: (.*?) steam:\s*(.*?)\s*\|.*?\) caused by (.*)";
if (!log.Contains("steam:")) log = log.Replace("INVALID", "EOS: 0000000000000000000000000000000 steam: 00000000000000000");
var match = Regex.Match(log, pattern);
if (match.Success)
{
string victimName = match.Groups[3].Value.Trim();
string damage = match.Groups[4].Value.Trim();
string attackerSteamId = match.Groups[7].Value.Trim();
string weapon = ExtractMiddlePart(match.Groups[8].Value.Trim());
// 查找受害者SteamID
string victimSteamId = FindSteamIdByName(victimName);
RainOpsLog.Log($"[BUFF] Wound Match: Victim={victimName}({victimSteamId}), Attacker={attackerSteamId}, Dmg={damage}");
if (HasBuff(attackerSteamId) || HasBuff(victimSteamId))
{
string attackerName = FindNameBySteamId(attackerSteamId) ?? "Unknown";
// 通知攻击者
if (!string.IsNullOrEmpty(attackerSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Wound_Attacker, new Dictionary<string, string> {
{ "Victim", victimName }, { "Damage", damage }, { "Weapon", weapon }, { "Attacker", attackerName }
});
await SendWarn(attackerSteamId, msg, true);
}
// 通知受害者
if (!string.IsNullOrEmpty(victimSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Wound_Victim, new Dictionary<string, string> {
{ "Victim", victimName }, { "Damage", damage }, { "Weapon", weapon }, { "Attacker", attackerName }
});
await SendWarn(victimSteamId, msg, true);
}
}
else
{
RainOpsLog.Log($"[BUFF] Wound No Buff Active: {attackerSteamId} or {victimSteamId}");
}
}
else
{
RainOpsLog.Log($"[BUFF] Wound Match Failed: {log}");
}
}
/// <summary>
/// 处理死亡(Die)日志,通知双方
/// </summary>
/// <param name="log"></param>
/// <returns></returns>
private async Task HandleDie(string log)
{
// 优化正则:忽略 Controller ID 具体文本
string pattern = @"\[(.*?)\]\[(.*?)\]LogSquadTrace: \[DedicatedServer\]Die\(\): Player:\s*(.*?)\s*KillingDamage=(.*?) from (.*?) \(Online IDs: EOS: (.*?) steam:\s*(.*?)\s*\|.*?\) caused by (.*)";
if (!log.Contains("steam:")) log = log.Replace("INVALID", "EOS: 0000000000000000000000000000000 steam: 00000000000000000");
var match = Regex.Match(log, pattern);
if (match.Success)
{
string victimName = match.Groups[3].Value.Trim();
string damage = match.Groups[4].Value.Trim();
string attackerSteamId = match.Groups[7].Value.Trim();
string weapon = ExtractMiddlePart(match.Groups[8].Value.Trim());
string victimSteamId = FindSteamIdByName(victimName);
RainOpsLog.Log($"[BUFF] Die Match: Victim={victimName}({victimSteamId}), Attacker={attackerSteamId}");
if (HasBuff(attackerSteamId) || HasBuff(victimSteamId))
{
string attackerName = FindNameBySteamId(attackerSteamId) ?? "Unknown";
// 通知攻击者
if (!string.IsNullOrEmpty(attackerSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Die_Attacker, new Dictionary<string, string> {
{ "Victim", victimName }, { "Damage", damage }, { "Weapon", weapon }, { "Attacker", attackerName }
});
await SendWarn(attackerSteamId, msg, true);
}
// 通知受害者
if (!string.IsNullOrEmpty(victimSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Die_Victim, new Dictionary<string, string> {
{ "Victim", victimName }, { "Damage", damage }, { "Weapon", weapon }, { "Attacker", attackerName }
});
await SendWarn(victimSteamId, msg, true);
}
}
else
{
RainOpsLog.Log($"[BUFF] Die No Buff Active: {attackerSteamId} or {victimSteamId}");
}
}
else
{
RainOpsLog.Log($"[BUFF] Die Match Failed: {log}");
}
}
/// <summary>
/// 处理救治(Revived)日志,通知双方
/// </summary>
/// <param name="log"></param>
/// <returns></returns>
private async Task HandleRevived(string log)
{
string pattern = @"\[.*?\]\[.*?\]LogSquad:(.*?)\s*\(Online IDs: EOS: (.*?) steam:\s*(.*?)\) has revived (.*?)\s*\(Online IDs: EOS: (.*?) steam:\s*(.*?)\)\.";
var match = Regex.Match(log, pattern);
if (match.Success)
{
string reviverName = match.Groups[1].Value.Trim();
string reviverSteamId = match.Groups[3].Value.Trim();
string revivedName = match.Groups[4].Value.Trim();
string revivedSteamId = match.Groups[6].Value.Trim();
RainOpsLog.Log($"[BUFF] Revive Match: Reviver={reviverName}({reviverSteamId}), Revived={revivedName}({revivedSteamId})");
if (HasBuff(reviverSteamId) || HasBuff(revivedSteamId))
{
if (!string.IsNullOrEmpty(reviverSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Revive_Reviver, new Dictionary<string, string> {
{ "Revived", revivedName }, { "Reviver", reviverName }
});
await SendWarn(reviverSteamId, msg, true);
}
if (!string.IsNullOrEmpty(revivedSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Revive_Revived, new Dictionary<string, string> {
{ "Revived", revivedName }, { "Reviver", reviverName }
});
await SendWarn(revivedSteamId, msg, true);
}
}
else
{
RainOpsLog.Log($"[BUFF] Revive No Buff Active: {reviverSteamId} or {revivedSteamId}");
}
}
else
{
RainOpsLog.Log($"[BUFF] Revive Match Failed: {log}");
}
}
/// <summary>
/// 处理攻击(Attack)日志,通知双方
/// </summary>
/// <param name="log"></param>
/// <returns></returns>
private async Task HandleAttack(string log)
{
// 优化正则:忽略 Controller ID 具体文本
string pattern = @"\[.*?\]\[.*?\]LogSquad: Player:\s*(.*?)\s*ActualDamage=(.*?) from (.*?) \(Online IDs: EOS: (.*?) steam:\s*(.*?)\s*\|.*?\)caused by (.*)";
if (!log.Contains("steam:")) log = log.Replace("INVALID", "EOS: 0000000000000000000000000000000 steam: 00000000000000000");
var match = Regex.Match(log, pattern);
if (match.Success)
{
string victimName = match.Groups[1].Value.Trim();
string damage = match.Groups[2].Value.Trim();
string attackerSteamId = match.Groups[5].Value.Trim();
string weapon = ExtractMiddlePart(match.Groups[6].Value.Trim());
// 查找受害者SteamID
string victimSteamId = FindSteamIdByName(victimName);
RainOpsLog.Log($"[BUFF] Attack Match: Victim={victimName}({victimSteamId}), Attacker={attackerSteamId}, Dmg={damage}");
if (HasBuff(attackerSteamId) || HasBuff(victimSteamId))
{
string attackerName = FindNameBySteamId(attackerSteamId) ?? "Unknown";
// 通知攻击者
if (!string.IsNullOrEmpty(attackerSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Attack, new Dictionary<string, string> {
{ "Victim", victimName }, { "Damage", damage }, { "Weapon", weapon }, { "Attacker", attackerName }
});
await SendWarn(attackerSteamId, msg, true);
}
// 通知受害者
if (!string.IsNullOrEmpty(victimSteamId))
{
string msg = FormatMessage(_config.MsgTemplate_Attack, new Dictionary<string, string> {
{ "Victim", victimName }, { "Damage", damage }, { "Weapon", weapon }, { "Attacker", attackerName }
});
await SendWarn(victimSteamId, msg, true);
}
}
else
{
RainOpsLog.Log($"[BUFF] Attack No Buff Active: {attackerSteamId} or {victimSteamId}");
}
}
else
{
RainOpsLog.Log($"[BUFF] Attack Match Failed: {log}");
}
}
/// <summary>
/// 检查玩家是否拥有活跃BUFF
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
private bool HasBuff(string steamId)
{
if (string.IsNullOrEmpty(steamId)) return false;
lock (_lock)
{
if (_activeBuffs.TryGetValue(steamId, out var state))
{
return state.ExpireTime > DateTime.Now;
}
}
return false;
}
/// <summary>
/// 根据玩家名称查找SteamID
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
private string FindSteamIdByName(string name)
{
var players = Program.RconCache?.Players;
if (players == null) return null;
return players.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.SteamId;
}
/// <summary>
/// 根据SteamID查找玩家名称
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
private string FindNameBySteamId(string steamId)
{
var players = Program.RconCache?.Players;
if (players == null) return null;
return players.FirstOrDefault(p => p.SteamId == steamId)?.Name;
}
/// <summary>
/// 提取字符串中间部分(通常用于简化武器名称)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private string ExtractMiddlePart(string input)
{
if (string.IsNullOrEmpty(input))
return input;
// 移除末尾的 _C_xxxxx 部分
string cleaned = Regex.Replace(input, @"_C_\d+$", "");
// 分割字符串
string[] parts = cleaned.Split('_');
// 从数组中间提取关键部分
// BP_Soldier_PLA_SquadLeader_Woodland 的关键是 PLA 和 SquadLeader
if (parts.Length >= 4)
{
// 提取阵营通常是第3部分
string faction = parts[2];
// 提取兵种通常是第4部分
string role = parts[3];
return $"{faction}_{role}";
}
else if (parts.Length >= 2)
{
// 如果格式不标准,至少返回后两个部分
return $"{parts[parts.Length - 2]}_{parts[parts.Length - 1]}";
}
return input;
}
/// <summary>
/// 格式化消息模板
/// </summary>
private string FormatMessage(string template, Dictionary<string, string> args)
{
if (string.IsNullOrEmpty(template)) return "";
string result = template;
foreach (var kv in args)
{
result = result.Replace("{" + kv.Key + "}", kv.Value);
}
return result;
}
/// <summary>
/// 发送AdminWarn警告通知给玩家
/// </summary>
/// <param name="steamId"></param>
/// <param name="msg"></param>
/// <param name="checkSilent">是否检查静默模式</param>
/// <returns></returns>
private async Task SendWarn(string steamId, string msg, bool checkSilent = false)
{
if (checkSilent)
{
lock (_lock)
{
if (_activeBuffs.TryGetValue(steamId, out var state) && state.IsSilent)
{
return;
}
}
}
if (Program.Client != null && Program.Client.IsConnected && !string.IsNullOrEmpty(steamId))
{
RainOpsLog.Log($"[BUFF] Sending AdminWarn to {steamId}: {msg}");
// 防止消息中的双引号破坏RCON命令结构
msg = msg.Replace("\"", "'");
// 移除换行符
msg = msg.Replace("\r", "").Replace("\n", " ");
// AdminWarn "SteamId" "Message"
await Program.Client.SendCommandAsync($"AdminWarn \"{steamId}\" \"{msg}\"");
}
else
{
RainOpsLog.Log($"[BUFF] SendWarn Failed: Client Connected={Program.Client?.IsConnected}, SteamId={steamId}");
}
}
}
}