using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using RainOpsMini.Helpers; using RainOpsMini.Helpers.RCON; using RainOpsMini.Services; using RainOpsMini; using RainOpsMini.Models; namespace RainOpsMini.Plugins { public class RedemptionPlugin : IPlugin, IDisposable { public string Name => "积分兑换插件"; public string Description => "允许玩家使用积分兑换VIP或SVIP权限"; public PluginCategory Category => PluginCategory.PointsFunction; public bool IsEnabled => _config.Enabled; private RedemptionConfig _config = new RedemptionConfig(); private readonly string _configPath; public RedemptionPlugin() { _configPath = Path.Combine(Environment.CurrentDirectory, "PluginConfig", "RedemptionConfig.json"); LoadConfig(); Program.OnRconMessageReceived += OnRconMessage; } public object GetConfig() => _config; public void UpdateConfig(JsonElement config) { try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _config = JsonSerializer.Deserialize(config.GetRawText(), options) ?? new RedemptionConfig(); SaveConfig(); } catch (Exception ex) { RainOpsLog.Log($"[RedemptionPlugin] UpdateConfig Error: {ex.Message}"); } } public void ResetConfig() { _config = new RedemptionConfig(); SaveConfig(); } private void LoadConfig() { try { if (File.Exists(_configPath)) { var json = File.ReadAllText(_configPath); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _config = JsonSerializer.Deserialize(json, options) ?? new RedemptionConfig(); } else { SaveConfig(); } } catch (Exception ex) { RainOpsLog.Log($"[RedemptionPlugin] LoadConfig Error: {ex.Message}"); } } private void SaveConfig() { try { var dir = Path.GetDirectoryName(_configPath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var options = new JsonSerializerOptions { WriteIndented = true }; var json = JsonSerializer.Serialize(_config, options); File.WriteAllText(_configPath, json); } catch (Exception ex) { RainOpsLog.Log($"[RedemptionPlugin] SaveConfig Error: {ex.Message}"); } } 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(); // List Command if (content.Equals(_config.ListCommand, StringComparison.OrdinalIgnoreCase)) { await HandleListCommand(chatMsg.SteamId); return; } // Redeem Command if (content.StartsWith(_config.RedeemCommand, StringComparison.OrdinalIgnoreCase)) { string param = content.Substring(_config.RedeemCommand.Length).Trim(); if (int.TryParse(param, out int index)) { await HandleRedeemCommand(chatMsg.SteamId, chatMsg.Name, index); } else if (string.IsNullOrWhiteSpace(param)) { // If user just types "Redeem", maybe show list? Or help? // Let's show list for convenience await HandleListCommand(chatMsg.SteamId); } } } catch (Exception ex) { RainOpsLog.Log($"[RedemptionPlugin] OnRconMessage Error: {ex.Message}"); } } private async Task HandleListCommand(string steamId) { if (_config.Options == null || _config.Options.Count == 0) { await SendWarn(steamId, "当前没有任何可兑换的选项。"); return; } var points = await PointsService.GetPointsAsync(steamId); // 1. 发送标题和积分 await SendWarn(steamId, $"【积分兑换列表】 (当前积分: {points})"); await Task.Delay(200); // 2. 逐条发送选项 for (int i = 0; i < _config.Options.Count; i++) { var opt = _config.Options[i]; await SendWarn(steamId, $"{i + 1}. {opt.Name} - {opt.Cost}积分 ({opt.Description})"); await Task.Delay(200); } // 3. 发送操作提示 await SendWarn(steamId, $"请输入 '{_config.RedeemCommand} + 序号' 进行兑换,例如: {_config.RedeemCommand} 1"); } private async Task HandleRedeemCommand(string steamId, string playerName, int index) { if (_config.Options == null || index < 1 || index > _config.Options.Count) { await SendWarn(steamId, "无效的兑换序号。"); return; } var option = _config.Options[index - 1]; // 1. Check Points int currentPoints = await PointsService.GetPointsAsync(steamId); if (currentPoints < option.Cost) { await SendWarn(steamId, $"积分不足!需要 {option.Cost} 积分,当前拥有 {currentPoints} 积分。"); return; } // 2. Deduct Points await PointsService.AdjustPointsAsync(steamId, -option.Cost); // 3. Apply VIP/SVIP try { ApplyVip(steamId, playerName, option); await SendWarn(steamId, $"兑换成功!消耗 {option.Cost} 积分,获得 {option.Name}。"); RainOpsLog.Log($"[Redemption] Player {playerName}({steamId}) redeemed {option.Name} for {option.Cost} points."); } catch (Exception ex) { // Refund if failed (optional, but good practice) await PointsService.AdjustPointsAsync(steamId, option.Cost); RainOpsLog.Log($"[Redemption] Error applying VIP for {steamId}: {ex.Message}"); await SendWarn(steamId, "兑换发生错误,积分已退回,请联系管理员。"); } } private void ApplyVip(string steamId, string playerName, RedemptionOption option) { var existingVip = VipStorage.GetBySteamId(steamId); DateTime? currentVipExpiry = null; DateTime? currentSvipExpiry = null; string finalGroup = ""; DateTime finalExpiry = DateTime.MinValue; // Load existing expiries if (existingVip != null) { currentVipExpiry = existingVip.VipExpiryDate; currentSvipExpiry = existingVip.SvipExpiryDate; // Migration/Fallback logic similar to CdkService if (currentVipExpiry == null && currentSvipExpiry == null) { // Legacy support or just simple ExpiryDate if (existingVip.Group == "SVIP_RainOpsMini") currentSvipExpiry = existingVip.ExpiryDate; else currentVipExpiry = existingVip.ExpiryDate; } } // Calculate new expiries bool isSvip = option.Group == "SVIP_RainOpsMini"; DateTime now = DateTime.Now; if (isSvip) { if (currentSvipExpiry.HasValue && currentSvipExpiry.Value > now) { currentSvipExpiry = currentSvipExpiry.Value.AddDays(option.DurationDays); } else { currentSvipExpiry = now.AddDays(option.DurationDays); } } else // VIP { if (currentVipExpiry.HasValue && currentVipExpiry.Value > now) { currentVipExpiry = currentVipExpiry.Value.AddDays(option.DurationDays); } else { currentVipExpiry = now.AddDays(option.DurationDays); } } // Determine final group and expiry for the main fields bool hasActiveSvip = currentSvipExpiry.HasValue && currentSvipExpiry.Value > now; bool hasActiveVip = currentVipExpiry.HasValue && currentVipExpiry.Value > now; if (hasActiveSvip) { finalGroup = "SVIP_RainOpsMini"; finalExpiry = currentSvipExpiry.Value; } else if (hasActiveVip) { finalGroup = "VIP_RainOpsMini"; finalExpiry = currentVipExpiry.Value; } else { // Should not happen if we just added time finalGroup = option.Group; finalExpiry = now.AddDays(option.DurationDays); } // Save if (existingVip != null) { existingVip.Group = finalGroup; existingVip.ExpiryDate = finalExpiry; existingVip.VipExpiryDate = currentVipExpiry; existingVip.SvipExpiryDate = currentSvipExpiry; existingVip.Remark = $"积分兑换: {option.Name}"; VipStorage.Update(existingVip); // Should trigger save } else { var newVip = new VipMember { SteamId = steamId, Name = playerName, Group = finalGroup, ExpiryDate = finalExpiry, VipExpiryDate = currentVipExpiry, SvipExpiryDate = currentSvipExpiry, Remark = $"积分兑换: {option.Name}", CreatedAt = DateTime.Now }; VipStorage.Add(newVip); } // Sync to server config try { string cfgPath = System.IO.Path.Combine(Environment.CurrentDirectory, "SquadGame", "ServerConfig", "Admins.cfg"); VipStorage.SyncToAdminConfig(cfgPath); } catch (Exception ex) { RainOpsLog.Log($"[Redemption] Sync Admin Config Failed: {ex.Message}"); } } private async Task SendWarn(string steamId, string message) { if (Program.Client != null && Program.Client.IsConnected) { // 防止消息中的双引号破坏RCON命令结构 message = message.Replace("\"", "'"); // 移除换行符 message = message.Replace("\r", "").Replace("\n", " "); await Program.Client.SendCommandAsync($"AdminWarn \"{steamId}\" \"{message}\""); } } public void Dispose() { Program.OnRconMessageReceived -= OnRconMessage; } } }