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

FREE PlaceholderAPI : 2.1.1 2.1.1

Нет прав для скачивания
C#:
// Uncomment this line to enable some debug output and performance measurers
// #define DEBUG

#if DEBUG
using System.Diagnostics;
#endif

#if RUST
using Network;
#endif
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;

namespace Oxide.Plugins
{
    [Info("Placeholder API", "misticos", "2.1.1")]
    [Description("Centralized location to query data from other plugins. Streamlined, convenient, and performant.")]
    class PlaceholderAPI : CovalencePlugin
    {
        #region Variables
        
#if RUST
        [PluginReference("RustCore")]
        private Plugin _rustCore = null;
#endif

#if SEVENDAYSTODIE
        private int _bloodMoonFrequency;
#endif

        private Dictionary<string, Placeholder> _placeholdersByName =
            new Dictionary<string, Placeholder>();

        private static PlaceholderAPI _ins;

        private const string PermissionList = "placeholderapi.list";
        private const string CommandNameList = "placeholderapi.list";

        private const string PermissionTest = "placeholderapi.test";
        private const string CommandNameTest = "placeholderapi.test";

        private const string HookNameReady = "OnPlaceholderAPIReady";
        private const string HookNameAddressDataRetrieved = "OnAddressDataRetrieved";

        #endregion

        #region Configuration

        private Configuration _config;

