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 { /// /// AFK 踢人插件:自动清理长时间未加入小队的玩家 /// 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 { /// /// 是否启用 /// public bool Enabled { get; set; } = false; /// /// 插件启动时广播的消息 (为空则不广播) /// public string StartMsg { get; set; } = "本服务器已启用自动清理挂机插件,请及时加入小队"; /// /// 每次检查间隔周期(秒) /// public int CheckIntervalSeconds { get; set; } = 60; /// /// 二次确认等待时间(秒):发现未入队玩家后,等待多久再次确认 /// public int RecheckDelaySeconds { get; set; } = 60; /// /// 开启人员最小数 /// public int MinServerPlayers { get; set; } = 80; /// /// 每次踢出人数 /// public int MaxKicksPerRun { get; set; } = 3; /// /// 是否对未加入小队的玩家提醒尽快加入 /// public bool WarnBeforeKick { get; set; } = true; /// /// 提醒内容 /// public string WarningMessage { get; set; } = "您尚未加入小队!请尽快加入小队,否则将在 {0} 秒后被踢出。"; /// /// 踢出后显示提示 /// public string KickReason { get; set; } = "AFK / 长时间未加入小队"; /// /// 白名单(永不踢出人员) /// public List WhitelistSteamIds { get; set; } = new List(); } /// /// 获取当前配置 /// /// 配置对象 public object GetConfig() => _config; /// /// 重置配置为默认值 /// public void ResetConfig() { _config = new AfkKickConfig(); } /// /// 更新配置 /// /// JSON 格式的配置元素 public void UpdateConfig(JsonElement config) { try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var newConfig = JsonSerializer.Deserialize(config.GetRawText(), options); if (newConfig != null) { _config = newConfig; } } catch (Exception ex) { RainOpsLog.Log($"[AfkKick] 配置更新失败: {ex.Message}"); } } /// /// 后台任务执行入口,周期性检查挂机玩家 /// /// 取消令牌 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); } } /// /// 检查并踢出挂机玩家的核心逻辑 /// 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} 人"); } } } }