mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-06 21:36:25 +08:00
240 lines
8.5 KiB
C#
240 lines
8.5 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|