        private class Configuration
        {
            [JsonProperty(PropertyName = "Placeholders", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, string> Placeholders = new Dictionary<string, string>
            {
                {"key", "Custom value"}
            };

            [JsonProperty(PropertyName = "Culture Ietf Tag")]
            public string DefaultCulture = "en-US";

            [JsonProperty(PropertyName = "Local Time Offset")]
            public TimeSpan LocalTimeOffset = TimeSpan.Zero;
            
            #if RUST
            [JsonProperty(PropertyName = "Map Wipe Schedule")]
            public WipeSchedule MapWipe = new WipeSchedule();

            [JsonProperty(PropertyName = "Blueprints Wipe Schedule")]
            public WipeSchedule BlueprintsWipe = new WipeSchedule();
            #endif

            [JsonProperty(PropertyName = "Request Address Data (ip-api.com)")]
            public bool RequestAddressData = true;

            [JsonIgnore]
            public CultureInfo Culture;

            public class WipeSchedule
            {
                [JsonConverter(typeof(StringEnumConverter))]
                [JsonProperty(PropertyName = "Every First Month Day")]
                public DayOfWeek? EveryFirstDay = DayOfWeek.Friday;

                [JsonConverter(typeof(StringEnumConverter))]
                [JsonProperty(PropertyName = "Every N Day")]
                public DayOfWeek? EveryDay = DayOfWeek.Monday;

                [JsonProperty(PropertyName = "Every")]
                public TimeSpan? Every = TimeSpan.FromDays(7);

                [JsonProperty(PropertyName = "Time")]
                public TimeSpan? Time = TimeSpan.FromHours(12);

                public DateTime GetNextWipeDate(DateTime lastWipe)
                {
                    var date = lastWipe;
                    if (EveryFirstDay != null)
                    {
                        var nextWipeCurrentMonth = GetFirstMonthDayOfWeek(lastWipe, EveryFirstDay.Value);
                        date = nextWipeCurrentMonth < lastWipe
                            ? GetFirstMonthDayOfWeek(lastWipe.AddMonths(1), EveryFirstDay ?? DayOfWeek.Friday)
                            : nextWipeCurrentMonth;
                    }

                    if (EveryDay != null)
                    {
                        var nextDay = GetNextDayOfWeek(lastWipe, EveryDay.Value);
                        if (nextDay < date && nextDay > lastWipe)
                            date = nextDay;
                    }

                    if (Every != null)
                    {
                        var next = lastWipe.Add(Every.Value);
                        if (next > date)
                            date = next;
                    }

                    if (Time != null)
                        date = date.Date + Time.Value;

                    return date;
                }

                private DateTime GetFirstMonthDayOfWeek(DateTime date, DayOfWeek day) =>
                    GetNextDayOfWeek(date.AddDays(-date.Day + 1), day);

                private DateTime GetNextDayOfWeek(DateTime date, DayOfWeek day) =>
                    date.AddDays(Math.Abs(date.DayOfWeek - day - 7) % 7);
            }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<Configuration>();
                if (_config == null) throw new Exception();

                try
                {
                    _config.Culture = CultureInfo.GetCultureInfoByIetfLanguageTag(_config.DefaultCulture);
                }
                catch
                {
                    _config.Culture = CultureInfo.CurrentCulture;
                    Interface.Oxide.LogInfo(
                        $"{_config.DefaultCulture} is an invalid language tag! Valid: {string.Join(", ", CultureInfo.GetCultures(CultureTypes.AllCultures).Select(x => x.IetfLanguageTag).ToArray())}");
                }

                SaveConfig();
            }
            catch
            {
                PrintError("Your configuration file contains an error. Using default configuration values.");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig() => Config.WriteObject(_config);

        protected override void LoadDefaultConfig() => _config = new Configuration();

        #endregion

        #region Commands

        private void CommandList(IPlayer player, string command, string[] args)
        {
            if (!player.IsAdmin && !player.HasPermission(PermissionList))
            {
                player.Reply(GetMsg("No Permission", player.Id));
                return;
            }

            var builder = new StringBuilder();

            var separator = GetMsg("Command: List: Entry Separator", player.Id);
            var format = GetMsg("Command: List: Entry Format", player.Id);
            var noDescription = GetMsg("Command: List: No Description", player.Id);
            var noCache = GetMsg("Command: List: No Cache", player.Id);
            foreach (var kvp in _placeholdersByName.OrderBy(x => x.Key))
            {
                if (args.Length != 0)
                {
                    var flag = false;
                    foreach (var arg in args)
                    {
                        if (kvp.Key.Contains(arg))
                        {
                            flag = true;
                            break;
                        }
                    }

                    if (!flag)
                        continue;
                }

                if (builder.Length != 0)
                    builder.Append(separator);

                builder.Append(format).Replace("{name}", kvp.Key).Replace("{owner}", kvp.Value.Owner.Title)
                    .Replace("{description}",
                        string.IsNullOrEmpty(kvp.Value.Description) ? noDescription : kvp.Value.Description)
                    .Replace("{cache}", kvp.Value.HasCache() ? kvp.Value.CacheTTLParsed.ToString() : noCache);
            }

            if (builder.Length == 0)
            {
                player.Reply(GetMsg("Command: List: Not Found", player.Id));
                return;
            }

            var list = builder.ToString();
            
            // Clear it. No Clear method.
            builder.Length = 0;
            
            player.Reply(builder.Append(GetMsg("Command: List: Format", player.Id)).Replace("{list}", list)
                .ToString());
        }

        private class Options
        {
            public IPlayer Target;
            public bool IgnoreCache;
            public int Parsed;

            public Options(IPlayer caller, string[] args)
            {
                Target = caller;
                IgnoreCache = false;
                Parsed = 0;

                if (args.Length < 3)
                    return;

                for (var i = args.Length - 1; i > 0 && i > args.Length - 5; i -= 2)
                {
                    switch (args[i - 1]?.ToLower(CultureInfo.InvariantCulture))
                    {
                        case "player":
                        case "p":
                        {
                            Parsed++;
                            Target = _ins.players.FindPlayer(args[i]) ?? caller;
                            break;
                        }

                        case "ignorecache":
                        case "ic":
                        {
                            Parsed++;
                            if (!bool.TryParse(args[i], out IgnoreCache))
                                IgnoreCache = false;

                            break;
                        }

                        default:
                            return;
                    }
                }
            }
        }
        
        private void CommandTest(IPlayer player, string command, string[] args)
        {
            if (!player.IsAdmin && !player.HasPermission(PermissionTest))
            {
                player.Reply(GetMsg("No Permission", player.Id));
                return;
            }
            
            if (args.Length == 0)
            {
                player.Reply(GetMsg("Command: Test: Syntax", player.Id));
                return;
            }

            var options = new Options(player, args);
            var builder = new StringBuilder(string.Join(" ", args.Take(args.Length - options.Parsed * 2).ToArray()));

            Placeholder.Run(options.Target, builder, options.IgnoreCache);

            var result = builder.Length > 0 ? builder.ToString(0, Math.Min(builder.Length, 1024)) : "NONE";
            player.Reply($"RESULT: {result.Trim('{', '}')}");
        }

        #endregion

        #region Temporary Data

        public static class AddressHandler
        {
            public static Queue<KeyValuePair<IPlayer, string>> QueuedPlayers = new Queue<KeyValuePair<IPlayer, string>>();
            public static Dictionary<string, AddressResponse> Cache = new Dictionary<string, AddressResponse>();
            public static Dictionary<string, JObject> CachedJson = new Dictionary<string, JObject>();

            public const float FrequencyRequest = 60f / 45f * 1.1f;
            public const float FrequencyBulkRequest = 60f / 15f * 1.1f;

            private const string AddressDataRequest =
                "http://ip-api.com/json/{0}?fields=status,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,mobile,proxy,hosting";

            private const string AddressDataBulkRequest =
                "http://ip-api.com/batch?fields=status,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,mobile,proxy,hosting";

            public static bool Enqueue(IPlayer player, string address)
            {
                if (player == null || Cache.ContainsKey(player.Address))
                    return false;
#if DEBUG
                Interface.Oxide.LogDebug($"Enqueuing player in {nameof(AddressHandler)} ({player.Id}/{address})");
#endif

                QueuedPlayers.Enqueue(new KeyValuePair<IPlayer, string>(player, address));
                return true;
            }

            public static void Clear()
            {
                QueuedPlayers.Clear();
                Cache.Clear();
            }

            public static void RunTimers()
            {
                _ins.timer.Every(FrequencyRequest, ProcessQueue);
                _ins.timer.Every(FrequencyBulkRequest, ProcessQueueBulk);
            }

            private static void ProcessQueue()
            {
                if (QueuedPlayers.Count == 0)
                    return;

                var kvp = QueuedPlayers.Dequeue();
                Request(kvp.Key, kvp.Value);
            }

            private static void Request(IPlayer player, string address)
            {
                _ins.webrequest.Enqueue(string.Format(AddressDataRequest, address), string.Empty,
                    (code, response) =>
                    {
                        if (code != 200)
                        {
                            Interface.Oxide.LogWarning(
                                $"There was an issue connecting to \"ip-api.com\"! Some address placeholders may not be available. Code: {code}");

                            return;
                        }

                        var addressData = JsonConvert.DeserializeObject<AddressResponse>(response);
                        if (!addressData.IsSuccess)
                            return;

                        Cache[player.Address] = addressData;
                        Interface.Oxide.CallHook(HookNameAddressDataRetrieved, player, false);
#if DEBUG
                        Interface.Oxide.LogDebug("Received data for address (1)");
#endif
                    }, _ins);
            }

            private static void ProcessQueueBulk()
            {
                if (QueuedPlayers.Count == 0)
                    return;

                var players = new IPlayer[Math.Min(100, QueuedPlayers.Count)];
                var addresses = new string[players.Length];
                var body = new StringBuilder();
                
                body.Append('[');
                for (var i = 0; i < players.Length; i++)
                {
                    if (i != 0)
                        body.Append(',');


                    var kvp = QueuedPlayers.Dequeue();
                    
                    players[i] = kvp.Key;
                    body.Append('"');
                    body.Append(addresses[i] = kvp.Value);
                    body.Append('"');
                }
                body.Append(']');

                RequestBulk(players, addresses, body.ToString());
            }

            private static void RequestBulk(IPlayer[] players, string[] addresses, string body)
            {
                _ins.webrequest.Enqueue(AddressDataBulkRequest, body, (code, response) =>
                {
                    if (code != 200)
                        goto nosuccess;

                    var deserializedResponses = JsonConvert.DeserializeObject<List<AddressResponse>>(response);
                    if (deserializedResponses == null || deserializedResponses.Count < players.Length)
                        goto nosuccess;

                    for (var i = 0; i < deserializedResponses.Count; i++)
                    {
                        var addressData = deserializedResponses[i];
                        if (!addressData.IsSuccess)
                            continue;

                        Cache[addresses[i]] = addressData;
                        Interface.Oxide.CallHook(HookNameAddressDataRetrieved, players[i], false);
                    }

#if DEBUG
                    Interface.Oxide.LogDebug($"Received bulk data for addresses ({players.Length})");
#endif
                    return;

                    nosuccess:
                    Interface.Oxide.LogWarning(
                        $"There was an issue connecting to \"ip-api.com\" for a bulk request! Many address placeholders may not be available. Code: {code}");
                }, _ins, RequestMethod.POST);
            }
        }

        public class AddressResponse
        {
            [JsonProperty(PropertyName = "status")]
            public string Status = null;

            [JsonIgnore]
            public bool IsSuccess => Status == "success";

            [JsonProperty(PropertyName = "continent")]
            public string Continent = null;

            [JsonProperty(PropertyName = "continentCode")]
            public string ContinentCode = null;

            [JsonProperty(PropertyName = "country")]
            public string Country = null;

            [JsonProperty(PropertyName = "countryCode")]
            public string CountryCode = null;

            [JsonProperty(PropertyName = "regionName")]
            public string Region = null;

            [JsonProperty(PropertyName = "region")]
            public string RegionCode = null;

            [JsonProperty(PropertyName = "city")]
            public string City = null;

            [JsonProperty(PropertyName = "district")]
            public string District = null;

            [JsonProperty(PropertyName = "zip")]
            public string ZipCode = null;

            [JsonProperty(PropertyName = "lat")]
            public float Latitude = 0f;

            [JsonProperty(PropertyName = "lon")]
            public float Longitude = 0f;

            [JsonProperty(PropertyName = "timezone")]
            public string Timezone = null;

            [JsonProperty(PropertyName = "offset")]
            public int TimezoneOffset = 0;

            [JsonProperty(PropertyName = "currency")]
            public string Currency = null;

            [JsonProperty(PropertyName = "isp")]
            public string ISP = null;

            [JsonProperty(PropertyName = "org")]
            public string Organization = null;

            [JsonProperty(PropertyName = "as", NullValueHandling = NullValueHandling.Ignore)]
            public string AS = null;

            [JsonProperty(PropertyName = "asname", NullValueHandling = NullValueHandling.Ignore)]
            public string ASName = null;

            [JsonProperty(PropertyName = "mobile")]
            public bool Mobile = false;

            [JsonProperty(PropertyName = "proxy")]
            public bool Proxy = false;

            [JsonProperty(PropertyName = "hosting")]
            public bool Hosting = false;
        }

        #endregion

        #region Placeholders

        public class Placeholder
        {
            public string Name;
            public string Description;

            public Plugin Owner;

            public Func<IPlayer, string, object> Action;

            public double CacheTTL = 0d;
            public TimeSpan CacheTTLParsed;
            public Dictionary<string, Dictionary<string, KeyValuePair<DateTime, object>>> Cache;

            public Placeholder(Plugin owner, string name, string description, Func<IPlayer, string, object> action, double cacheTTL)
            {
                Owner = owner;
                Name = name;
                Description = description;
                Action = action;
                CacheTTL = cacheTTL;
                
                if (!HasCache())
                    return;

                Cache = new Dictionary<string, Dictionary<string, KeyValuePair<DateTime, object>>>();
                CacheTTLParsed = TimeSpan.FromSeconds(CacheTTL);
            }

            public bool HasCache() => !double.IsNaN(CacheTTL) && CacheTTL > 0d;

            public object Evaluate(DateTime now, IPlayer player, string option, bool ignoreCache)
            {
                if (ignoreCache || Cache == null)
                    goto evaluate;
                
                Dictionary<string, KeyValuePair<DateTime, object>> cachedData;
                if (!Cache.TryGetValue(player?.Id ?? string.Empty, out cachedData))
                {
                    Cache[player?.Id ?? string.Empty] = new Dictionary<string, KeyValuePair<DateTime, object>>();
                    goto evaluate;
                }
                
                KeyValuePair<DateTime, object> cached;
                if (cachedData.TryGetValue(option, out cached) && cached.Key > now)
                {
                    return cached.Value;
                }

                evaluate:
                var result = Action.Invoke(player, option);
                if (Cache != null)
                    Cache[player?.Id ?? string.Empty][option] = new KeyValuePair<DateTime, object>(now + CacheTTLParsed, result);
                
                return result;
            }

            /*
             * 1 - Name
             * 2 - Option
             * 3 - Format
             */
            private static readonly Regex InputRegex =
                new Regex(@"{([^!:{}""]+)(?:!([^:{}""]+)|:([^!{}""]+))*?}", RegexOptions.Compiled);

            public static void Run(IPlayer player, StringBuilder builder, bool ignoreCache)
            {
#if DEBUG
                Interface.Oxide.LogDebug($"Executing replacement. Input length: {builder.Length}");

                var replaced = 0;
                var watch = Stopwatch.StartNew();
#endif
                var inputString = builder.ToString();

                var offset = 0;
                var now = DateTime.UtcNow;
                foreach (Match match in InputRegex.Matches(inputString))
                {
                    // Using group numbers instead of named groups. Saves us around 0.9%
                    var nameGroup = match.Groups[1];
                    
                    // builder.ToString because .Value uses string.Substring which is a bit slower. Saves us around 0.3%
                    Placeholder placeholder;
                    if (!_ins._placeholdersByName.TryGetValue(
                            builder.ToString(nameGroup.Index + offset, nameGroup.Length), out placeholder) ||
                        placeholder.Action == null)
                        continue;

                    var optionGroup = match.Groups[2];
                    var option = optionGroup.Success
                        ? builder.ToString(optionGroup.Index + offset, optionGroup.Length)
                        : string.Empty;

                    var formatGroup = match.Groups[3];
                    var formatted = Format(placeholder.Evaluate(now, player, option, ignoreCache),
                        formatGroup.Success ? builder.ToString(formatGroup.Index + offset, formatGroup.Length) : null,
                        option);

                    builder.Remove(match.Index + offset, match.Length);
                    builder.Insert(match.Index + offset, formatted);

                    offset += formatted.Length - match.Length;

#if DEBUG
                    replaced++;
#endif
                }
#if DEBUG
                watch.Stop();

                Interface.Oxide.LogDebug(
                    $"Execution took: {watch.Elapsed.TotalMilliseconds:0.00}ms. Replaced entries: {replaced}. Output length: {builder.Length}. Per entry replaced: {watch.Elapsed.TotalMilliseconds / replaced:0.0000}ms");
#endif
            }

            private static string Format(object value, string format, string option)
            {
                if (value == null)
                    return string.Empty;

                if (value is DateTime && option?.ToLower(CultureInfo.CurrentCulture) == "local")
                    value = (DateTime) value + _ins._config.LocalTimeOffset;

                if (string.IsNullOrEmpty(format))
                    return value.ToString();

                if (value is bool)
                {
                    // string.Split has some awful performance, this lets us have a close one
                    var index = format.IndexOf('|', 1, format.Length - 2);
                    if (index != -1)
                    {
                        if ((bool) value)
                            return format.Substring(0, index);
                        return format.Substring(index);
                    }

                    Interface.Oxide.LogError($"Invalid format for 'bool': {format}");
                }

                var formattable = value as IFormattable;
                return formattable != null ? formattable.ToString(format, _ins._config.Culture) : string.Format('{' + format + '}', value);
            }
        }

        #endregion

        #region Hooks

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                {"Command: List: Not Found", "There are no registered placeholders."},
                {"Command: List: Format", "Placeholders:\n{list}"},
                {"Command: List: Entry Format", "{name} ({owner}, {cache}) - {description}"},
                {"Command: List: Entry Separator", "\n"},
                {"Command: List: No Description", "No description"},
                {"Command: List: No Cache", "No cache"},
                {"Command: Test: Syntax", "Usage: (Text) [player Player] [ignoreCache Ignore Cache]"},
                {"Command: Test: Player Not Found", "Player specified was not found."},
                {"No Permission", "You do not have enough permissions."}
            }, this);
        }

