mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-06 05:16:26 +08:00
491 lines
16 KiB
C#
491 lines
16 KiB
C#
using Microsoft.Extensions.Hosting;
|
||
using RainOpsMini.Helpers;
|
||
using RainOpsMini.Helpers.RCON;
|
||
using RainOpsMini.Services;
|
||
using RainOpsMini;
|
||
using RainOpsMini.Models;
|
||
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace RainOpsMini.Plugins
|
||
{
|
||
public class RedPacketConfig
|
||
{
|
||
/// <summary>
|
||
/// 是否启用插件
|
||
/// </summary>
|
||
public bool Enabled { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 红包过期时间(秒),默认300秒(5分钟)
|
||
/// </summary>
|
||
public int TimeoutSeconds { get; set; } = 300;
|
||
|
||
/// <summary>
|
||
/// 发红包触发关键字
|
||
/// </summary>
|
||
public string SendKeyword { get; set; } = "发红包";
|
||
|
||
/// <summary>
|
||
/// 抢红包触发关键字
|
||
/// </summary>
|
||
public List<string> GrabKeywords { get; set; } = new List<string> { "抢红包", "抢", "qhb", "QHB" };
|
||
}
|
||
|
||
public class RedPacketSession
|
||
{
|
||
/// <summary>
|
||
/// 发送者SteamID
|
||
/// </summary>
|
||
public string SenderSteamId { get; set; } = "";
|
||
|
||
/// <summary>
|
||
/// 发送者名称
|
||
/// </summary>
|
||
public string SenderName { get; set; } = "";
|
||
|
||
/// <summary>
|
||
/// 红包总积分
|
||
/// </summary>
|
||
public int TotalPoints { get; set; }
|
||
|
||
/// <summary>
|
||
/// 红包总个数
|
||
/// </summary>
|
||
public int TotalCount { get; set; }
|
||
|
||
/// <summary>
|
||
/// 红包开始时间
|
||
/// </summary>
|
||
public DateTime StartTime { get; set; }
|
||
|
||
/// <summary>
|
||
/// 待领取的红包金额栈
|
||
/// </summary>
|
||
public ConcurrentStack<int> Packets { get; set; } = new ConcurrentStack<int>();
|
||
|
||
/// <summary>
|
||
/// 已领取红包的玩家记录 (SteamId -> Points)
|
||
/// </summary>
|
||
public ConcurrentDictionary<string, int> GrabbedPlayers { get; set; } = new ConcurrentDictionary<string, int>();
|
||
|
||
/// <summary>
|
||
/// 剩余红包个数
|
||
/// </summary>
|
||
public int RemainingCount => Packets.Count;
|
||
|
||
/// <summary>
|
||
/// 剩余积分总额
|
||
/// </summary>
|
||
public int RemainingPoints => Packets.Sum();
|
||
}
|
||
|
||
public class RedPacketPlugin : BackgroundService, IPlugin
|
||
{
|
||
public string Name => "红包插件";
|
||
public string Description => "玩家可以发送拼手气红包,其他玩家抢红包";
|
||
public PluginCategory Category => PluginCategory.PointsFunction;
|
||
public bool IsEnabled => _config.Enabled;
|
||
|
||
private RedPacketConfig _config = new RedPacketConfig();
|
||
private RedPacketSession? _currentSession;
|
||
private readonly object _sessionLock = new object();
|
||
private readonly string _configPath;
|
||
|
||
/// <summary>
|
||
/// 构造函数,初始化配置路径并加载配置
|
||
/// </summary>
|
||
public RedPacketPlugin()
|
||
{
|
||
_configPath = Path.Combine(Environment.CurrentDirectory, "PluginConfig", "RedPacketConfig.json");
|
||
LoadConfig();
|
||
Program.OnRconMessageReceived += OnRconMessage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前配置
|
||
/// </summary>
|
||
/// <returns>配置对象</returns>
|
||
public object GetConfig() => _config;
|
||
|
||
/// <summary>
|
||
/// 更新配置
|
||
/// </summary>
|
||
/// <param name="config">JSON配置元素</param>
|
||
public void UpdateConfig(JsonElement config)
|
||
{
|
||
try
|
||
{
|
||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||
_config = JsonSerializer.Deserialize<RedPacketConfig>(config.GetRawText(), options) ?? new RedPacketConfig();
|
||
SaveConfig();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] UpdateConfig Error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置配置为默认值
|
||
/// </summary>
|
||
public void ResetConfig()
|
||
{
|
||
_config = new RedPacketConfig();
|
||
SaveConfig();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// </summary>
|
||
public override void Dispose()
|
||
{
|
||
Program.OnRconMessageReceived -= OnRconMessage;
|
||
base.Dispose();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 后台任务执行入口,定期检查红包过期
|
||
/// </summary>
|
||
/// <param name="stoppingToken">取消令牌</param>
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||
{
|
||
while (!stoppingToken.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
if (_config.Enabled)
|
||
{
|
||
await CheckTimeout();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] Timeout Check Error: {ex.Message}");
|
||
}
|
||
|
||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查红包是否过期
|
||
/// </summary>
|
||
private async Task CheckTimeout()
|
||
{
|
||
RedPacketSession? expiredSession = null;
|
||
|
||
lock (_sessionLock)
|
||
{
|
||
if (_currentSession != null)
|
||
{
|
||
if ((DateTime.Now - _currentSession.StartTime).TotalSeconds >= _config.TimeoutSeconds)
|
||
{
|
||
expiredSession = _currentSession;
|
||
_currentSession = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (expiredSession != null)
|
||
{
|
||
int refundAmount = expiredSession.RemainingPoints;
|
||
if (refundAmount > 0)
|
||
{
|
||
await PointsService.AdjustPointsAsync(expiredSession.SenderSteamId, refundAmount);
|
||
string msg = $"红包已过期,剩余 {refundAmount} 积分已退回给发起者 {expiredSession.SenderName}。";
|
||
await Broadcast(msg);
|
||
RainOpsLog.Log($"[{Name}] Red packet expired. Refunded {refundAmount} to {expiredSession.SenderName} ({expiredSession.SenderSteamId}).");
|
||
}
|
||
else
|
||
{
|
||
await Broadcast("红包已过期,已被抢完。");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理RCON消息
|
||
/// </summary>
|
||
/// <param name="msg">RCON消息内容</param>
|
||
private async void OnRconMessage(string msg)
|
||
{
|
||
if (!_config.Enabled) return;
|
||
|
||
try
|
||
{
|
||
var chatMsg = RconParser.ParseChatMessage(msg);
|
||
if (chatMsg == null) return;
|
||
|
||
string content = chatMsg.Message.Trim();
|
||
|
||
// 检查发红包命令
|
||
if (content.StartsWith(_config.SendKeyword, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
await HandleSendCommand(chatMsg, content);
|
||
return;
|
||
}
|
||
|
||
// 检查抢红包命令
|
||
if (_config.GrabKeywords.Any(k => string.Equals(content, k, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
await HandleGrabCommand(chatMsg);
|
||
return;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] OnRconMessage Error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理发红包命令
|
||
/// </summary>
|
||
/// <param name="chatMsg">聊天消息</param>
|
||
/// <param name="content">消息内容</param>
|
||
private async Task HandleSendCommand(ChatMessage chatMsg, string content)
|
||
{
|
||
// 格式: 发红包 数量 总积分
|
||
var parts = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||
if (parts.Length != 3)
|
||
{
|
||
// 可选:提示格式错误?为了不刷屏,用户只输入“发红包”时不提示
|
||
return;
|
||
}
|
||
|
||
if (!int.TryParse(parts[1], out int count) || !int.TryParse(parts[2], out int totalPoints))
|
||
{
|
||
await SendWarn(chatMsg.SteamId, "格式错误!正确格式:发红包 数量 总积分 (例如:发红包 10 100)");
|
||
return;
|
||
}
|
||
|
||
if (count <= 0 || totalPoints <= 0)
|
||
{
|
||
await SendWarn(chatMsg.SteamId, "数量和积分必须大于0!");
|
||
return;
|
||
}
|
||
|
||
if (totalPoints < count)
|
||
{
|
||
await SendWarn(chatMsg.SteamId, "总积分不能小于红包数量(每人至少1积分)!");
|
||
return;
|
||
}
|
||
|
||
lock (_sessionLock)
|
||
{
|
||
if (_currentSession != null)
|
||
{
|
||
SendWarn(chatMsg.SteamId, "当前已有正在进行的红包活动,请稍后再试!").GetAwaiter();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 检查余额
|
||
int currentPoints = await PointsService.GetPointsAsync(chatMsg.SteamId);
|
||
if (currentPoints < totalPoints)
|
||
{
|
||
await SendWarn(chatMsg.SteamId, $"积分不足!当前积分:{currentPoints}");
|
||
return;
|
||
}
|
||
|
||
// 扣除积分
|
||
await PointsService.AdjustPointsAsync(chatMsg.SteamId, -totalPoints);
|
||
|
||
// 生成红包
|
||
var packets = GenerateRandomPackets(totalPoints, count);
|
||
var session = new RedPacketSession
|
||
{
|
||
SenderSteamId = chatMsg.SteamId,
|
||
SenderName = chatMsg.Name,
|
||
TotalPoints = totalPoints,
|
||
TotalCount = count,
|
||
StartTime = DateTime.Now,
|
||
Packets = new ConcurrentStack<int>(packets)
|
||
};
|
||
|
||
lock (_sessionLock)
|
||
{
|
||
_currentSession = session;
|
||
}
|
||
|
||
RainOpsLog.Log($"[{Name}] {chatMsg.Name} sent red packet: {count} packets, {totalPoints} points.");
|
||
await Broadcast($"土豪 {chatMsg.Name} 发出了 {count} 个拼手气红包,总积分 {totalPoints}!输入 '{_config.GrabKeywords[0]}' 立即开抢!");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理抢红包命令
|
||
/// </summary>
|
||
/// <param name="chatMsg">聊天消息</param>
|
||
private async Task HandleGrabCommand(ChatMessage chatMsg)
|
||
{
|
||
RedPacketSession? session;
|
||
lock (_sessionLock)
|
||
{
|
||
session = _currentSession;
|
||
}
|
||
|
||
if (session == null)
|
||
{
|
||
await SendWarn(chatMsg.SteamId, "当前没有可抢的红包!");
|
||
return;
|
||
}
|
||
|
||
if (session.GrabbedPlayers.ContainsKey(chatMsg.SteamId))
|
||
{
|
||
await SendWarn(chatMsg.SteamId, "你已经抢过这个红包了!");
|
||
return;
|
||
}
|
||
|
||
if (session.Packets.TryPop(out int points))
|
||
{
|
||
if (session.GrabbedPlayers.TryAdd(chatMsg.SteamId, points))
|
||
{
|
||
// 增加积分
|
||
int newTotal = await PointsService.AdjustPointsAsync(chatMsg.SteamId, points);
|
||
|
||
await SendWarn(chatMsg.SteamId, $"抢到了 {points} 积分!当前总积分:{newTotal}");
|
||
RainOpsLog.Log($"[{Name}] {chatMsg.Name} grabbed {points} points.");
|
||
|
||
// 检查是否抢完
|
||
if (session.Packets.IsEmpty)
|
||
{
|
||
FinishSession(session);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
await SendWarn(chatMsg.SteamId, "手慢了,红包已被抢完!");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结束红包会话并结算
|
||
/// </summary>
|
||
/// <param name="session">红包会话对象</param>
|
||
private void FinishSession(RedPacketSession session)
|
||
{
|
||
lock (_sessionLock)
|
||
{
|
||
if (_currentSession == session)
|
||
{
|
||
_currentSession = null;
|
||
}
|
||
}
|
||
|
||
// 寻找运气王
|
||
var luckiest = session.GrabbedPlayers.OrderByDescending(x => x.Value).FirstOrDefault();
|
||
|
||
// 从缓存获取玩家名称以优化显示
|
||
string luckiestName = "未知玩家";
|
||
var player = Program.RconCache.Players.FirstOrDefault(p => p.SteamId == luckiest.Key);
|
||
if (player != null) luckiestName = player.Name;
|
||
|
||
Broadcast($"红包已被抢完!运气王是 {luckiestName},抢到了 {luckiest.Value} 积分!").GetAwaiter();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成随机红包金额
|
||
/// </summary>
|
||
/// <param name="totalPoints">总积分</param>
|
||
/// <param name="count">红包数量</param>
|
||
/// <returns>红包金额列表</returns>
|
||
private List<int> GenerateRandomPackets(int totalPoints, int count)
|
||
{
|
||
// 简单逻辑:
|
||
// 1. 先给每个红包分配1积分
|
||
// 2. 将剩余积分随机分配
|
||
|
||
int[] result = new int[count];
|
||
for (int i = 0; i < count; i++) result[i] = 1;
|
||
|
||
int remaining = totalPoints - count;
|
||
Random rnd = new Random();
|
||
|
||
for (int i = 0; i < remaining; i++)
|
||
{
|
||
int index = rnd.Next(count);
|
||
result[index]++;
|
||
}
|
||
|
||
// 打乱顺序
|
||
return result.OrderBy(x => rnd.Next()).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发送全服广播
|
||
/// </summary>
|
||
/// <param name="message">消息内容</param>
|
||
private async Task Broadcast(string message)
|
||
{
|
||
if (Program.Client != null && Program.Client.IsConnected)
|
||
{
|
||
await Program.Client.SendCommandAsync($"AdminBroadcast \"{message}\"");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发送警告消息给特定玩家
|
||
/// </summary>
|
||
/// <param name="steamId">玩家SteamID</param>
|
||
/// <param name="message">消息内容</param>
|
||
private async Task SendWarn(string steamId, string message)
|
||
{
|
||
if (Program.Client != null && Program.Client.IsConnected)
|
||
{
|
||
await Program.Client.SendCommandAsync($"AdminWarn \"{steamId}\" \"{message}\"");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从文件加载配置
|
||
/// </summary>
|
||
private void LoadConfig()
|
||
{
|
||
if (File.Exists(_configPath))
|
||
{
|
||
try
|
||
{
|
||
string json = File.ReadAllText(_configPath);
|
||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||
_config = JsonSerializer.Deserialize<RedPacketConfig>(json, options) ?? new RedPacketConfig();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] LoadConfig Error: {ex.Message}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
SaveConfig();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存配置到文件
|
||
/// </summary>
|
||
private void SaveConfig()
|
||
{
|
||
try
|
||
{
|
||
string dir = Path.GetDirectoryName(_configPath);
|
||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||
|
||
string json = JsonSerializer.Serialize(_config, new JsonSerializerOptions { WriteIndented = true });
|
||
File.WriteAllText(_configPath, json);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
RainOpsLog.Log($"[{Name}] SaveConfig Error: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|