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

FREE MoneyTime : 2.4.0 2.4.0

Нет прав для скачивания
C#:
/*
 * Money Time was originally created by Spiritwind and maintained by Wulf.
 * Original C# version copyright (c) 2020 Wulf.
 * Fixes and maintenance changes copyright (c) 2026 SeesAll.
 * Licensed under the MIT License; retain the original copyright and license notice.
 * Project notes: https://github.com/SeesAll/MoneyTime-Fixed
 */
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Money Time", "Wulf, SeesAll", "2.4.0")]
    [Description("Pays active players with Economics money or Server Rewards points for playing")]
    public class MoneyTime : CovalencePlugin
    {
        private const string PermissionPrefix = "moneytime.";
        private const string AdminPermission = "moneytime.admin";
        private const string LegacyMultiplierKey = "Permission-based mulitipliers";
        private const string MultiplierKey = "Permission-based multipliers";
        private const float HeartbeatInterval = 5f;

        #region Configuration

        private Configuration _config;

        private class Configuration
        {
            [JsonProperty("Currency provider (Economics or ServerRewards)")]
            public string CurrencyProvider = "Economics";

            [JsonProperty("Enable AFK API plugin support")]
            public bool AfkApi;

            [JsonProperty("Base payout amount")]
            public double BasePayout = 100d;

            [JsonProperty("Payout interval (seconds)")]
            public int PayoutInterval = 600;

            [JsonProperty("Time alive bonus")]
            public bool TimeAliveBonus;

            [JsonProperty("Time alive multiplier")]
            public double TimeAliveMultiplier;

            [JsonProperty("Allow permission-based multipliers to stack")]
            public bool StackMultipliers;

            [JsonProperty("Stacked multiplier mode (Additive or Multiplicative)")]
            public string StackedMultiplierMode = "Additive";

            [JsonProperty("New player welcome bonus")]
            public double WelcomeBonus = 500d;

            [JsonProperty("Notify players of successful payouts")]
            public bool NotifySuccessfulPayouts = true;

            [JsonProperty("Notify players of missed AFK payouts")]
            public bool NotifyMissedPayouts = true;

            [JsonProperty("Reset player data on a new save")]
            public bool ResetDataOnNewSave;

            [JsonProperty(MultiplierKey, ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public SortedDictionary<string, double> PermissionMultipliers = new SortedDictionary<string, double>
            {
                ["vip"] = 5d,
                ["donor"] = 2.5d
            };
        }

        protected override void LoadDefaultConfig()
        {
            _config = new Configuration();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                JObject rawConfig = Config.ReadObject<JObject>() ?? new JObject();
                bool changed = MigrateLegacyConfiguration(rawConfig);
                _config = rawConfig.ToObject<Configuration>() ?? new Configuration();
                changed |= ValidateConfiguration();

                HashSet<string> actualKeys = new HashSet<string>(rawConfig.Properties().Select(x => x.Name));
                HashSet<string> expectedKeys = new HashSet<string>(JObject.FromObject(_config).Properties().Select(x => x.Name));
                changed |= !actualKeys.SetEquals(expectedKeys);
                if (changed)
                {
                    LogWarning("Configuration was migrated or repaired; saving the normalized configuration");
                    SaveConfig();
                }
            }
            catch (Exception exception)
            {
                LogError($"Configuration file {Name}.json is invalid ({exception.Message}); replacing it with defaults");
                LoadDefaultConfig();
                ValidateConfiguration();
                SaveConfig();
            }
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config, true);
        }

        private bool MigrateLegacyConfiguration(JObject rawConfig)
        {
            bool changed = false;
            JToken legacyMultipliers;
            if (rawConfig.TryGetValue(LegacyMultiplierKey, out legacyMultipliers))
            {
                if (rawConfig.Property(MultiplierKey) == null) rawConfig[MultiplierKey] = legacyMultipliers;
                rawConfig.Remove(LegacyMultiplierKey);
                changed = true;
            }

            JToken economicsToken;
            JToken rewardsToken;
            bool hadEconomics = rawConfig.TryGetValue("Enable Economics as default currency", out economicsToken);
            bool hadRewards = rawConfig.TryGetValue("Enable Server Rewards as default currency", out rewardsToken);
            if (hadEconomics || hadRewards)
            {
                bool economics = hadEconomics && economicsToken.Type == JTokenType.Boolean && economicsToken.Value<bool>();
                bool rewards = hadRewards && rewardsToken.Type == JTokenType.Boolean && rewardsToken.Value<bool>();
                rawConfig["Currency provider (Economics or ServerRewards)"] = rewards && !economics ? "ServerRewards" : "Economics";
                rawConfig.Remove("Enable Economics as default currency");
                rawConfig.Remove("Enable Server Rewards as default currency");
                changed = true;
            }
            return changed;
        }

        private bool ValidateConfiguration()
        {
            bool changed = false;
            string provider = (_config.CurrencyProvider ?? string.Empty).Replace(" ", string.Empty);
            if (provider.Equals("Economics", StringComparison.OrdinalIgnoreCase))
            {
                changed |= _config.CurrencyProvider != "Economics";
                _config.CurrencyProvider = "Economics";
            }
            else if (provider.Equals("ServerRewards", StringComparison.OrdinalIgnoreCase))
            {
                changed |= _config.CurrencyProvider != "ServerRewards";
                _config.CurrencyProvider = "ServerRewards";
            }
            else
            {
                LogWarning($"Unknown currency provider '{_config.CurrencyProvider}'; using Economics");
                _config.CurrencyProvider = "Economics";
                changed = true;
            }

            if (!IsFinite(_config.BasePayout) || _config.BasePayout <= 0d)
            {
                LogWarning("Base payout amount must be greater than zero; using 100");
                _config.BasePayout = 100d;
                changed = true;
            }
            if (_config.PayoutInterval < 1)
            {
                LogWarning("Payout interval must be at least one second; using 600");
                _config.PayoutInterval = 600;
                changed = true;
            }
            if (!IsFinite(_config.TimeAliveMultiplier) || _config.TimeAliveMultiplier < 0d)
            {
                LogWarning("Time alive multiplier cannot be negative; using zero");
                _config.TimeAliveMultiplier = 0d;
                changed = true;
            }
            if (!IsFinite(_config.WelcomeBonus) || _config.WelcomeBonus < 0d)
            {
                LogWarning("Welcome bonus cannot be negative; using zero");
                _config.WelcomeBonus = 0d;
                changed = true;
            }

            string stackMode = _config.StackedMultiplierMode ?? string.Empty;
            if (stackMode.Equals("Additive", StringComparison.OrdinalIgnoreCase))
            {
                changed |= stackMode != "Additive";
                _config.StackedMultiplierMode = "Additive";
            }
            else if (stackMode.Equals("Multiplicative", StringComparison.OrdinalIgnoreCase))
            {
                changed |= stackMode != "Multiplicative";
                _config.StackedMultiplierMode = "Multiplicative";
            }
            else
            {
                LogWarning($"Unknown stacked multiplier mode '{stackMode}'; using Additive");
                _config.StackedMultiplierMode = "Additive";
                changed = true;
            }

            if (_config.PermissionMultipliers == null)
            {
                _config.PermissionMultipliers = new SortedDictionary<string, double>();
                changed = true;
            }

            SortedDictionary<string, double> valid = new SortedDictionary<string, double>();
            foreach (KeyValuePair<string, double> entry in _config.PermissionMultipliers)
            {
                string name = (entry.Key ?? string.Empty).Trim().ToLowerInvariant();
                if (string.IsNullOrEmpty(name) || name.Any(char.IsWhiteSpace) || !IsFinite(entry.Value) || entry.Value <= 0d)
                {
                    LogWarning($"Ignoring invalid permission multiplier '{entry.Key}'");
                    changed = true;
                    continue;
                }
                valid[name] = entry.Value;
                changed |= name != entry.Key;
            }
            _config.PermissionMultipliers = valid;
            return changed;
        }

        private static bool IsFinite(double value)
        {
            return !double.IsNaN(value) && !double.IsInfinity(value);
        }

        #endregion

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["MissedPayout"] = "You were inactive and missed this payout.",
                ["ReceivedForPlaying"] = "You received $payout.amount for actively playing.",
                ["ReceivedForTimeAlive"] = "You received $payout.amount for staying alive for $time.alive.",
                ["ReceivedWelcomeBonus"] = "You received $payout.amount as a welcome bonus.",
                ["NoPermission"] = "You do not have permission to use this command.",
                ["AdminUsage"] = "Usage: moneytime status | moneytime resetwelcome <Steam ID|all>",
                ["WelcomeResetAll"] = "Welcome bonus eligibility was reset for all stored players.",
                ["WelcomeResetPlayer"] = "Welcome bonus eligibility was reset for $player.id.",
                ["Status"] = "Provider: $provider | Base payout: $amount | Interval: $interval seconds | Your multiplier: $multiplier"
            }, this);
        }

        #endregion

        #region Data Storage

        private StoredData _storedData;

        private class StoredData
        {
            [JsonProperty("Data version")]
            public int Version = 2;

            [JsonProperty("Players")]
            public Dictionary<string, PlayerInfo> Players = new Dictionary<string, PlayerInfo>();
        }

        private class PlayerInfo
        {
            // Keep the legacy field name so existing data remains readable.
            [JsonProperty("WelcomeBonus")]
            public bool WelcomeBonus;

            [JsonProperty("Pending time alive bonus")]
            public double PendingAliveBonus;

            [JsonProperty("Pending time alive seconds")]
            public double PendingAliveSeconds;
        }

        private void LoadData()
        {
            try
            {
                _storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
                if (_storedData.Players == null) _storedData.Players = new Dictionary<string, PlayerInfo>();
                _storedData.Version = 2;
            }
            catch (Exception exception)
            {
                LogError($"Data file {Name}.json could not be read ({exception.Message}); starting with fresh data");
                _storedData = new StoredData();
            }
        }

        private void SaveData()
        {
            if (_storedData != null) Interface.Oxide.DataFileSystem.WriteObject(Name, _storedData);
        }

        private PlayerInfo GetPlayerInfo(string playerId)
        {
            PlayerInfo info;
            if (!_storedData.Players.TryGetValue(playerId, out info) || info == null)
            {
                info = new PlayerInfo();
                _storedData.Players[playerId] = info;
            }
            return info;
        }

        private void OnServerSave()
        {
            SaveData();
        }

        private void OnNewSave(string filename)
        {
            if (!_config.ResetDataOnNewSave) return;
            _storedData = new StoredData();
            SaveData();
            Log("Player reward data was reset for the new save");
        }

        #endregion

        #region Initialization and Scheduling

        [PluginReference]
        private Plugin Economics, ServerRewards, AFKAPI;

        private readonly Dictionary<string, PayoutState> _payouts = new Dictionary<string, PayoutState>();
        private readonly Dictionary<string, float> _lifeStartedAt = new Dictionary<string, float>();
        private readonly Dictionary<string, double> _permissionMultipliers = new Dictionary<string, double>();
        private readonly HashSet<string> _loggedWarnings = new HashSet<string>();

        private class PayoutState
        {
            public double Amount;
            public float DueAt;
            public float PendingRetryAt;
        }

        private enum PayoutResult
        {
            Success,
            SkippedAfk,
            Failed
        }

        private void Init()
        {
            permission.RegisterPermission(AdminPermission, this);
            foreach (KeyValuePair<string, double> entry in _config.PermissionMultipliers)
            {
                string permissionName = PermissionPrefix + entry.Key;
                _permissionMultipliers[permissionName] = entry.Value;
                permission.RegisterPermission(permissionName, this);
                Log($"Registered permission '{permissionName}' with a {FormatAmount(entry.Value)}x multiplier");
            }
            LoadData();
            if (!_config.TimeAliveBonus)
            {
                Unsubscribe(nameof(OnPlayerDeath));
                Unsubscribe(nameof(OnUserRespawned));
            }
        }

        private void OnServerInitialized()
        {
            foreach (IPlayer player in players.Connected) InitializePlayer(player, true);
            float heartbeat = Math.Min(HeartbeatInterval, Math.Max(1f, _config.PayoutInterval));
            timer.Every(heartbeat, ProcessPayouts);
            ValidateDependencies();
        }

        private void Unload()
        {
            SaveData();
            _permissionMultipliers.Clear();
            _payouts.Clear();
            _lifeStartedAt.Clear();
            _loggedWarnings.Clear();
        }

        private void InitializePlayer(IPlayer player, bool resetSchedule)
        {
            if (player == null || !player.Id.IsSteamId()) return;
            GetPlayerInfo(player.Id);
            double appliedMultiplier;
            double amount = CalculatePayout(player, out appliedMultiplier);

            PayoutState state;
            if (!_payouts.TryGetValue(player.Id, out state))
            {
                state = new PayoutState();
                _payouts[player.Id] = state;
                resetSchedule = true;
                state.PendingRetryAt = Time.realtimeSinceStartup;
            }
            state.Amount = amount;
            if (resetSchedule) state.DueAt = Time.realtimeSinceStartup + _config.PayoutInterval;

            if (_config.TimeAliveBonus && !_lifeStartedAt.ContainsKey(player.Id))
                _lifeStartedAt[player.Id] = Time.realtimeSinceStartup;
        }

        private double CalculatePayout(IPlayer player, out double appliedMultiplier)
        {
            List<double> multipliers = new List<double>();
            foreach (KeyValuePair<string, double> entry in _permissionMultipliers)
                if (player.HasPermission(entry.Key)) multipliers.Add(entry.Value);

            appliedMultiplier = 1d;
            if (multipliers.Count == 0) return _config.BasePayout;

            if (!_config.StackMultipliers) appliedMultiplier = multipliers.Max();
            else if (_config.StackedMultiplierMode == "Multiplicative")
                foreach (double multiplier in multipliers) appliedMultiplier *= multiplier;
            else appliedMultiplier = multipliers.Sum();

            return _config.BasePayout * appliedMultiplier;
        }

        private void ProcessPayouts()
        {
            float now = Time.realtimeSinceStartup;
            foreach (IPlayer player in players.Connected)
            {
                if (!player.Id.IsSteamId()) continue;
                PayoutState state;
                if (!_payouts.TryGetValue(player.Id, out state))
                {
                    InitializePlayer(player, true);
                    state = _payouts[player.Id];
                }

                if (state.PendingRetryAt <= now)
                {
                    TryPendingRewards(player);
                    state.PendingRetryAt = now + Math.Min(30f, _config.PayoutInterval);
                }
                if (state.DueAt > now) continue;

                PayoutResult result = TryPayout(player, state.Amount, "ReceivedForPlaying", true, null);
                state.DueAt = result == PayoutResult.Failed
                    ? now + Math.Min(30f, _config.PayoutInterval)
                    : now + _config.PayoutInterval;
            }
        }

        private void ValidateDependencies()
        {
            if (_config.CurrencyProvider == "Economics" && (Economics == null || !Economics.IsLoaded))
                WarnOnce("economics-missing", "Economics is configured but is not loaded; rewards will remain pending or retry");
            else if (_config.CurrencyProvider == "ServerRewards" && (ServerRewards == null || !ServerRewards.IsLoaded))
                WarnOnce("serverrewards-missing", "ServerRewards is configured but is not loaded; rewards will remain pending or retry");

            if (_config.AfkApi && (AFKAPI == null || !AFKAPI.IsLoaded))
                WarnOnce("afkapi-missing", "AFK API support is enabled but AFKAPI is not loaded; payouts will continue without AFK filtering");
        }

        #endregion

        #region Permission Updates

        private void OnGroupPermissionGranted(string groupName, string permissionName)
        {
            if (_permissionMultipliers.ContainsKey(permissionName)) RecalculateConnectedPlayers();
        }

        private void OnGroupPermissionRevoked(string groupName, string permissionName)
        {
            if (_permissionMultipliers.ContainsKey(permissionName)) RecalculateConnectedPlayers();
        }

        private void OnUserPermissionGranted(string playerId, string permissionName)
        {
            if (_permissionMultipliers.ContainsKey(permissionName)) RecalculatePlayer(playerId);
        }

        private void OnUserPermissionRevoked(string playerId, string permissionName)
        {
            if (_permissionMultipliers.ContainsKey(permissionName)) RecalculatePlayer(playerId);
        }

        private void OnUserGroupAdded(string playerId, string groupName)
        {
            RecalculatePlayer(playerId);
        }

        private void OnUserGroupRemoved(string playerId, string groupName)
        {
            RecalculatePlayer(playerId);
        }

        private void RecalculateConnectedPlayers()
        {
            foreach (IPlayer player in players.Connected) InitializePlayer(player, false);
        }

        private void RecalculatePlayer(string playerId)
        {
            IPlayer player = players.FindPlayerById(playerId);
            if (player != null && player.IsConnected) InitializePlayer(player, false);
        }

        #endregion

        #region Payout Handling

        private PayoutResult TryPayout(IPlayer player, double amount, string messageKey, bool requireActivity, string timeAlive)
        {
            if (player == null || !player.IsConnected || !IsFinite(amount) || amount <= 0d) return PayoutResult.Failed;
            if (requireActivity && IsPlayerAfk(player))
            {
                if (_config.NotifyMissedPayouts) Message(player, "MissedPayout");
                return PayoutResult.SkippedAfk;
            }

            bool success;
            double creditedAmount = amount;
            try
            {
                if (_config.CurrencyProvider == "Economics")
                {
                    if (Economics == null || !Economics.IsLoaded)
                    {
                        WarnOnce("economics-missing", "Economics is unavailable; payout will be retried");
                        return PayoutResult.Failed;
                    }
                    object result = Economics.Call("Deposit", player.Id, amount);
                    success = result is bool && (bool)result;
                }
                else
                {
                    if (ServerRewards == null || !ServerRewards.IsLoaded)
                    {
                        WarnOnce("serverrewards-missing", "ServerRewards is unavailable; payout will be retried");
                        return PayoutResult.Failed;
                    }
                    int points = Convert.ToInt32(Math.Round(amount, MidpointRounding.AwayFromZero));
                    if (points <= 0)
                    {
                        WarnOnce("serverrewards-rounding", "A ServerRewards payout rounded to zero and was not issued");
                        return PayoutResult.Failed;
                    }
                    object result = ServerRewards.Call("AddPoints", player.Id, points);
                    success = result is bool && (bool)result;
                    creditedAmount = points;
                }
            }
            catch (Exception exception)
            {
                LogError($"Payout failed for {player.Name} ({player.Id}): {exception.Message}");
                return PayoutResult.Failed;
            }

            if (!success)
            {
                WarnOnce("provider-rejected", $"{_config.CurrencyProvider} rejected a payout");
                return PayoutResult.Failed;
            }

            if (_config.NotifySuccessfulPayouts)
            {
                string message = GetLang(messageKey, player.Id).Replace("$payout.amount", FormatAmount(creditedAmount));
                if (timeAlive != null) message = message.Replace("$time.alive", timeAlive);
                player.Reply(message);
            }
            return PayoutResult.Success;
        }

        private bool IsPlayerAfk(IPlayer player)
        {
            if (!_config.AfkApi || AFKAPI == null || !AFKAPI.IsLoaded) return false;
            ulong playerId;
            return ulong.TryParse(player.Id, out playerId) && AFKAPI.Call<bool>("IsPlayerAFK", playerId);
        }

        private void TryPendingRewards(IPlayer player)
        {
            PlayerInfo info = GetPlayerInfo(player.Id);
            bool changed = false;
            if (_config.WelcomeBonus > 0d && !info.WelcomeBonus &&
                TryPayout(player, _config.WelcomeBonus, "ReceivedWelcomeBonus", false, null) == PayoutResult.Success)
            {
                info.WelcomeBonus = true;
                changed = true;
            }

            if (info.PendingAliveBonus > 0d &&
                TryPayout(player, info.PendingAliveBonus, "ReceivedForTimeAlive", false, FormatDuration(info.PendingAliveSeconds)) == PayoutResult.Success)
            {
                info.PendingAliveBonus = 0d;
                info.PendingAliveSeconds = 0d;
                changed = true;
            }
            if (changed) SaveData();
        }

        private void OnUserConnected(IPlayer player)
        {
            InitializePlayer(player, true);
            if (player != null && player.Id.IsSteamId())
            {
                TryPendingRewards(player);
                _payouts[player.Id].PendingRetryAt = Time.realtimeSinceStartup + Math.Min(30f, _config.PayoutInterval);
            }
        }

        private void OnUserDisconnected(IPlayer player)
        {
            if (player == null) return;
            _payouts.Remove(player.Id);
            _lifeStartedAt.Remove(player.Id);
        }

        private void OnUserRespawned(IPlayer player)
        {
            if (player != null && player.Id.IsSteamId()) _lifeStartedAt[player.Id] = Time.realtimeSinceStartup;
        }

        private void OnPlayerDeath(BasePlayer basePlayer, HitInfo hitInfo)
        {
            if (!_config.TimeAliveBonus || basePlayer == null || basePlayer.IPlayer == null) return;
            IPlayer player = basePlayer.IPlayer;
            float startedAt;
            if (!_lifeStartedAt.TryGetValue(player.Id, out startedAt)) return;

            _lifeStartedAt.Remove(player.Id);
            double secondsAlive = Math.Max(0d, Time.realtimeSinceStartup - startedAt);
            if (secondsAlive <= 0d || _config.TimeAliveMultiplier <= 0d) return;

            double amount = (secondsAlive / _config.PayoutInterval) * _config.BasePayout * _config.TimeAliveMultiplier;
            if (!IsFinite(amount) || amount <= 0d) return;

            PlayerInfo info = GetPlayerInfo(player.Id);
            info.PendingAliveBonus += amount;
            info.PendingAliveSeconds += secondsAlive;
            SaveData();
            TryPendingRewards(player);
        }

        #endregion

        #region Commands and Helpers

        [Command("moneytime")]
        private void MoneyTimeCommand(IPlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!player.IsServer && !player.HasPermission(AdminPermission))
            {
                Message(player, "NoPermission");
                return;
            }

            if (args.Length == 0 || args[0].Equals("status", StringComparison.OrdinalIgnoreCase))
            {
                double multiplier = 1d;
                if (!player.IsServer && player.Id.IsSteamId()) CalculatePayout(player, out multiplier);
                string status = GetLang("Status", player.Id)
                    .Replace("$provider", _config.CurrencyProvider)
                    .Replace("$amount", FormatAmount(_config.BasePayout))
                    .Replace("$interval", _config.PayoutInterval.ToString(CultureInfo.InvariantCulture))
                    .Replace("$multiplier", FormatAmount(multiplier) + "x");
                player.Reply(status);
                return;
            }

            if (!args[0].Equals("resetwelcome", StringComparison.OrdinalIgnoreCase) || args.Length < 2)
            {
                Message(player, "AdminUsage");
                return;
            }

            if (args[1].Equals("all", StringComparison.OrdinalIgnoreCase))
            {
                foreach (PlayerInfo info in _storedData.Players.Values)
                    if (info != null) info.WelcomeBonus = false;
                SaveData();
                Message(player, "WelcomeResetAll");
                return;
            }

            string targetId = args[1];
            if (!targetId.IsSteamId())
            {
                Message(player, "AdminUsage");
                return;
            }
            GetPlayerInfo(targetId).WelcomeBonus = false;
            SaveData();
            player.Reply(GetLang("WelcomeResetPlayer", player.Id).Replace("$player.id", targetId));
        }

        private string GetLang(string key, string playerId = null)
        {
            return lang.GetMessage(key, this, playerId);
        }

        private void Message(IPlayer player, string key)
        {
            if (player != null) player.Reply(GetLang(key, player.Id));
        }

        private void WarnOnce(string key, string message)
        {
            if (_loggedWarnings.Add(key)) LogWarning(message);
        }

        private static string FormatAmount(double amount)
        {
            return amount.ToString("0.##", CultureInfo.InvariantCulture);
        }

        private static string FormatDuration(double totalSeconds)
        {
            TimeSpan duration = TimeSpan.FromSeconds(Math.Max(0d, totalSeconds));
            int totalHours = (int)duration.TotalHours;
            if (totalHours > 0) return $"{totalHours}h {duration.Minutes:00}m {duration.Seconds:00}s";
            if (duration.Minutes > 0) return $"{duration.Minutes}m {duration.Seconds:00}s";
            return $"{duration.Seconds}s";
        }

        #endregion
    }
}

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