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

240 lines
8.5 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.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using RainOpsMini;
using RainOpsMini.Helpers;
using RainOpsMini.Helpers.RCON;
using RainOpsMini.Models;
namespace RainOpsMini.Plugins
{
/// <summary>
/// 自动队伍平衡插件
/// </summary>
public class TeamBalancePlugin : BackgroundService, IPlugin
{
/// <summary>
/// 插件名称
/// </summary>
public string Name => "自动队伍平衡";
/// <summary>
/// 插件描述
/// </summary>
public string Description => "当两队人数差大于设定值时,自动将优势方最后加入的玩家切换到劣势方";
/// <summary>
/// 插件分类
/// </summary>
public PluginCategory Category => PluginCategory.BasicFunction;
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnabled => _config.Enabled;
private TeamBalanceConfig _config = new TeamBalanceConfig();
private readonly Dictionary<string, DateTime> _playerJoinTimes = new Dictionary<string, DateTime>();
// 冷却时间,防止频繁切换或反复切换同一人
private DateTime _lastSwitchTime = DateTime.MinValue;
/// <summary>
/// 自动队伍平衡配置类
/// </summary>
public class TeamBalanceConfig
{
/// <summary>
/// 是否启用插件
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// 检查间隔(秒)
/// </summary>
public int CheckIntervalSeconds { get; set; } = 30;
/// <summary>
/// 触发平衡的人数差阈值
/// </summary>
public int PlayerCountDifferenceThreshold { get; set; } = 3;
/// <summary>
/// 白名单不被自动切换的玩家SteamID
/// </summary>
public List<string> WhitelistSteamIds { get; set; } = new List<string>();
/// <summary>
/// 切换前是否发送警告/通知
/// </summary>
public bool BroadcastOnSwitch { get; set; } = true;
/// <summary>
/// 切换时的通知消息
/// </summary>
public string SwitchMessage { get; set; } = "由于队伍人数不平衡,您已被自动切换到另一方。";
}
/// <summary>
/// 获取当前配置
/// </summary>
/// <returns>配置对象</returns>
public object GetConfig() => _config;
/// <summary>
/// 重置配置为默认值
/// </summary>
public void ResetConfig()
{
_config = new TeamBalanceConfig();
}
/// <summary>
/// 更新配置
/// </summary>
/// <param name="config">JSON配置元素</param>
public void UpdateConfig(JsonElement config)
{
try
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true
};
_config = JsonSerializer.Deserialize<TeamBalanceConfig>(config.GetRawText(), options) ?? new TeamBalanceConfig();
}
catch (Exception ex)
{
RainOpsLog.Log($"[TeamBalance] 配置更新失败: {ex.Message}");
}
}
/// <summary>
/// 后台任务执行方法
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (_config.Enabled && Program.Client != null && Program.Client.IsConnected)
{
await CheckAndBalanceTeams();
}
}
catch (Exception ex)
{
RainOpsLog.Log($"[TeamBalance] Error: {ex.Message}");
}
await Task.Delay(TimeSpan.FromSeconds(_config.CheckIntervalSeconds), stoppingToken);
}
}
/// <summary>
/// 检查并平衡队伍
/// </summary>
private async Task CheckAndBalanceTeams()
{
// Rule 4: 完全拷贝一份数据
List<PlayerInfo> players;
lock (Program.RconCache) // 这里的lock可能不是必须的因为RconCache.Players也是替换操作但为了安全起见
{
var cachePlayers = Program.RconCache.Players;
if (cachePlayers == null) return;
players = new List<PlayerInfo>(cachePlayers);
}
if (players.Count == 0) return;
// 更新加入时间
var currentSteamIds = players.Select(p => p.SteamId).ToHashSet();
// 移除离开的玩家
var leftPlayers = _playerJoinTimes.Keys.Where(k => !currentSteamIds.Contains(k)).ToList();
foreach (var left in leftPlayers)
{
_playerJoinTimes.Remove(left);
}
// 添加新玩家
foreach (var p in players)
{
if (!_playerJoinTimes.ContainsKey(p.SteamId))
{
_playerJoinTimes[p.SteamId] = DateTime.Now;
}
}
// 计算队伍人数
var team1 = players.Where(p => p.TeamId == 1).ToList();
var team2 = players.Where(p => p.TeamId == 2).ToList();
int count1 = team1.Count;
int count2 = team2.Count;
int diff = Math.Abs(count1 - count2);
if (diff > _config.PlayerCountDifferenceThreshold)
{
// 确定人数较多的队伍
int largerTeamId = count1 > count2 ? 1 : 2;
int smallerTeamId = count1 > count2 ? 2 : 1;
var largerTeamPlayers = count1 > count2 ? team1 : team2;
// 寻找待切换的候选人
// 过滤白名单
var candidates = largerTeamPlayers
.Where(p => !_config.WhitelistSteamIds.Contains(p.SteamId))
.ToList();
if (candidates.Count > 0)
{
// 按加入时间倒序排序(最新加入的优先)
// 如果加入时间相同如初始加载使用ID倒序作为打破平局的依据假设ID越大越新
var target = candidates
.OrderByDescending(p => _playerJoinTimes.TryGetValue(p.SteamId, out var time) ? time : DateTime.MinValue)
.ThenByDescending(p => p.Id)
.FirstOrDefault();
if (target != null)
{
await PerformSwitch(target, smallerTeamId);
}
}
}
}
/// <summary>
/// 执行玩家切换队伍操作
/// </summary>
/// <param name="player">目标玩家</param>
/// <param name="targetTeamId">目标队伍ID</param>
private async Task PerformSwitch(PlayerInfo player, int targetTeamId)
{
if (_config.BroadcastOnSwitch)
{
// 通知玩家
// AdminWarn <NameOrSteamId> <Reason>
string warnCmd = $"AdminWarn \"{player.SteamId}\" {_config.SwitchMessage}";
await Program.Client.SendCommandAsync(warnCmd);
// 可选:全服广播
// string broadcastCmd = $"AdminBroadcast balanced {player.Name} to Team {targetTeamId}";
// await Program.Client.SendCommandAsync(broadcastCmd);
}
// 执行切换命令
// Command: AdminForceTeamChange <NameOrSteamId>
// 使用 SteamId 更安全
string command = $"AdminForceTeamChange \"{player.SteamId}\"";
RainOpsLog.Log($"[TeamBalance] Switching player {player.Name} ({player.SteamId}) to Team {targetTeamId}");
await Program.Client.SendCommandAsync(command);
}
}
}