        private void Init()
        {
            _ins = this;

            permission.RegisterPermission(PermissionList, this);
            permission.RegisterPermission(PermissionTest, this);

            AddCovalenceCommand(CommandNameList, nameof(CommandList));
            AddCovalenceCommand(CommandNameTest, nameof(CommandTest));
        }

        private void Loaded()
        {

            foreach (var kvp in _config.Placeholders)
            {
                AddPlaceholder(this, kvp.Key, (p, o) => kvp.Value);
            }

            if (_config.RequestAddressData)
                AddressHandler.RunTimers();
        }

        // Remove all placeholders registered for this plugin
        private void OnPluginUnloaded(Plugin plugin)
        {
            if (ReferenceEquals(this, plugin))
                return;
#if DEBUG
            Interface.Oxide.LogDebug($"{plugin.Title} unloaded, removing existing placeholders..");
#endif
            // I am so sorry.. I did not want to do this, honestly..
            foreach (var kvp in _placeholdersByName.ToArray())
            {
                if (kvp.Value.Owner == plugin)
                    _placeholdersByName.Remove(kvp.Value.Name);
            }
        }

        private void OnPluginLoaded(Plugin plugin)
        {
            if (ReferenceEquals(this, plugin))
                return;
#if DEBUG
            Interface.Oxide.LogDebug($"{plugin.Title} loaded, calling hook!");
#endif
            NextTick(() => CallReady(plugin)); // Wait until reference is set
        }

