mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-06 05:16:26 +08:00
244 lines
8.7 KiB
C#
244 lines
8.7 KiB
C#
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} 人");
|
||
}
|
||
}
|
||
}
|
||
}
|