mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-11 15:25:33 +08:00
Initial commit: add RainOpsMini project and documentation
This commit is contained in:
490
Plugins/RedPacketPlugin.cs
Normal file
490
Plugins/RedPacketPlugin.cs
Normal file
@@ -0,0 +1,490 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user