        private void OnServerInitialized(bool isServer)
        {
#if SEVENDAYSTODIE
            _bloodMoonFrequency = GamePrefs.GetInt(EnumGamePrefs.BloodMoonFrequency);
#endif
            
            RegisterInbuiltPlaceholders();
            
#if DEBUG
            Interface.Oxide.LogDebug($"{nameof(OnServerInitialized)}, calling hook!");
#endif
            if (isServer)
                CallReady(null);
            else // Wait until references are set
                NextTick(() => CallReady(null));

            foreach (var player in players.Connected)
            {
                OnUserApprovedInternal(player, player.Address);
            }
        }

        private void Unload()
        {
            AddressHandler.Clear();
            _ins = null;
        }

        private void OnUserApproved(string username, string id, string address) =>
            OnUserApprovedInternal(players.FindPlayerById(id), address);

        private void OnUserApprovedInternal(IPlayer player, string address)
        {
            // Skip if we request user data and it was NOT cached - hook will be called
            if (_config.RequestAddressData && AddressHandler.Enqueue(player, address))
                return;
            
            Interface.Oxide.CallHook(HookNameAddressDataRetrieved, player, true);
        }

        #endregion

        #region API

        [HookMethod(nameof(ProcessPlaceholders))]
        private void ProcessPlaceholders(IPlayer player, StringBuilder builder, bool ignoreCache = false)
        {
            if (builder == null || builder.Length == 0)
                return;

            Placeholder.Run(player, builder, ignoreCache);
        }

