Иконка ресурса

FREE EasyVoteExtended : 3.3.2 3.3.2

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Facepunch.Extend;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("Easy Vote Extended", "dFxPhoeniX & TimRS", "3.3.2")]
    [Description("The best Rust server voting system")]
    public class EasyVoteExtended : RustPlugin {
        ////////////////////////////////////////////////////////////
        // Runtime State
        ////////////////////////////////////////////////////////////
        private readonly HashSet<string> pendingStatusChecks = new HashSet<string>();
        private readonly Dictionary<string, string> statusRequestTokens = new Dictionary<string, string>();
        private readonly Dictionary<ulong, string> scheduledVoteChecks = new Dictionary<ulong, string>();
        private readonly HashSet<string> reportedConfigurationWarnings = new HashSet<string>();
        private readonly Queue<Action> claimRequestQueue = new Queue<Action>();
        private readonly Queue<Action> voteRequestQueue = new Queue<Action>();
        private readonly Dictionary<string, float> commandCooldowns = new Dictionary<string, float>();
        private readonly Dictionary<string, string> scheduledClaimRetries = new Dictionary<string, string>();
        private Dictionary<string, List<string>> pendingRewardCommands = new Dictionary<string, List<string>>();
        private Dictionary<string, PendingClaimTransaction> pendingClaimTransactions = new Dictionary<string, PendingClaimTransaction>();
        private readonly Dictionary<string, string> activeClaimRequests = new Dictionary<string, string>();
        private readonly HashSet<ulong> finalizingPlayers = new HashSet<ulong>();
        private readonly HashSet<ulong> deliveringPlayers = new HashSet<ulong>();
        private readonly Dictionary<string, long> storageWarningTimes = new Dictionary<string, long>();
        private readonly Queue<DiscordNotification> discordQueue = new Queue<DiscordNotification>();
        private int runtimeGeneration;
        private bool configurationLoaded;
        private bool dataLoaded;
        private bool unloading;
        private bool resetInProgress;
        private bool wipeResetRequested;
        private bool debugEnabled;
        private bool claimsDirty;
        private VoteResetJournal pendingResetJournal;
        private string discordRequestToken;
        private long discordNextAttemptUtcTicks;
        private int consecutiveClaimRequests;
        ////////////////////////////////////////////////////////////
        // Constants
        ////////////////////////////////////////////////////////////
        private const int CurrentConfigurationVersion = 1;
        private const int DefaultAutomaticVoteCheckInterval = 300;
        private const int DefaultVoteFollowUpCheckDelay = 60;
        private const float VoteRequestSpacing = 0.2f;
        private const float ManualCommandCooldown = 10f;
        private const float VoteApiRequestTimeout = 15f;
        private const int MaximumClaimAttempts = 6;
        private const string PendingRewardsDataFileName = "EasyVoteExtended_PendingRewards";
        private const string PendingClaimsDataFileName = "EasyVoteExtended_PendingClaims";
        private const string PendingRewardTransactionPrefix = "__EVE_TX__:";
        private const string ClaimStateCreated = "Created";
        private const string ClaimStateSent = "Sent";
        private const string ClaimStateUncertain = "Uncertain";
        private const string ClaimStateConfirmed = "Confirmed";
        private const string ClaimStateManualReview = "ManualReview";
        private const int MaximumCommandsPerDelivery = 32;
        private const float LocalProcessingInterval = 5f;
        private const string ClaimStateClosed = "Closed";
        private const string ResetJournalDataFileName = "EasyVoteExtended_ResetJournal";
        ////////////////////////////////////////////////////////////
        // Models
        ////////////////////////////////////////////////////////////
        private enum RewardDeliveryResult {
            None, Pending, Partial, Dispatched, Failed
        }
        private class VoteResetJournal {
            public string OperationId;
            public string SteamId;
            public bool AllPlayers;
        }
        private class DiscordNotification {
            public string Url;
            public string Body;
            public int Attempts;
        }
        private class PendingClaimTransaction {
            public string TransactionId;
            public ulong PlayerId;
            public string PlayerName;
            public string ServerName;
            public string Site;
            public bool NotifyPlayer;
            public int Attempts;
            public string State;
            public bool HadAmbiguousFailure;
            public int TargetVoteCount;
            public int RecoveryAttempts;
            public long NextAttemptUtcTicks;
            public long CreatedUtcTicks;
            public bool NeedsStatusCheck;
            public List<string> RewardCommands;
            public string ReviewReason;
        }
        ////////////////////////////////////////////////////////////
        // Oxide Hooks
        ////////////////////////////////////////////////////////////
        private void Init() {
            if (!configurationLoaded || _config == null) {
                ConsoleError("The plugin is paused because its configuration could not be loaded. Repair the original file and reload the plugin.");
                return;
            }
            LoadMessages();
            debugEnabled = _config.DebugSettings[ConfigDefaultKeys.DebugEnabled].ToBool();
            if (_config.DebugSettings[ConfigDefaultKeys.VerboseDebugEnabled].ToBool()) {
                ConsoleWarn("Verbose debugging overrides voting API responses. Do not enable it on a production server.");
            }
            dataLoaded = LoadVoteData();
            dataLoaded &= LoadPendingRewardsData();
            dataLoaded &= LoadPendingClaimsData();
            if (!dataLoaded) {
                ConsoleError("Voting is paused to protect unreadable data. Original files were not overwritten. Repair the files and reload the plugin.");
                return;
            }
            if (!RecoverResetJournal()) {
                return;
            }
            if (wipeResetRequested) {
                wipeResetRequested = false;
                ResetAllVoteData();
            }
        }
        private void OnServerInitialized() {
            if (!configurationLoaded || !dataLoaded) {
                return;
            }
            ConsoleLog(CanProcessVotes() ? "Easy Vote Extended has been initialized..." : "Easy Vote Extended is waiting for its pending reset to finish safely.");
            StartRequestQueueProcessor();
            StartAutomaticVoteChecks();
            timer.Every(LocalProcessingInterval, ProcessLocalPendingWork);
            timer.Every(1f, ProcessDiscordQueue);
            RecoverPendingClaimTransactions();
            timer.Once(2f, () => {
                if (!CanProcessVotes()) {
                    return;
                }
                foreach (BasePlayer player in BasePlayer.activePlayerList.ToArray()) {
                    if (player == null || !player.IsConnected) {
                        continue;
                    }
                    CheckIfPlayerDataExists(player);
                    DeliverPendingRewards(player);
                    CheckVotingStatus(player, false);
                }
            }
            );
        }
        private void Unload() {
            unloading = true;
            activeClaimRequests.Clear();
            pendingStatusChecks.Clear();
            statusRequestTokens.Clear();
            scheduledVoteChecks.Clear();
            reportedConfigurationWarnings.Clear();
            claimRequestQueue.Clear();
            voteRequestQueue.Clear();
            commandCooldowns.Clear();
            scheduledClaimRetries.Clear();
            discordQueue.Clear();
            discordRequestToken = null;
            // Preserve a reset intent if storage recovered just before unloading.
            if (dataLoaded && pendingResetJournal != null) {
                TryWriteJson(GetDataPath(ResetJournalDataFileName), pendingResetJournal);
            }
            // Do not overwrite unreadable files or a reset that still needs recovery.
            if (dataLoaded && !resetInProgress && claimsDirty) {
                SavePendingClaimsData();
            }
        }
        private void OnNewSave(string filename) {
            if (!configurationLoaded || !_config.PluginSettings[ConfigDefaultKeys.ClearRewardsOnWipe].ToBool()) {
                return;
            }
            if (!dataLoaded || resetInProgress) {
                wipeResetRequested = true;
                return;
            }
            ConsoleLog("New map data detected. Resetting stored votes and pending rewards.");
            ResetAllVoteData();
        }
        private void OnPlayerConnected(BasePlayer player) {
            if (!CanProcessVotes() || player == null) {
                return;
            }
            CheckIfPlayerDataExists(player);
            ulong connectedPlayerId = player.userID;
            timer.Once(2f, () => {
                if (!CanProcessVotes()) {
                    return;
                }
                BasePlayer connectedPlayer = BasePlayer.FindByID(connectedPlayerId);
                if (connectedPlayer != null && connectedPlayer.IsConnected) {
                    DeliverPendingRewards(connectedPlayer);
                }
            }
            );
            bool checkOnSleepEnded = _config.NotificationSettings[ConfigDefaultKeys.OnPlayerSleepEnded].ToBool();
            bool notifyOnConnected = _config.NotificationSettings[ConfigDefaultKeys.OnPlayerConnected].ToBool();
            if (!checkOnSleepEnded) {
                CheckVotingStatus(player, notifyOnConnected);
            }
        }
        private void OnPlayerDisconnected(BasePlayer player, string reason) {
            if (player == null) {
                return;
            }
            commandCooldowns.Remove($"{player.UserIDString}:vote");
            commandCooldowns.Remove($"{player.UserIDString}:claim");
            scheduledVoteChecks.Remove(player.userID);
        }
        private void OnPlayerSleepEnded(BasePlayer player) {
            if (!CanProcessVotes() || player == null) {
                return;
            }
            DeliverPendingRewards(player);
            if (_config.NotificationSettings[ConfigDefaultKeys.OnPlayerSleepEnded].ToBool()) {
                CheckVotingStatus(player);
            }
        }
        ////////////////////////////////////////////////////////////
        // General Methods
        ////////////////////////////////////////////////////////////
        private void HandleClaimWebRequestCallback(int code, string response, string pendingClaimKey, string transactionId, string requestToken) {
            PendingClaimTransaction transaction;
            if (!ConsumeClaimRequest(pendingClaimKey, transactionId, requestToken, out transaction)) {
                return;
            }
            if (debugEnabled) {
                _Debug($"Claim response: transaction={transaction.TransactionId}, player={transaction.PlayerId}, site={transaction.Site}, HTTP={code}.");
            }
            if (code != 200) {
                FailClaimRequest(pendingClaimKey, transaction, $"Claim HTTP status {code}.");
                return;
            }
            response = response?.Trim();
            if (_config.DebugSettings[ConfigDefaultKeys.VerboseDebugEnabled].ToBool()) {
                response = _config.DebugSettings[ConfigDefaultKeys.ClaimAPIRepsonseCode];
            }
            if (response == "1") {
                ConfirmClaimTransaction(pendingClaimKey, transaction);
                return;
            }
            if (response == "2" && transaction.HadAmbiguousFailure) {
                // This recovery assumes only one consumer claims this external vote.
                ConfirmClaimTransaction(pendingClaimKey, transaction);
                return;
            }
            if (response == "0" && transaction.HadAmbiguousFailure) {
                MarkClaimForReview(pendingClaimKey, transaction, "The claim returned 0 after an ambiguous request. Verify the provider before granting or discarding it.");
                return;
            }
            if (response == "0" || response == "2") {
                if (!CloseClaimTransaction(pendingClaimKey, transaction)) {
                    return;
                }
                BasePlayer player = FindPlayer(transaction.PlayerId);
                if (transaction.NotifyPlayer && player != null && player.IsConnected) {
                    string messageKey = response == "2" ? "AlreadyVoted" : "ClaimStatus";
                    player.ChatMessage(_lang(messageKey, player.UserIDString, _config.PluginSettings[ConfigDefaultKeys.Prefix], transaction.Site, transaction.ServerName));
                }
                return;
            }
            FailClaimRequest(pendingClaimKey, transaction, "Unexpected claim response; expected plain 0, 1 or 2.");
        }
        private bool TryGetActiveClaimTransaction(string pendingClaimKey, string transactionId, out PendingClaimTransaction transaction) {
            transaction = null;
            return CanProcessVotes() && !string.IsNullOrEmpty(transactionId) && pendingClaimTransactions.TryGetValue(pendingClaimKey, out transaction) && transaction != null && string.Equals(transaction.TransactionId, transactionId, StringComparison.Ordinal);
        }
        private void ConfirmClaimTransaction(string pendingClaimKey, PendingClaimTransaction transaction) {
            PendingClaimTransaction current;
            if (transaction == null || !TryGetActiveClaimTransaction(pendingClaimKey, transaction.TransactionId, out current) || !ReferenceEquals(current, transaction)) {
                return;
            }
            if (transaction.TargetVoteCount <= 0) {
                int highestVoteCount = GetStoredVoteCount(transaction.PlayerId.ToString());
                foreach (PendingClaimTransaction pending in pendingClaimTransactions.Values) {
                    if (pending != null && pending.PlayerId == transaction.PlayerId && pending.State == ClaimStateConfirmed) {
                        highestVoteCount = Math.Max(highestVoteCount, pending.TargetVoteCount);
                    }
                }
                if (highestVoteCount == int.MaxValue) {
                    MarkClaimForReview(pendingClaimKey, transaction, "The vote counter has reached its maximum value. Correct the counter before resolving this claim.");
                    return;
                }
                transaction.TargetVoteCount = highestVoteCount + 1;
            }
            if (transaction.RewardCommands == null) {
                string playerName = GetResolvedPlayerName(FindPlayer(transaction.PlayerId), transaction.PlayerName, transaction.PlayerId.ToString());
                transaction.RewardCommands = BuildRewardCommands(transaction.PlayerId, playerName, transaction.TargetVoteCount, _config.PluginSettings[ConfigDefaultKeys.RewardIsCumulative].ToBool());
            }
            transaction.State = ClaimStateConfirmed;
            transaction.NeedsStatusCheck = false;
            transaction.NextAttemptUtcTicks = 0;
            if (!SavePendingClaimsData()) {
                ScheduleClaimRetry(pendingClaimKey);
                return;
            }
            if (debugEnabled) {
                _Debug($"Claim confirmed: transaction={transaction.TransactionId}, player={transaction.PlayerId}, target votes={transaction.TargetVoteCount}.");
            }
            FinalizeConfirmedClaimTransaction(pendingClaimKey, transaction.TransactionId);
        }
        private void FinalizeConfirmedClaimTransaction(string pendingClaimKey, string transactionId) {
            PendingClaimTransaction transaction;
            if (!TryGetActiveClaimTransaction(pendingClaimKey, transactionId, out transaction) || transaction.State != ClaimStateConfirmed) {
                return;
            }
            if (transaction.NextAttemptUtcTicks > DateTime.UtcNow.Ticks || !finalizingPlayers.Add(transaction.PlayerId)) {
                return;
            }
            try {
                if (pendingClaimTransactions.Values.Any(other => other != null && !ReferenceEquals(other, transaction) && other.PlayerId == transaction.PlayerId && other.State == ClaimStateConfirmed && other.TargetVoteCount > 0 && other.TargetVoteCount < transaction.TargetVoteCount)) {
                    ScheduleClaimRetry(pendingClaimKey);
                    return;
                }
                if (transaction.TargetVoteCount <= 0 || transaction.RewardCommands == null) {
                    finalizingPlayers.Remove(transaction.PlayerId);
                    ConfirmClaimTransaction(pendingClaimKey, transaction);
                    return;
                }
                // Persist confirmation before changing the counter or preparing delivery.
                if (claimsDirty && !SavePendingClaimsData()) {
                    ScheduleClaimRetry(pendingClaimKey);
                    return;
                }
                string steamId = transaction.PlayerId.ToString();
                int currentVoteCount = GetStoredVoteCount(steamId);
                if (currentVoteCount < transaction.TargetVoteCount && !TrySetStoredVoteCount(steamId, transaction.TargetVoteCount)) {
                    ScheduleClaimRetry(pendingClaimKey);
                    return;
                }
                bool rewardConfigured = transaction.RewardCommands.Any(command => !string.IsNullOrWhiteSpace(command));
                if (rewardConfigured && !QueuePendingRewardTransaction(steamId, transaction.TransactionId, transaction.RewardCommands)) {
                    ScheduleClaimRetry(pendingClaimKey);
                    return;
                }
                // Delivery remains locked while this confirmed transaction exists.
                if (!RemovePendingClaimTransaction(pendingClaimKey)) {
                    ScheduleClaimRetry(pendingClaimKey);
                    return;
                }
                BasePlayer player = FindPlayer(transaction.PlayerId);
                string playerName = GetResolvedPlayerName(player, transaction.PlayerName, steamId);
                int generation = runtimeGeneration;
                RewardDeliveryResult deliveryResult = rewardConfigured ? DeliverPendingRewards(player, false) : RewardDeliveryResult.None;
                if (!CanProcessVotes() || generation != runtimeGeneration) {
                    return;
                }
                bool dispatched = deliveryResult == RewardDeliveryResult.Dispatched;
                bool dispatchFailed = deliveryResult == RewardDeliveryResult.Failed;
                string displayedVoteCount = GetStoredVoteCount(steamId).ToString();
                if (player != null && player.IsConnected) {
                    string messageKey = !rewardConfigured ? "ThankYouNoReward" : (dispatchFailed ? "ThankYouDispatchFailed" : (dispatched ? "ThankYou" : "ThankYouPending"));
                    player.ChatMessage(_lang(messageKey, player.UserIDString, _config.PluginSettings[ConfigDefaultKeys.Prefix], displayedVoteCount, transaction.Site));
                }
                if (_config.Discord[ConfigDefaultKeys.DiscordEnabled].ToBool()) {
                    string messageKey = !rewardConfigured ? "DiscordWebhookMessageNoReward" : (dispatchFailed ? "DiscordWebhookMessageFailed" : (dispatched ? "DiscordWebhookMessage" : "DiscordWebhookMessagePending"));
                    DiscordSendMessage(_lang(messageKey, null, playerName, transaction.ServerName, transaction.Site));
                }
                if (_config.NotificationSettings[ConfigDefaultKeys.GlobalChatAnnouncements].ToBool()) {
                    string messageKey = !rewardConfigured ? "GlobalChatAnnouncementsNoReward" : (dispatchFailed ? "GlobalChatAnnouncementsFailed" : (dispatched ? "GlobalChatAnnouncements" : "GlobalChatAnnouncementsPending"));
                    foreach (BasePlayer recipient in BasePlayer.activePlayerList.ToArray()) {
                        if (recipient != null && recipient.IsConnected) {
                            recipient.ChatMessage(_lang(messageKey, recipient.UserIDString, _config.PluginSettings[ConfigDefaultKeys.Prefix], playerName, displayedVoteCount));
                        }
                    }
                }
            }
            finally {
                finalizingPlayers.Remove(transaction.PlayerId);
            }
        }
        private void HandleStatusWebRequestCallback(int code, string response, ulong playerId, string playerName, string serverName, string site, bool notifyPlayer, string pendingStatusKey, string statusRequestToken) {
            if (!TryConsumeStatusRequest(pendingStatusKey, statusRequestToken)) {
                return;
            }
            if (debugEnabled) {
                _Debug($"Vote status response: player={playerId}, server={serverName}, site={site}, HTTP={code}.");
            }
            if (code != 200) {
                ConsoleWarn($"Vote status request failed for {playerId} on {site} ({serverName}): HTTP {code}. Check the provider, connectivity and configured credentials.");
                return;
            }
            response = response?.Trim();
            if (_config.DebugSettings[ConfigDefaultKeys.VerboseDebugEnabled].ToBool()) {
                response = _config.DebugSettings[ConfigDefaultKeys.CheckAPIResponseCode];
            }
            string pendingClaimKey = GetPendingClaimKey(playerId, serverName, site);
            if (pendingClaimTransactions.ContainsKey(pendingClaimKey)) {
                return;
            }
            if (response == "1") {
                // Keep the exact username used by this status request for its claim.
                EnqueueClaimRequest(playerId, playerName, serverName, site, notifyPlayer);
                return;
            }
            BasePlayer player = FindPlayer(playerId);
            if (response == "0" || response == "2") {
                if (notifyPlayer && player != null && player.IsConnected) {
                    if (response == "0") {
                        player.ChatMessage(_lang("NoRewards", player.UserIDString, _config.PluginSettings[ConfigDefaultKeys.Prefix], serverName, site));
                    }
                    else {
                        player.ChatMessage(_lang("AlreadyVoted", player.UserIDString, _config.PluginSettings[ConfigDefaultKeys.Prefix], site));
                    }
                }
                return;
            }
            ConsoleWarn($"Unexpected status response from {site} for {playerId} on {serverName}. Expected plain 0, 1 or 2.");
        }
        private bool IsActiveStatusRequest(string pendingStatusKey, string statusRequestToken) {
            string activeToken;
            return CanProcessVotes() && !string.IsNullOrEmpty(statusRequestToken) && statusRequestTokens.TryGetValue(pendingStatusKey, out activeToken) && string.Equals(activeToken, statusRequestToken, StringComparison.Ordinal);
        }
        private bool TryConsumeStatusRequest(string pendingStatusKey, string statusRequestToken) {
            if (!IsActiveStatusRequest(pendingStatusKey, statusRequestToken)) {
                return false;
            }
            statusRequestTokens.Remove(pendingStatusKey);
            pendingStatusChecks.Remove(pendingStatusKey);
            return true;
        }
        private void CancelStatusRequest(string pendingStatusKey, string statusRequestToken) {
            if (!IsActiveStatusRequest(pendingStatusKey, statusRequestToken)) {
                return;
            }
            statusRequestTokens.Remove(pendingStatusKey);
            pendingStatusChecks.Remove(pendingStatusKey);
        }
        private void CancelStatusRequestsForPlayer(string steamId) {
            if (string.IsNullOrEmpty(steamId)) {
                return;
            }
            foreach (string pendingStatusKey in statusRequestTokens.Keys.Where(key => key.StartsWith(steamId + ":", StringComparison.Ordinal)).ToList()) {
                statusRequestTokens.Remove(pendingStatusKey);
                pendingStatusChecks.Remove(pendingStatusKey);
            }
        }
        private void EnqueueClaimRequest(ulong playerId, string playerName, string serverName, string site, bool notifyPlayer) {
            if (!CanProcessVotes()) {
                return;
            }
            string pendingClaimKey = GetPendingClaimKey(playerId, serverName, site);
            PendingClaimTransaction transaction;
            if (!pendingClaimTransactions.TryGetValue(pendingClaimKey, out transaction) || transaction == null) {
                transaction = new PendingClaimTransaction {
                    TransactionId = Guid.NewGuid().ToString("N"), PlayerId = playerId, PlayerName = playerName, ServerName = serverName, Site = site, NotifyPlayer = notifyPlayer, State = ClaimStateCreated, CreatedUtcTicks = DateTime.UtcNow.Ticks
                }
                ;
                pendingClaimTransactions[pendingClaimKey] = transaction;
            }
            else {
                transaction.NotifyPlayer |= notifyPlayer;
            }
            if (!SavePendingClaimsData()) {
                ScheduleClaimRetry(pendingClaimKey);
                return;
            }
            QueuePendingClaimTransaction(pendingClaimKey);
        }
        private void QueuePendingClaimTransaction(string pendingClaimKey) {
            PendingClaimTransaction transaction;
            if (!CanProcessVotes() || !pendingClaimTransactions.TryGetValue(pendingClaimKey, out transaction) || transaction == null) {
                return;
            }
            if (activeClaimRequests.ContainsKey(pendingClaimKey)) {
                return;
            }
            if (transaction.NextAttemptUtcTicks > DateTime.UtcNow.Ticks) {
                ScheduleClaimRetry(pendingClaimKey);
                return;
            }
            if (transaction.State == ClaimStateConfirmed) {
                FinalizeConfirmedClaimTransaction(pendingClaimKey, transaction.TransactionId);
                return;
            }
            if (transaction.State == ClaimStateClosed) {
                if (!SavePendingClaimsData() || !RemovePendingClaimTransaction(pendingClaimKey)) {
                    ScheduleClaimRetry(pendingClaimKey);
                }
                return;
            }
            if (transaction.State == ClaimStateManualReview) {
                return;
            }
            bool reconcile = transaction.NeedsStatusCheck || transaction.State == ClaimStateUncertain;
            int attempts = reconcile ? transaction.RecoveryAttempts : transaction.Attempts;
            if (attempts >= MaximumClaimAttempts) {
                MarkClaimForReview(pendingClaimKey, transaction, "The retry limit was reached. The transaction has been preserved for manual review.");
                return;
            }
            string requestUrl;
            if (!TryBuildTransactionUrl(transaction, reconcile ? ConfigDefaultKeys.apiStatus : ConfigDefaultKeys.apiClaim, out requestUrl)) {
                MarkClaimForReview(pendingClaimKey, transaction, "The configured provider or credentials could not be used to build the request URL.");
                return;
            }
            string transactionId = transaction.TransactionId;
            string requestToken = Guid.NewGuid().ToString("N");
            activeClaimRequests[pendingClaimKey] = requestToken;
            scheduledClaimRetries.Remove(pendingClaimKey);
            QueueVoteRequest(() => {
                PendingClaimTransaction current;
                string activeToken;
                if (!TryGetActiveClaimTransaction(pendingClaimKey, transactionId, out current) || !activeClaimRequests.TryGetValue(pendingClaimKey, out activeToken) || activeToken != requestToken) {
                    ReleaseClaimRequest(pendingClaimKey, requestToken);
                    return;
                }
                string previousState = current.State;
                if (reconcile) {
                    current.RecoveryAttempts++;
                }
                else {
                    current.Attempts++;
                }
                current.State = ClaimStateSent;
                current.NextAttemptUtcTicks = 0;
                if (!SavePendingClaimsData()) {
                    if (reconcile) {
                        current.RecoveryAttempts--;
                    }
                    else {
                        current.Attempts--;
                    }
                    current.State = previousState;
                    ReleaseClaimRequest(pendingClaimKey, requestToken);
                    ScheduleClaimRetry(pendingClaimKey);
                    return;
                }
                if (debugEnabled) {
                    _Debug($"Sending {(reconcile ? "recovery status" : "claim")} request: transaction={transactionId}, player={current.PlayerId}, site={current.Site}, attempt={(reconcile ? current.RecoveryAttempts : current.Attempts)}.");
                }
                Action<int, string> callback = (code, response) => {
                    if (reconcile) {
                        HandleClaimReconciliation(code, response, pendingClaimKey, transactionId, requestToken);
                    }
                    else {
                        HandleClaimWebRequestCallback(code, response, pendingClaimKey, transactionId, requestToken);
                    }
                }
                ;
                try {
                    webrequest.Enqueue(requestUrl, null, (code, response) => NextTick(() => callback(code, response)), this, RequestMethod.GET, null, VoteApiRequestTimeout);
                    timer.Once(VoteApiRequestTimeout + 5f, () => callback(-1, null));
                }
                catch (Exception) {
                    callback(-1, null);
                }
            }
            , true);
        }
        private void ScheduleClaimRetry(string pendingClaimKey) {
            PendingClaimTransaction transaction;
            if (!CanProcessVotes() || !pendingClaimTransactions.TryGetValue(pendingClaimKey, out transaction) || transaction == null || transaction.State == ClaimStateManualReview) {
                return;
            }
            string token;
            if (scheduledClaimRetries.TryGetValue(pendingClaimKey, out token)) {
                return;
            }
            bool localCompletion = transaction.State == ClaimStateConfirmed || transaction.State == ClaimStateClosed;
            int attempts = transaction.NeedsStatusCheck ? transaction.RecoveryAttempts : transaction.Attempts;
            if (!localCompletion && attempts >= MaximumClaimAttempts && !activeClaimRequests.ContainsKey(pendingClaimKey)) {
                MarkClaimForReview(pendingClaimKey, transaction, "The retry limit was reached. Inspect the preserved transaction with eve.pending.");
                return;
            }
            long now = DateTime.UtcNow.Ticks;
            if (transaction.NextAttemptUtcTicks <= now) {
                int backoffAttempts = Math.Max(transaction.Attempts, transaction.RecoveryAttempts);
                double delay = localCompletion ? 10d : (backoffAttempts <= 3 ? Math.Max(10d, backoffAttempts * 10d) : 300d);
                transaction.NextAttemptUtcTicks = now + (long)(delay * TimeSpan.TicksPerSecond);
                SavePendingClaimsData();
            }
            float remaining = (float)Math.Max(0.1d, Math.Min(300d, (transaction.NextAttemptUtcTicks - now) / (double)TimeSpan.TicksPerSecond));
            string transactionId = transaction.TransactionId;
            token = Guid.NewGuid().ToString("N");
            string retryToken = token;
            scheduledClaimRetries[pendingClaimKey] = retryToken;
            if (debugEnabled) {
                _Debug($"Retry scheduled: transaction={transactionId}, state={transaction.State}, delay={remaining:0.0}s.");
            }
            timer.Once(remaining, () => {
                string currentToken;
                PendingClaimTransaction current;
                if (!scheduledClaimRetries.TryGetValue(pendingClaimKey, out currentToken) || currentToken != retryToken || !TryGetActiveClaimTransaction(pendingClaimKey, transactionId, out current)) {
                    return;
                }
                scheduledClaimRetries.Remove(pendingClaimKey);
                QueuePendingClaimTransaction(pendingClaimKey);
            }
            );
        }
        private void RecoverPendingClaimTransactions() {
            if (!CanProcessVotes()) {
                return;
            }
            bool changed = false;
            foreach (PendingClaimTransaction transaction in pendingClaimTransactions.Values) {
                if (string.IsNullOrEmpty(transaction.TransactionId)) {
                    transaction.TransactionId = Guid.NewGuid().ToString("N");
                    changed = true;
                }
                if (transaction.CreatedUtcTicks == 0) {
                    transaction.CreatedUtcTicks = DateTime.UtcNow.Ticks;
                    changed = true;
                }
                if (string.IsNullOrEmpty(transaction.State) || transaction.State == ClaimStateSent) {
                    transaction.State = transaction.Attempts > 0 ? ClaimStateUncertain : ClaimStateCreated;
                    transaction.HadAmbiguousFailure |= transaction.Attempts > 0;
                    changed = true;
                }
                if (transaction.State == ClaimStateUncertain) {
                    transaction.NeedsStatusCheck = true;
                    transaction.HadAmbiguousFailure = true;
                    changed = true;
                }
            }
            if (changed && !SavePendingClaimsData()) {
                foreach (string key in pendingClaimTransactions.Keys.ToArray()) {
                    ScheduleClaimRetry(key);
                }
                return;
            }
            foreach (KeyValuePair<string, PendingClaimTransaction> entry in pendingClaimTransactions.OrderBy(entry => entry.Value.TargetVoteCount).ToArray()) {
                QueuePendingClaimTransaction(entry.Key);
            }
        }
        private string GetPendingClaimKey(ulong playerId, string serverName, string site) {
            return $"{playerId}:{serverName}:{site}";
        }
        private bool RemovePendingClaimTransaction(string pendingClaimKey) {
            PendingClaimTransaction transaction;
            if (!pendingClaimTransactions.TryGetValue(pendingClaimKey, out transaction)) {
                return true;
            }
            pendingClaimTransactions.Remove(pendingClaimKey);
            if (!SavePendingClaimsData()) {
                // Keep delivery locked until the removal is durable.
                pendingClaimTransactions[pendingClaimKey] = transaction;
                return false;
            }
            scheduledClaimRetries.Remove(pendingClaimKey);
            reportedConfigurationWarnings.Remove($"review:{transaction.TransactionId}");
            string requestToken;
            if (activeClaimRequests.TryGetValue(pendingClaimKey, out requestToken)) {
                ReleaseClaimRequest(pendingClaimKey, requestToken);
            }
            return true;
        }
        private bool ConsumeClaimRequest(string pendingClaimKey, string transactionId, string requestToken, out PendingClaimTransaction transaction) {
            transaction = null;
            string activeToken;
            if (!TryGetActiveClaimTransaction(pendingClaimKey, transactionId, out transaction) || !activeClaimRequests.TryGetValue(pendingClaimKey, out activeToken) || activeToken != requestToken) {
                return false;
            }
            ReleaseClaimRequest(pendingClaimKey, requestToken);
            return true;
        }
        private void ReleaseClaimRequest(string pendingClaimKey, string requestToken) {
            string activeToken;
            if (activeClaimRequests.TryGetValue(pendingClaimKey, out activeToken) && activeToken == requestToken) {
                activeClaimRequests.Remove(pendingClaimKey);
            }
        }
        private void FailClaimRequest(string key, PendingClaimTransaction transaction, string reason) {
            transaction.State = ClaimStateUncertain;
            transaction.HadAmbiguousFailure = true;
            transaction.NeedsStatusCheck = true;
            transaction.ReviewReason = reason;
            SavePendingClaimsData();
            ConsoleWarn($"Vote transaction {transaction.TransactionId} for {transaction.PlayerId} on {transaction.Site}: {reason}");
            ScheduleClaimRetry(key);
        }
        private void MarkClaimForReview(string key, PendingClaimTransaction transaction, string reason) {
            transaction.State = ClaimStateManualReview;
            transaction.ReviewReason = reason;
            transaction.NextAttemptUtcTicks = 0;
            scheduledClaimRetries.Remove(key);
            SavePendingClaimsData();
            ConsoleWarn($"Vote transaction {transaction.TransactionId} for {transaction.PlayerId} requires manual review: {reason} Use eve.pending and eve.resolve.");
        }
        private bool CloseClaimTransaction(string key, PendingClaimTransaction transaction) {
            transaction.State = ClaimStateClosed;
            transaction.NextAttemptUtcTicks = 0;
            if (!SavePendingClaimsData() || !RemovePendingClaimTransaction(key)) {
                ScheduleClaimRetry(key);
                return false;
            }
            return true;
        }
        private void HandleClaimReconciliation(int code, string response, string key, string transactionId, string requestToken) {
            PendingClaimTransaction transaction;
            if (!ConsumeClaimRequest(key, transactionId, requestToken, out transaction)) {
                return;
            }
            if (debugEnabled) {
                _Debug($"Recovery response: transaction={transaction.TransactionId}, player={transaction.PlayerId}, site={transaction.Site}, HTTP={code}.");
            }
            if (code != 200) {
                FailClaimRequest(key, transaction, $"Recovery status HTTP {code}.");
                return;
            }
            response = response?.Trim();
            if (_config.DebugSettings[ConfigDefaultKeys.VerboseDebugEnabled].ToBool()) {
                response = _config.DebugSettings[ConfigDefaultKeys.CheckAPIResponseCode];
            }
            if (response == "1") {
                transaction.State = ClaimStateCreated;
                transaction.RecoveryAttempts = 0;
                transaction.NeedsStatusCheck = false;
                transaction.NextAttemptUtcTicks = 0;
                if (!SavePendingClaimsData()) {
                    ScheduleClaimRetry(key);
                    return;
                }
                QueuePendingClaimTransaction(key);
            }
            else if (response == "2" && transaction.HadAmbiguousFailure) {
                ConfirmClaimTransaction(key, transaction);
            }
            else if (response == "0" || response == "2") {
                MarkClaimForReview(key, transaction, "The provider no longer reports an unclaimed vote after an ambiguous request. Verify the outcome before resolving it.");
            }
            else {
                FailClaimRequest(key, transaction, "Unexpected recovery status response; expected plain 0, 1 or 2.");
            }
        }
        private bool TryBuildTransactionUrl(PendingClaimTransaction transaction, string apiKey, out string url) {
            url = null;
            if (transaction == null || _config.Servers == null) {
                return false;
            }
            KeyValuePair<string, Dictionary<string, string>> server = _config.Servers.FirstOrDefault(entry => entry.Key.Equals(transaction.ServerName, StringComparison.OrdinalIgnoreCase));
            if (string.IsNullOrEmpty(server.Key) || server.Value == null) {
                return false;
            }
            KeyValuePair<string, string> site = server.Value.FirstOrDefault(entry => entry.Key.Equals(transaction.Site, StringComparison.OrdinalIgnoreCase));
            string configuredSiteName;
            Dictionary<string, string> apiConfiguration;
            string serverId;
            string serverKey;
            if (string.IsNullOrEmpty(site.Key) || !TryGetVoteSiteConfiguration(site.Key, out configuredSiteName, out apiConfiguration) || !TryParseServerCredentials(site.Value, out serverId, out serverKey)) {
                return false;
            }
            try {
                url = FormatApiUrl(apiConfiguration, apiKey, transaction.PlayerId, transaction.PlayerName, serverId, serverKey);
                Uri parsed;
                return Uri.TryCreate(url, UriKind.Absolute, out parsed) && (parsed.Scheme == Uri.UriSchemeHttps || parsed.Scheme == Uri.UriSchemeHttp);
            }
            catch (Exception) {
                return false;
            }
        }
        private bool HasUncommittedRewards(ulong playerId) {
            return pendingClaimTransactions.Values.Any(transaction => transaction != null && transaction.PlayerId == playerId && transaction.State == ClaimStateConfirmed);
        }
        private BasePlayer FindPlayer(ulong playerId) {
            return BasePlayer.FindByID(playerId) ?? BasePlayer.FindSleeping(playerId);
        }
        private string GetResolvedPlayerName(BasePlayer player, string fallbackName, string steamId) {
            if (player != null && !string.IsNullOrWhiteSpace(player.displayName)) {
                return player.displayName;
            }
            return string.IsNullOrWhiteSpace(fallbackName) ? steamId : fallbackName;
        }
        private string FormatApiUrl(Dictionary<string, string> apiConfiguration, string apiKey, BasePlayer player, string serverId, string serverKey) {
            if (player == null) {
                throw new ArgumentNullException(nameof(player));
            }
            return FormatApiUrl(apiConfiguration, apiKey, player.userID, player.displayName, serverId, serverKey);
        }
        private string FormatApiUrl(Dictionary<string, string> apiConfiguration, string apiKey, ulong playerId, string playerName, string serverId, string serverKey) {
            string apiLink = apiConfiguration[apiKey];
            bool usernameApiEnabled = apiConfiguration[ConfigDefaultKeys.apiUsername].ToBool();
            string encodedServerKey = Uri.EscapeDataString(serverKey);
            string encodedServerId = Uri.EscapeDataString(serverId);
            string encodedPlayerIdentifier = usernameApiEnabled ? Uri.EscapeDataString(playerName ?? string.Empty) : playerId.ToString();
            // Extra format arguments are ignored when the URL does not use them.
            // {0} = API key/token, {1} = Steam ID or username, {2} = server ID.
            return string.Format(apiLink, encodedServerKey, encodedPlayerIdentifier, encodedServerId);
        }
        private bool TryGetVoteSiteConfiguration(string site, out string configuredSiteName, out Dictionary<string, string> apiConfiguration) {
            configuredSiteName = null;
            apiConfiguration = null;
            if (_config.VoteSitesAPI == null) {
                return false;
            }
            foreach (KeyValuePair<string, Dictionary<string, string>> voteSite in _config.VoteSitesAPI) {
                if (!voteSite.Key.Equals(site, StringComparison.OrdinalIgnoreCase)) {
                    continue;
                }
                configuredSiteName = voteSite.Key;
                apiConfiguration = voteSite.Value;
                break;
            }
            if (apiConfiguration == null || !apiConfiguration.ContainsKey(ConfigDefaultKeys.apiClaim) || !apiConfiguration.ContainsKey(ConfigDefaultKeys.apiStatus) || !apiConfiguration.ContainsKey(ConfigDefaultKeys.apiLink) || !apiConfiguration.ContainsKey(ConfigDefaultKeys.apiUsername) || string.IsNullOrWhiteSpace(apiConfiguration[ConfigDefaultKeys.apiClaim]) || string.IsNullOrWhiteSpace(apiConfiguration[ConfigDefaultKeys.apiStatus]) || string.IsNullOrWhiteSpace(apiConfiguration[ConfigDefaultKeys.apiLink])) {
                return false;
            }
            return true;
        }
        private bool TryParseServerCredentials(string configuredValue, out string serverId, out string serverKey) {
            serverId = null;
            serverKey = null;
            if (string.IsNullOrWhiteSpace(configuredValue)) {
                return false;
            }
            string[] idKey = configuredValue.Split(new[] {
                ':'
            }
            , 2);
            if (idKey.Length != 2) {
                return false;
            }
            serverId = idKey[0].Trim();
            serverKey = idKey[1].Trim();
            if (string.IsNullOrWhiteSpace(serverId) || string.IsNullOrWhiteSpace(serverKey)) {
                return false;
            }
            if (serverId.Equals("ID", StringComparison.OrdinalIgnoreCase) || serverKey.Equals("KEY", StringComparison.OrdinalIgnoreCase)) {
                return false;
            }
            return true;
        }
        private int GetStoredVoteCount(string steamId) {
            if (!dataLoaded || DataFile == null || string.IsNullOrWhiteSpace(steamId)) {
                return 0;
            }
            object value = DataFile[steamId];
            int count;
            return value != null && int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out count) ? Math.Max(0, count) : 0;
        }
        private List<string> BuildRewardCommands(ulong playerId, string playerName, int playerVoteCount, bool cumulativeRewards) {
            List<string> commands = new List<string>();
            string steamId = playerId.ToString();
            AddRewardCommands(commands, "@", steamId, playerName);
            if (playerVoteCount == 1) {
                AddRewardCommands(commands, "first", steamId, playerName);
            }
            foreach (KeyValuePair<string, List<string>> reward in _config.Rewards) {
                int requiredVoteCount;
                if (!TryGetNumericRewardVoteCount(reward.Key, out requiredVoteCount)) {
                    continue;
                }
          
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху