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

244 lines
8.7 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.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using RainOpsMini.Helpers;
using RainOpsMini.Helpers.RCON;
using RainOpsMini.Models;
namespace RainOpsMini.Plugins
{
/// <summary>
/// AFK 踢人插件:自动清理长时间未加入小队的玩家
/// </summary>
public class AfkKickPlugin : BackgroundService, IPlugin
{
public string Name => "自动清理挂机";
public string Description => "自动清理挂机(未加入小队)的玩家";
public PluginCategory Category => PluginCategory.BasicFunction;
public bool IsEnabled => _config.Enabled;
private AfkKickConfig _config = new AfkKickConfig();
// 配置模型类
public class AfkKickConfig
{
/// <summary>
/// 是否启用
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// 插件启动时广播的消息 (为空则不广播)
/// </summary>
public string StartMsg { get; set; } = "本服务器已启用自动清理挂机插件,请及时加入小队";
/// <summary>
/// 每次检查间隔周期(秒)
/// </summary>
public int CheckIntervalSeconds { get; set; } = 60;
/// <summary>
/// 二次确认等待时间(秒):发现未入队玩家后,等待多久再次确认
/// </summary>
public int RecheckDelaySeconds { get; set; } = 60;
/// <summary>
/// 开启人员最小数
/// </summary>
public int MinServerPlayers { get; set; } = 80;
/// <summary>
/// 每次踢出人数
/// </summary>
public int MaxKicksPerRun { get; set; } = 3;
/// <summary>
/// 是否对未加入小队的玩家提醒尽快加入
/// </summary>
public bool WarnBeforeKick { get; set; } = true;
/// <summary>
/// 提醒内容
/// </summary>
public string WarningMessage { get; set; } = "您尚未加入小队!请尽快加入小队,否则将在 {0} 秒后被踢出。";
/// <summary>
/// 踢出后显示提示
/// </summary>
public string KickReason { get; set; } = "AFK / 长时间未加入小队";
/// <summary>
/// 白名单(永不踢出人员)
/// </summary>
public List<string> WhitelistSteamIds { get; set; } = new List<string>();
}
/// <summary>
/// 获取当前配置
/// </summary>
/// <returns>配置对象</returns>
public object GetConfig() => _config;
/// <summary>
/// 重置配置为默认值
/// </summary>
public void ResetConfig()
{
_config = new AfkKickConfig();
}
/// <summary>
/// 更新配置
/// </summary>
/// <param name="config">JSON 格式的配置元素</param>
public void UpdateConfig(JsonElement config)
{
try
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var newConfig = JsonSerializer.Deserialize<AfkKickConfig>(config.GetRawText(), options);
if (newConfig != null)
{
_config = newConfig;
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[AfkKick] 配置更新失败: {ex.Message}");
}
}
/// <summary>
/// 后台任务执行入口,周期性检查挂机玩家
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
RainOpsLog.Log($"[{Name}] 插件服务已启动 (检测间隔: {_config.CheckIntervalSeconds}秒)");
// 插件启动时广播消息
if (_config.Enabled && !string.IsNullOrWhiteSpace(_config.StartMsg))
{
// 等待一小会儿确保 RCON 连接就绪(可选,取决于程序启动顺序)
await Task.Delay(5000, stoppingToken);
try
{
if (Program.Client != null && Program.Client.IsConnected)
{
await Program.Client.SendCommandAsync($"Broadcast {_config.StartMsg}");
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[{Name}] 启动广播失败: {ex.Message}");
}
}
while (!stoppingToken.IsCancellationRequested)
{
if (_config.Enabled)
{
try
{
await CheckAndKickAsync();
}
catch (Exception ex)
{
RainOpsLog.Log($"[AfkKick] 错误: {ex.Message}");
}
}
// 获取检查间隔,确保不小于 5 秒以避免高负载
int delay = _config.CheckIntervalSeconds;
if (delay < 5) delay = 5;
await Task.Delay(TimeSpan.FromSeconds(delay), stoppingToken);
}
}
/// <summary>
/// 检查并踢出挂机玩家的核心逻辑
/// </summary>
private async Task CheckAndKickAsync()
{
var client = Program.Client;
if (client == null || !client.IsConnected) return;
// 1. 获取第一轮数据
var players = Program.RconCache.Players.ToList();
var serverInfo = Program.RconCache.ServerInfo;
if (players == null || serverInfo == null) return;
// 检查服务器当前人数是否达到开启阈值
if (int.TryParse(serverInfo.PlayerCount_I, out int count) && count < _config.MinServerPlayers)
{
return;
}
// 识别 AFK/未分配小队的玩家
var targets = players.Where(p =>
(p.TeamId == 0 || p.SquadId == null) && // 未分配阵营 或 未加入小队
!_config.WhitelistSteamIds.Contains(p.SteamId)
).ToList();
if (targets.Count == 0) return;
// 2. 发送警告并等待
if (_config.WarnBeforeKick)
{
foreach (var player in targets)
{
string msg = string.Format(_config.WarningMessage, _config.RecheckDelaySeconds);
await client.SendCommandAsync($"AdminWarn \"{player.SteamId}\" \"{msg}\"");
}
}
// 等待二次确认时间
int waitMs = _config.RecheckDelaySeconds * 1000;
if (waitMs < 1000) waitMs = 1000;
await Task.Delay(waitMs);
// 3. 获取第二轮数据(最新数据)
// 注意:这里需要确保 RCON 缓存已更新,或者强制刷新。
// 建议RecheckDelaySeconds 应该大于 RCON 缓存刷新间隔。
var currentPlayers = Program.RconCache.Players;
if (currentPlayers == null) return; // 无法获取最新数据,放弃本次操作
// 再次筛选
var currentTargets = currentPlayers.Where(p =>
(p.TeamId == 0 || p.SquadId == null) &&
!_config.WhitelistSteamIds.Contains(p.SteamId)
).ToList();
// 4. 计算交集:仍在未入队名单中的玩家
var targetSteamIds = targets.Select(t => t.SteamId).ToHashSet();
var finalKickList = currentTargets.Where(p => targetSteamIds.Contains(p.SteamId)).ToList();
int kickedCount = 0;
foreach (var player in finalKickList)
{
// 限制每次运行的最大踢出人数
if (kickedCount >= _config.MaxKicksPerRun) break;
RainOpsLog.Log($"[AfkKick] 正在踢出 {player.Name} ({player.SteamId}) - 长时间未入队");
await client.SendCommandAsync($"Kick \"{player.SteamId}\" \"{_config.KickReason}\"");
kickedCount++;
}
// 5. 任务执行完毕后广播清理人数
if (kickedCount > 0)
{
await client.SendCommandAsync($"Broadcast 自动清理挂机玩家: {kickedCount} 人");
}
}
}
}