        // Leads to performance boosts (Tests: 30-70%)
        [HookMethod(nameof(GetProcessPlaceholders))]
        private object GetProcessPlaceholders(int version = 0)
        {
            switch (version)
            {
                case 0:
                {
                    return (Action<IPlayer, StringBuilder>) ((player, builder) => ProcessPlaceholders(player, builder));
                }

                case 1:
                {
                    return (Action<IPlayer, StringBuilder, bool>) ProcessPlaceholders;
                }
                
                default:
                {
                    return null;
                }
            }
        }

        [HookMethod(nameof(EvaluatePlaceholder))]
        private object EvaluatePlaceholder(IPlayer player, string name, string option, bool ignoreCache = false)
        {
            Placeholder placeholder;
            return !_placeholdersByName.TryGetValue(name, out placeholder)
                ? null
                : placeholder.Evaluate(DateTime.UtcNow, player, option, ignoreCache);
        }

        [HookMethod(nameof(GetEvaluatePlaceholder))]
        private object GetEvaluatePlaceholder(int version = 0)
        {
            switch (version)
            {
                case 0:
                {
                    return (Func<IPlayer, string, string, object>) ((player, name, option) =>
                        EvaluatePlaceholder(player, name, option));
                }
                
                case 1:
                {
                    return (Func<IPlayer, string, string, bool, object>) EvaluatePlaceholder;
                }

                default:
                {
                    return null;
                }
            }
        }

