mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-08 22:35:32 +08:00
1105 lines
41 KiB
C#
1105 lines
41 KiB
C#
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}");
|
||
}
|
||
}
|
||
}
|
||
}
|