mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-08 14:25:33 +08:00
410 lines
14 KiB
C#
410 lines
14 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using System.Text.RegularExpressions;
|
||
using RainOpsMini.Helpers;
|
||
using RainOpsMini.Helpers.RCON;
|
||
using RainOpsMini;
|
||
using RainOpsMini.Models;
|
||
using RainOpsMini.Models;
|
||
using RainOpsMini.Helpers.SquadGameLog;
|
||
using SqlSugar;
|
||
|
||
namespace RainOpsMini.Plugins
|
||
{
|
||
/// <summary>
|
||
/// 建队自动播报插件
|
||
/// </summary>
|
||
public class SquadCreationAnnouncePlugin : IPlugin, IDisposable
|
||
{
|
||
public string Name => "建队自动播报";
|
||
public string Description => "建队自动播报与限制插件 - 监控建队行为,支持时长限制、积分豁免及自动播报";
|
||
public PluginCategory Category => PluginCategory.BasicFunction;
|
||
public bool IsEnabled => _config.Enabled ;
|
||
|
||
private SquadCreationAnnounceConfig _config = new SquadCreationAnnounceConfig();
|
||
|
||
// Track squad creation attempts per match/session: SteamId -> Count
|
||
private static readonly Dictionary<string, int> _creationAttempts = new Dictionary<string, int>();
|
||
// Track last match map to clear attempts on map change
|
||
private string _lastMapName = "";
|
||
|
||
/// <summary>
|
||
/// 构造函数,订阅RCON消息
|
||
/// </summary>
|
||
public SquadCreationAnnouncePlugin()
|
||
{
|
||
Program.OnRconMessageReceived += OnRconMessage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前配置
|
||
/// </summary>
|
||
/// <returns>配置对象</returns>
|
||
public object GetConfig() => _config;
|
||
|
||
/// <summary>
|
||
/// 更新配置
|
||
/// </summary>
|
||
/// <param name="config">JSON配置元素</param>
|
||
public void UpdateConfig(JsonElement config)
|
||
{
|
||
try
|
||
{
|
||
var options = new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
};
|
||
_config = JsonSerializer.Deserialize<SquadCreationAnnounceConfig>(config.GetRawText(), options) ?? new SquadCreationAnnounceConfig();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] 配置更新失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置配置为默认值
|
||
/// </summary>
|
||
public void ResetConfig()
|
||
{
|
||
_config = new SquadCreationAnnounceConfig();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
Program.OnRconMessageReceived -= OnRconMessage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理RCON消息
|
||
/// </summary>
|
||
/// <param name="msg">RCON消息内容</param>
|
||
private void OnRconMessage(string msg)
|
||
{
|
||
if (_config.Enabled != true) return;
|
||
|
||
try
|
||
{
|
||
// 检测地图变更以重置计数器
|
||
CheckMapChange();
|
||
|
||
// 1. 建队检测
|
||
// LogSquad: Player: "PlayerName" (SteamId: 7656xxx) has created Squad 1 (Squad Name) on Team 1 (Team Name)
|
||
if (msg.Contains("has created Squad"))
|
||
{
|
||
HandleSquadCreation(msg);
|
||
}
|
||
|
||
// 2. 聊天查询
|
||
if (_config.ChatQuery == true)
|
||
{
|
||
var chatMsg = RconParser.ParseChatMessage(msg);
|
||
if (chatMsg != null)
|
||
{
|
||
HandleChatQuery(chatMsg);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] 处理消息异常: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测地图变更,重置建队尝试计数
|
||
/// </summary>
|
||
private void CheckMapChange()
|
||
{
|
||
var currentMap = Program.RconCache.ServerInfo?.MapName_s;
|
||
if (!string.IsNullOrEmpty(currentMap) && currentMap != _lastMapName)
|
||
{
|
||
_lastMapName = currentMap;
|
||
_creationAttempts.Clear();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理建队事件
|
||
/// </summary>
|
||
/// <param name="msg">日志消息</param>
|
||
private async void HandleSquadCreation(string msg)
|
||
{
|
||
// 解析消息
|
||
// LogSquad: Player: "Name" (SteamId: 123) has created Squad 5 (Alpha) on Team 1 (USA)
|
||
var match = Regex.Match(msg, @"Player: ""(.*?)"" \(SteamId: (\d+)\) has created Squad (\d+) \((.*?)\) on Team (\d+)");
|
||
if (!match.Success) return;
|
||
|
||
string playerName = match.Groups[1].Value;
|
||
string steamId = match.Groups[2].Value;
|
||
string squadId = match.Groups[3].Value;
|
||
string squadName = match.Groups[4].Value;
|
||
string teamId = match.Groups[5].Value;
|
||
|
||
// 1. 检查忽略地图
|
||
if (!string.IsNullOrEmpty(_config.Ignore))
|
||
{
|
||
var map = Program.RconCache.ServerInfo?.MapName_s ?? "";
|
||
var ignoreList = _config.Ignore.Split(new[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||
foreach (var ignore in ignoreList)
|
||
{
|
||
if (map.IndexOf(ignore, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
return; // 忽略的地图
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 追踪尝试次数
|
||
if (!_creationAttempts.ContainsKey(steamId)) _creationAttempts[steamId] = 0;
|
||
_creationAttempts[steamId]++;
|
||
|
||
if (_config.MaxTry > 0 && _creationAttempts[steamId] > _config.MaxTry)
|
||
{
|
||
// 超过最大尝试次数 -> 踢出
|
||
SendRconCommand($"AdminKick \"{steamId}\" \"建队尝试次数过多\"");
|
||
return;
|
||
}
|
||
|
||
// 3. 检查游戏时长
|
||
double playHours = GetPlayerTotalPlayHours(steamId);
|
||
double requiredHours = _config.MinRunHour; // 配置为整数但逻辑上是小时
|
||
|
||
bool isAllowed = false;
|
||
string allowReason = "";
|
||
|
||
if (requiredHours <= 0 || playHours >= requiredHours)
|
||
{
|
||
isAllowed = true;
|
||
allowReason = "满足时长要求";
|
||
}
|
||
else
|
||
{
|
||
// 时长不足,检查积分
|
||
if (_config.ExemptIntegral > 0)
|
||
{
|
||
if (TryDeductIntegral(steamId, _config.ExemptIntegral))
|
||
{
|
||
isAllowed = true;
|
||
allowReason = $"时长不足({playHours:F1}/{requiredHours}h),已扣除 {_config.ExemptIntegral} 积分抵扣";
|
||
SendRconCommand($"AdminWarn \"{steamId}\" {allowReason}");
|
||
}
|
||
else
|
||
{
|
||
isAllowed = false;
|
||
allowReason = $"时长不足({playHours:F1}/{requiredHours}h) 且积分不足,无法建队";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
isAllowed = false;
|
||
allowReason = $"时长不足({playHours:F1}/{requiredHours}h)";
|
||
}
|
||
}
|
||
|
||
if (isAllowed)
|
||
{
|
||
// 如果启用则广播
|
||
if (_config.RCONOut == true)
|
||
{
|
||
SendRconCommand($"Broadcast 玩家 {playerName} 建立了小队 {squadId} ({squadName}) [{allowReason}]");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 不允许 -> 警告并要求解散
|
||
SendRconCommand($"AdminWarn \"{steamId}\" 禁止建队: {allowReason}。请解散小队!");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理聊天查询命令
|
||
/// </summary>
|
||
/// <param name="chatMsg">聊天消息</param>
|
||
private void HandleChatQuery(ChatMessage chatMsg)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_config.LikeKey)) return;
|
||
|
||
string msg = chatMsg.Message.Trim();
|
||
var keys = _config.LikeKey.Split(new[] { ',', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||
|
||
bool matched = false;
|
||
foreach (var key in keys)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(key)) continue;
|
||
|
||
// 1. 直接匹配
|
||
if (msg.Contains(key, StringComparison.OrdinalIgnoreCase)) { matched = true; break; }
|
||
|
||
// 2. 首字母匹配 (拼音)
|
||
string initials = PinyinHelper.GetInitials(key);
|
||
if (!string.IsNullOrEmpty(initials) && msg.Contains(initials, StringComparison.OrdinalIgnoreCase)) { matched = true; break; }
|
||
|
||
// 3. 全拼匹配
|
||
string pinyin = PinyinHelper.GetPinyin(key);
|
||
if (!string.IsNullOrEmpty(pinyin) && msg.Contains(pinyin, StringComparison.OrdinalIgnoreCase)) { matched = true; break; }
|
||
}
|
||
|
||
if (matched)
|
||
{
|
||
double hours = GetPlayerTotalPlayHours(chatMsg.SteamId);
|
||
int integral = GetPlayerIntegral(chatMsg.SteamId);
|
||
|
||
string reply = $"[查询结果] 您的游戏时长: {hours:F1}小时, 积分: {integral}。建队要求: {_config.MinRunHour}小时" +
|
||
(_config.ExemptIntegral > 0 ? $" 或 {_config.ExemptIntegral}积分" : "");
|
||
|
||
if (_config.ChatOutType.Equals("Broad", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
SendRconCommand($"Broadcast {reply}");
|
||
}
|
||
else if (_config.ChatOutType.Equals("War", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
SendRconCommand($"AdminWarn \"{chatMsg.SteamId}\" {reply}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取玩家总游戏时长(小时)
|
||
/// </summary>
|
||
/// <param name="steamId">玩家SteamID</param>
|
||
/// <returns>游戏时长</returns>
|
||
private double GetPlayerTotalPlayHours(string steamId)
|
||
{
|
||
try
|
||
{
|
||
using (var db = DbHelper.GetInstance())
|
||
{
|
||
// 将连接次数作为粗略估计 (每次0.5小时)
|
||
int count = db.Queryable<Log_NewPlayerConnect>()
|
||
.Where(x => x.SteamID == steamId)
|
||
.Count();
|
||
return count * 0.5;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取玩家积分
|
||
/// </summary>
|
||
/// <param name="steamId">玩家SteamID</param>
|
||
/// <returns>当前积分</returns>
|
||
private int GetPlayerIntegral(string steamId)
|
||
{
|
||
try
|
||
{
|
||
using (var db = DbHelper.GetInstance())
|
||
{
|
||
var user = db.Queryable<UserIntegral>().InSingle(steamId);
|
||
return user?.Points ?? 0;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试扣除积分
|
||
/// </summary>
|
||
/// <param name="steamId">玩家SteamID</param>
|
||
/// <param name="amount">扣除数量</param>
|
||
/// <returns>是否扣除成功</returns>
|
||
private bool TryDeductIntegral(string steamId, int amount)
|
||
{
|
||
try
|
||
{
|
||
using (var db = DbHelper.GetInstance())
|
||
{
|
||
var user = db.Queryable<UserIntegral>().InSingle(steamId);
|
||
if (user != null && user.Points >= amount)
|
||
{
|
||
user.Points -= amount;
|
||
user.LastUpdated = DateTime.UtcNow;
|
||
db.Updateable(user).ExecuteCommand();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] 积分扣除失败: {ex.Message}");
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发送RCON命令
|
||
/// </summary>
|
||
/// <param name="cmd">命令内容</param>
|
||
private async void SendRconCommand(string cmd)
|
||
{
|
||
if (Program.Client != null && Program.Client.IsConnected)
|
||
{
|
||
await Program.Client.SendCommandAsync(cmd);
|
||
}
|
||
}
|
||
}
|
||
|
||
public class SquadCreationAnnounceConfig
|
||
{
|
||
|
||
/// <summary>
|
||
/// 开启此插件
|
||
/// </summary>
|
||
public bool Enabled { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 查询建队时间
|
||
/// </summary>
|
||
public string LikeKey { get; set; } = "建队时间";
|
||
|
||
|
||
/// <summary>
|
||
/// 收到建队推送是否发黄字
|
||
/// </summary>
|
||
public bool RCONOut { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 是否允许手动查询
|
||
/// </summary>
|
||
public bool ChatQuery { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 玩家手动查询时播报方式【Broad黄字】【War警告】
|
||
/// </summary>
|
||
public string ChatOutType { get; set; } = "Broad";
|
||
|
||
/// <summary>
|
||
/// 设置最小建队时长限制 0为关闭功能 不为1时代表建队所需最小时长例如:300
|
||
/// </summary>
|
||
public int MinRunHour { get; set; } = 300;
|
||
|
||
/// <summary>
|
||
/// 不启用的地图关键字(默认seed和巴士拉关闭) 每个地图英文逗号间隔 只需要地图部分名称即可不需要全部名称
|
||
/// </summary>
|
||
public string Ignore { get; set; } = "seed,basrah";
|
||
|
||
/// <summary>
|
||
/// 玩家建队最大尝试次数(防止玩家无限建队导致卡很多白字) 超出次数直接Kick出服务器
|
||
/// </summary>
|
||
public int MaxTry { get; set; } = 10;
|
||
|
||
|
||
/// <summary>
|
||
/// 不满足建队要求时使用积分抵扣【0不开启】【50需要消耗积分数,正整数】
|
||
/// </summary>
|
||
public int ExemptIntegral { get; set; } = 0;
|
||
}
|
||
}
|