        [HookMethod(nameof(ExistsPlaceholder))]
        private bool ExistsPlaceholder(string name) => _placeholdersByName.ContainsKey(name);
        
        [HookMethod(nameof(GetAddressData))]
        private JObject GetAddressData(string address)
        {
            if (!_config.RequestAddressData || string.IsNullOrEmpty(address))
                return null;

            JObject json;
            if (AddressHandler.CachedJson.TryGetValue(address, out json))
                return json;

            AddressResponse cached;
            return AddressHandler.Cache.TryGetValue(address, out cached)
                ? AddressHandler.CachedJson[address] = JObject.FromObject(cached)
                : null;
        }

        private static readonly Regex PlaceholderValidNameRegex =
            new Regex(@"^(?:[a-z]|\.)+$", RegexOptions.Compiled | RegexOptions.Singleline);

        [HookMethod(nameof(AddPlaceholder))]
        private bool AddPlaceholder(Plugin plugin, string name, Func<IPlayer, string, object> action,
            string description = null, double cacheTTL = double.NaN)
        {
#if DEBUG
            if (!ReferenceEquals(this, plugin))
                Interface.Oxide.LogDebug($"Adding placeholder ({name}) for {plugin?.Title ?? "null plugin"}..");
#endif
            if (plugin == null)
                return false;

            if (!PlaceholderValidNameRegex.IsMatch(name))
            {
                Interface.Oxide.LogWarning(
                    $"Plugin ({plugin.Title} by {plugin.Author}) tried to register a placeholder with an invalid name ({name})!");

                return false;
            }

            if (ExistsPlaceholder(name))
            {
                Interface.Oxide.LogWarning(
                    $"Plugin ({plugin.Title} by {plugin.Author}) tried to register an already existing placeholder ({name})!");

                return false;
            }

            _placeholdersByName.Add(name, new Placeholder(plugin, name, description, action, cacheTTL));
            return true;
        }

        [HookMethod(nameof(RemovePlaceholder))]
        private void RemovePlaceholder(string name) => _placeholdersByName.Remove(name);

        #endregion

        #region Helpers

        private void CallReady(Plugin plugin)
        {
            if (plugin == null)
                Interface.CallHook(HookNameReady, _config.RequestAddressData);
            else
                plugin.CallHook(HookNameReady, _config.RequestAddressData);
        }

        private string GetMsg(string key, string userId = null) => lang.GetMessage(key, this, userId);
        
#if RUST
        private SaveInfo _saveInfo;

        public DateTime GetLastBlueprintsWipeUniversal(bool time) => !time || _config.BlueprintsWipe.Time == null
            ? _saveInfo.CreationTime.ToUniversalTime()
            : _saveInfo.CreationTime.ToUniversalTime().Date + _config.BlueprintsWipe.Time.Value;
        
        public DateTime GetLastMapWipeUniversal(bool time) => !time || _config.MapWipe.Time == null
            ? SaveRestore.SaveCreatedTime
            : SaveRestore.SaveCreatedTime.Date + _config.MapWipe.Time.Value;
#endif
        private void RegisterInbuiltPlaceholders()
        {
#if RUST
            if (_saveInfo == null)
                _saveInfo = SaveInfo.Create(World.SaveFolderName +
                                            $"/player.blueprints.{Rust.Protocol.persistance}.db");

            AddPlaceholder(this, "world.seed", (p, o) => World.Seed);
            AddPlaceholder(this, "world.salt", (p, o) => World.Salt);
            AddPlaceholder(this, "world.url", (p, o) => World.Url);
            AddPlaceholder(this, "world.name", (p, o) => World.Name);
            AddPlaceholder(this, "world.size", (p, o) =>
            {
                switch (o?.ToLower(CultureInfo.CurrentCulture))
                {
                    case "km":
                    {
                        return World.Size / 1000;
                    }

                    case "km2":
                    case "km^2":
                    {
                        return World.Size * World.Size / (1000 * 1000);
                    }

                    case "m":
                    {
                        return World.Size;
                    }

                    case "m2":
                    case "m^2":
                    {
                        return World.Size * World.Size;
                    }

                    default:
                    {
                        return World.Size;
                    }
                }
            }, "Options: km, km2 (or km^2), m (default), m2 (or m^2)");

            AddPlaceholder(this, "server.description", (p, o) => ConVar.Server.description);
            
            AddPlaceholder(this, "server.protocol.network", (p, o) => Rust.Protocol.network);
            AddPlaceholder(this, "server.protocol.persistance", (p, o) => Rust.Protocol.persistance);
            AddPlaceholder(this, "server.protocol.report", (p, o) => Rust.Protocol.report);
            AddPlaceholder(this, "server.protocol.save", (p, o) => Rust.Protocol.save);

            AddPlaceholder(this, "server.players.stored", (p, o) => BasePlayer.activePlayerList.Count + BasePlayer.sleepingPlayerList.Count);
            AddPlaceholder(this, "server.players.sleepers", (p, o) => BasePlayer.sleepingPlayerList.Count);
            AddPlaceholder(this, "server.players.loading", (p, o) => ServerMgr.Instance.connectionQueue.joining.Count);
            AddPlaceholder(this, "server.players.queued", (p, o) => ServerMgr.Instance.connectionQueue.queue.Count);
            AddPlaceholder(this, "server.entities", (p, o) => BaseNetworkable.serverEntities.Count);
            AddPlaceholder(this, "server.fps", (p, o) => Performance.report.frameRate);
            AddPlaceholder(this, "server.fps.average", (p, o) => Performance.report.frameRateAverage);
            AddPlaceholder(this, "server.oxide.rust.version", (p, o) => _rustCore?.Version,
                "Oxide.Rust version installed");

            AddPlaceholder(this, "server.map.wipe.last", (p, o) => GetLastMapWipeUniversal(true),
                "Options: \"local\" to use local time offset, UTC (default)", 60 * 60);

            AddPlaceholder(this, "server.map.wipe.next",
                (p, o) => _config.MapWipe.GetNextWipeDate(GetLastMapWipeUniversal(false)),
                "Options: \"local\" to use local time offset, UTC (default)", 60 * 60);

            AddPlaceholder(this, "server.map.wipe.last.istoday",
                (p, o) => GetLastMapWipeUniversal(false).Date == DateTime.UtcNow.Date,
                cacheTTL: 60 * 15);

            AddPlaceholder(this, "server.map.wipe.next.istoday",
                (p, o) => _config.MapWipe.GetNextWipeDate(GetLastMapWipeUniversal(false)).Date == DateTime.UtcNow.Date,
                cacheTTL: 60 * 15);

            AddPlaceholder(this, "server.blueprints.wipe.last", (p, o) => GetLastBlueprintsWipeUniversal(true),
                "Options: \"local\" to use local time offset, UTC (default)", 60 * 60);

            AddPlaceholder(this, "server.blueprints.wipe.next",
                (p, o) => _config.BlueprintsWipe.GetNextWipeDate(GetLastBlueprintsWipeUniversal(false)),
                "Options: \"local\" to use local time offset, UTC (default)", 60 * 60);

            AddPlaceholder(this, "server.blueprints.wipe.last.istoday",
                (p, o) => GetLastBlueprintsWipeUniversal(false).Date == DateTime.UtcNow.Date,
                cacheTTL: 60 * 15);

            AddPlaceholder(this, "server.blueprints.wipe.next.istoday",
                (p, o) => _config.MapWipe.GetNextWipeDate(GetLastMapWipeUniversal(false)).Date == DateTime.UtcNow.Date,
                cacheTTL: 60 * 15);

            AddPlaceholder(this, "server.memory.used", (p, o) =>
            {
                // Already MB
                var used = Performance.current.memoryUsageSystem * 1f;
                switch (o?.ToLower(CultureInfo.CurrentCulture))
                {
                    case "kb":
                    {
                        return used * 1024;
                    }

                    case "mb":
                    {
                        return used;
                    }

                    case "gb":
                    {
                        return used / 1024;
                    }

                    default:
                    {
                        return used * (1024 * 1024);
                    }
                }
            }, "Options: B (default), KB, MB, GB", 1d);

            AddPlaceholder(this, "server.memory.total", (p, o) =>
            {
                // Already MB
                var used = UnityEngine.SystemInfo.systemMemorySize * 1f;
                switch (o?.ToLower(CultureInfo.CurrentCulture))
                {
                    case "kb":
                    {
                        return used * 1024;
                    }

                    case "mb":
                    {
                        return used;
                    }

                    case "gb":
                    {
                        return used / 1024;
                    }

                    default:
                    {
                        return used * (1024 * 1024);
                    }
                }
            }, "Options: B (default), KB, MB, GB", 60 * 60 * 4);

            AddPlaceholder(this, "server.network.in", (p, o) =>
            {
                var used = Net.sv.GetStat(null, BaseNetwork.StatTypeLong.BytesReceived_LastSecond) * 1f;
                switch (o?.ToLower(CultureInfo.CurrentCulture))
                {
                    case "kb":
                    case "kb/s":
                    {
                        return used / 1024;
                    }

                    case "mb":
                    case "mb/s":
                    {
                        return used / (1024 * 1024);
                    }

                    case "gb":
                    case "gb/s":
                    {
                        return used / (1024 * 1024 * 1024);
                    }

                    case "kbps":
                    {
                        return used / 1024 * 8;
                    }

                    case "mbps":
                    {
                        return used / (1024 * 1024) * 8;
                    }

                    case "gbps":
                    {
                        return used / (1024 * 1024 * 1024) * 8;
                    }

                    case "bps":
                    {
                        return used * 8;
                    }

                    default:
                    {
                        return used;
                    }
                }
            }, "Options: B (or B/s, default) KB (or KB/s), MB (or MB/s), GB (or GB/s), Bps, Kbps, Mbps, Gbps", 1d);

            AddPlaceholder(this, "server.network.out", (p, o) =>
            {
                var used = Net.sv.GetStat(null, BaseNetwork.StatTypeLong.BytesSent_LastSecond) * 1f;
                switch (o?.ToLower(CultureInfo.CurrentCulture))
                {
                    case "kb":
                    case "kb/s":
                    {
                        return used / 1024;
                    }

                    case "mb":
                    case "mb/s":
                    {
                        return used / (1024 * 1024);
                    }

                    case "gb":
                    case "gb/s":
                    {
                        return used / (1024 * 1024 * 1024);
                    }

                    case "kbps":
                    {
                        return used / 1024 * 8;
                    }

                    case "mbps":
                    {
                        return used / (1024 * 1024) * 8;
                    }

                    case "gbps":
                    {
                        return used / (1024 * 1024 * 1024) * 8;
                    }

                    case "bps":
                    {
                        return used * 8;
                    }

                    default:
                    {
                        return used;
                    }
                }
            }, "Options: B (or B/s, default) KB (or KB/s), MB (or MB/s), GB (or GB/s), Bps, Kbps, Mbps, Gbps", 1d);

            AddPlaceholder(this, "player.hasflag", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                BaseEntity.Flags flag;
                if (!Enum.TryParse(o, true, out flag))
                    return null;

                return basePlayer.HasFlag(flag);
            }, "Options: Flag name (See description on uMod for full list)");

            AddPlaceholder(this, "player.hasplayerflag", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                BasePlayer.PlayerFlags flag;
                if (!Enum.TryParse(o, true, out flag))
                    return null;

                return basePlayer.HasPlayerFlag(flag);
            }, "Options: Player flag name (See description on uMod for full list)");

            AddPlaceholder(this, "player.blueprints.hasunlocked", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                var item = ItemManager.FindItemDefinition(o);
                if (item == null)
                    return null;

                return basePlayer.blueprints.HasUnlocked(item);
            }, "Options: Item shortname", 10d);

            AddPlaceholder(this, "player.stats", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                int value;
                return basePlayer.stats.steam.intStats.TryGetValue(o, out value) ? value : 0;
            }, "Options: Statistics key name");

            AddPlaceholder(this, "player.team.any", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                return basePlayer.currentTeam == 0;
            });

            AddPlaceholder(this, "player.team.leader", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                if (basePlayer.currentTeam == 0)
                    return null;

                return basePlayer.Team.teamLeader;
            });

            AddPlaceholder(this, "player.team.invites", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                if (basePlayer.currentTeam == 0)
                    return null;

                return basePlayer.Team.invites.Count;
            });

            AddPlaceholder(this, "player.team.members", (p, o) =>
            {
                if (string.IsNullOrEmpty(o))
                    return null;

                var basePlayer = p?.Object as BasePlayer;
                if (basePlayer == null)
                    return null;

                if (basePlayer.currentTeam == 0)
                    return null;

                return basePlayer.Team.members.Count;
            });

            AddPlaceholder(this, "player.metabolism", (p, o) 
// ... [Code preview truncated due to length. Full source in attached file] ...

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