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

FREE ImageManager : 1.1.2 1.1.2

Нет прав для скачивания
C#:
//Reference: UnityEngine.UnityWebRequestModule

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
using UnityEngine.Networking;
using Application = Rust.Application;

// This plugin fixes several problems with image library & then some.
/*
 * Allows for easy & seamless image storing grabbing and updating.
 * It's able to self repair when the server sv.files have been deleted but a wipe hasn't occured or vice versa.
 * It stores best available quality avatars / pics & images as png files.
 * Stores images with unique identifiers to ensure reliable retrieval/removal.
 * It's much more performant & stores significantly less useless data.
 * Has a unique hook call system so you can specify your plugin and listen for when yours are ready specifically.
 * Has API hooks.

 * Why use this instead of ImageLibrary!? Because it's much simpler and easier to use in your plugins & much more reliable/better-quality-images/performant.
 * Its actually maintained! & doesn't have years of outdated code.. You can actually get support! & An Active Dev. :P
 *
 * More features are in the works.
 *
 * Update 1.1.0
 * Forgot to save a few edits before uploading initial file.
 * Updated API Hook calls for images to specify storage type png or jpg.
 * Rewrote SteamAvatar API webrequest to significantly reduce calls for steam pics. Now does 1 call for 100 pages of avatar pics.
 * Added to config MaxConcurrentImageRequests setter
 * Added to config bool Keep previous wipe info to increase performance / reduce logic checks.
 * Added Manual Reset Console Command.
 * Added code checks for image Gifs & auto convert them to JPG for storage.
 * Added specific logic checks to catch when the sv.file is deleted & a wipe has not occured in the event images have already been stored before.
 * TODO: Option to return only your list or the full list during hook requests.
 * TODO: Auto Remove Data that isn't used past x Days.
 *
 * Added config option RemoveUnUsedData past 30 days.
 * Fixed a reboot issue.
 * Fixed data not saving because oxides shit json handler.
 * Fixed restore issue by adding a check if it can restore prior to attempting..
 * Updated avatar Download attempt to fail-over to http tries after 2 failures with unity..
 * Added auto grab api-key from image-library function for new installers.
 * Steam Users are now tracked when they where last active so that we can auto remove them from storage past x days to reduce data accumulation on in-active players.
 * ^ This is checked each time the plugin loads & updates when that player connects to the server
 * Reset server cmd only is now imagemanager.reset ( do this if you manually deleted the sv.files & didn't wipe.
 *
 * Update 1.1.2
 * Updated Awake methods to fix loaded hook not triggering after defaults where added & reboot occured.
 * Added Preload Methods to ensure the loaded hooks are always triggered on boot.
 * Managed to sneak default logic in before server has initialized if its not the first time installation.
 * Updated Various checks to fix certain scenarios being broken.
 * Updated API to callback the custom plugin hook for each call in the event its already added.
 * Now handles cancellation tokens accordingly
 */
namespace Oxide.Plugins
{
    [Info("Image Manager", "Khan", "1.1.2")]
    [Description("Stores crystal clear steam avatars/Images in png for use in GUI Plugins.")]
    public class ImageManager : RustPlugin
    {
        #region Fields

        private const string NoAvatar = "https://i.imgur.com/MFEb4C2.png";
        private const string NoImage = "https://i.imgur.com/YpxrKZ8.png";
        private const string FolderName = "ImageManager";
        private const string SteamPics = "ImageManager/SteamPics";
        private const string Images = "ImageManager/Images";
        private const string Identifiers = "ImageManager/Identifiers";
        private static ImageManager _instance;
        private static NetworkableId _serverInstance;

        public class Image
        {
            public string Display;
            public uint ImageData;
            public uint Identifier;
            public FileStorage.Type Format;
            public string Url;
            public DateTime LastAccessed;
        }

        #endregion

        #region Config

        private PluginConfig _config;

        private class PluginConfig : SerializableConfiguration
        {
            [JsonProperty("Requires Steam API Key > https://steamcommunity.com/dev/apikey")]
            public string SteamAPIKey = "";

            [JsonProperty("Configuration Settings to help fine tune performance on your machine.")]
            public Options Options = new Options();

            [JsonProperty("Server Instance ID (DO NOT EDIT)")]
            public ulong ServerID;
        }

        private class Options
        {
            [JsonProperty("Keeps Previous Wipe Data")]
            public bool KeepData = true;
            [JsonProperty("Auto Remove Data that isn't used past x Days: Default = 30, 0 = disabled")]
            public int RemoveUnUsedData = 30;
            [JsonProperty("Sets batch limits on Images: Default = 30")]
            public int MaxConcurrentImageRequests = 30;
        }

        internal class SerializableConfiguration
        {
            public string ToJson() => JsonConvert.SerializeObject(this, Formatting.Indented);

            public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
        }

        private bool MaybeUpdateConfig(SerializableConfiguration config)
        {
            var currentWithDefaults = config.ToDictionary();
            var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
            return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
        }

        private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw)
        {
            bool changed = false;

            foreach (var key in currentWithDefaults.Keys)
            {
                object currentRawValue;
                if (currentRaw.TryGetValue(key, out currentRawValue))
                {
                    var defaultDictValue = currentWithDefaults[key] as Dictionary<string, object>;
                    var currentDictValue = currentRawValue as Dictionary<string, object>;

                    if (defaultDictValue != null)
                    {
                        if (currentDictValue == null)
                        {
                            currentRaw[key] = currentWithDefaults[key];
                            changed = true;
                        }
                        else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue))
                            changed = true;
                    }
                }
                else
                {
                    currentRaw[key] = currentWithDefaults[key];
                    changed = true;
                }
            }

            return changed;
        }

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

        [PluginReference]
        Plugin ImageLibrary;
        class ConfigData
        {
            [JsonProperty(PropertyName = "Avatars - Store player avatars")]
            public bool StoreAvatars { get; set; }

            [JsonProperty(PropertyName = "Steam API key (get one here https://steamcommunity.com/dev/apikey)")]
            public string SteamAPIKey { get; set; }

            [JsonProperty(PropertyName = "URL to web folder containing all item icons")]
            public string ImageURL { get; set; }

            [JsonProperty(PropertyName = "Progress - Show download progress in console")]
            public bool ShowProgress { get; set; }

            [JsonProperty(PropertyName = "Progress - Time between update notifications")]
            public int UpdateInterval { get; set; }
            
            [JsonProperty(PropertyName = "User Images - Manually define images to be loaded")]
            public Dictionary<string, string> UserImages { get; set; }

            public Oxide.Core.VersionNumber Version { get; set; }
        }

        private void Import()
        {
            if (!string.IsNullOrEmpty(_config.SteamAPIKey) || ImageLibrary == null) return;
            var get = Config.ReadObject<ConfigData>();
            if (string.IsNullOrEmpty(get.SteamAPIKey)) return;
            get.SteamAPIKey = _config.SteamAPIKey;
            SaveConfig();
        }

        private bool _canRestore;
        protected override void LoadConfig()
        {
            base.LoadConfig();

            try
            {
                _instance = this;
                _config = Config.ReadObject<PluginConfig>();
                if (MaybeUpdateConfig(_config))
                {
                    PrintWarning("Configuration appears to be outdated; updating and saving.");
                    SaveConfig();
                }

                Import();

                if (!Directory.Exists(FolderName))
                    Directory.CreateDirectory(FolderName);
            }
            catch (FileNotFoundException)
            {
                LoadDefaultConfig();
                Import();
                SaveConfig();
            }
            catch (Exception ex)
            {
                PrintWarning($"Failed to load config file (is the config file corrupt?) ({ex.Message})");
            }
        }

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

        #endregion

        #region Number Pooling

        // max 4,294,967,295 i use 0 to 200k
        private static NumberPool _pool;
        private class NumberPool
        {
            public SortedSet<uint> Pooled;
            private uint _min = 0;
            private uint _max = 200000;

            public NumberPool(SortedSet<uint> stored)
            {
                Pooled = stored.IsNullOrEmpty() ? GenerateRange(_min, _max) : stored;
            }

            private SortedSet<uint> GenerateRange(uint min, uint max)
            {
                SortedSet<uint> range = new SortedSet<uint>();
                for (uint i = min; i <= max; i++)
                {
                    range.Add(i);
                }
                return range;
            }

            public uint Min => Pooled.Min;

            public uint Max => Pooled.Max;

            public uint GetNumber()
            {
                if (Pooled.Count == 0)
                    throw new InvalidOperationException("Number pool is empty!? contact dev lol");

                uint number = Min;
                Pooled.Remove(number);
                return number;
            }

            public void ReturnNumber(uint number)
            {
                if (number < _min || number > _max)
                    throw new ArgumentOutOfRangeException(nameof(number), "Number is out of the pool range!?");

                Pooled.Add(number);
            }

            public void Clear() => Pooled.Clear();

            public void SavePool() => Interface.Oxide.DataFileSystem.WriteObject<SortedSet<uint>>(Identifiers, Pooled);
        }

        #endregion

        #region Helpers

        private void ForceUnload()
        {
            Server.Command("o.unload ImageManager");
            Server.Command("c.unload ImageManager");
        }

        private static TextTable CreateTable(string id, string create, string download, string store, string code, string error)
        {
            TextTable textTable = new TextTable();
            textTable.AddColumn("ID");
            textTable.AddColumn("Failed to Create URL");
            textTable.AddColumn("Failed to Download URL");
            textTable.AddColumn("Failed to Store URL");
            textTable.AddColumn("Code");
            textTable.AddColumn("Error");
            string[] textData = new string[6];
            textData[0] = id;
            textData[1] = create;
            textData[2] = download;
            textData[3] = store;
            textData[4] = code;
            textData[5] = error;
            textTable.AddRow(textData);
            return textTable;
        }

        private static bool IsStored(uint data, uint id) => FileStorage.server.Get(data, FileStorage.Type.png, _serverInstance, id) != null;

        private void ReLoad()
        {
            Image data;
            if (_storedImages.TryGetValue(NoImage, out data))
            {
                if (!IsStored(data.ImageData, data.Identifier))
                    _canRestore = true;
            }

            if (_canRestore && _config.Options.RemoveUnUsedData != 0)
                RemoveUnusedData(_config.Options.RemoveUnUsedData);

            PictureMono();
            AvatarMono();
        }

        #endregion

        #region Avatar Data

        private const string AvatarKey = "0";
        static ConcurrentDictionary<string, Image> _storedAvatars = new ConcurrentDictionary<string, Image>();
        static ConcurrentDictionary<string, ConcurrentQueue<KeyValuePair<string, string>>> _pluginQueuesAvatars = new ConcurrentDictionary<string, ConcurrentQueue<KeyValuePair<string, string>>>();
        static ConcurrentQueue<string> _pluginOrderAvatar = new ConcurrentQueue<string>();
        static Avatar _avatar;
        private GameObject _avatarObject;
        private void AvatarMono(bool remove = false)
        {
            if (remove)
            {
                if (_avatarObject != null)
                    UnityEngine.Object.Destroy(_avatarObject);
                return;
            }

            _avatarObject = new GameObject("ImageManagerAvatar");
            _avatar = _avatarObject.AddComponent<Avatar>();
        }

        private void PreloadAvatar()
        {
            string plugin = "LoadedAvatars";
            ConcurrentQueue<KeyValuePair<string, string>> queue = _pluginQueuesAvatars.GetOrAdd(plugin, new ConcurrentQueue<KeyValuePair<string, string>>());
            if (!_storedAvatars.ContainsKey(AvatarKey))
                queue.Enqueue(new KeyValuePair<string, string>(AvatarKey, NoAvatar));

            foreach (BasePlayer p in BasePlayer.allPlayerList)
            {
                if (!p.userID.IsSteamId() || _storedAvatars.ContainsKey(p.UserIDString))
                    continue;

                queue.Enqueue(new KeyValuePair<string, string>(p.UserIDString, ""));
            }

            if (!_pluginOrderAvatar.Contains(plugin))
                _pluginOrderAvatar.Enqueue(plugin);
        }

        private static Image TryGetAvatar(string player = AvatarKey) => _storedAvatars.TryGetValue(player, out Image data) ? data : _storedAvatars[AvatarKey];

        private static void SaveAvatars()
        {
            Dictionary<string, Image> temp = new Dictionary<string, Image>(_storedAvatars);
            Interface.Oxide.DataFileSystem.WriteObject(SteamPics, temp);
        }

        private class Avatar : MonoBehaviour
        {
            public bool IsAdding = false;
            public bool IsRemoving = false;
            private uint _default;
            private static string _apiKey;
            private static readonly string BaseUrl = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/";
            private static ConcurrentQueue<string> _subtraction = new ConcurrentQueue<string>();
            private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

            private void Awake()
            {
                _apiKey = _instance._config.SteamAPIKey;
                if (!_pluginOrderAvatar.IsEmpty)
                    StartAdding(_pluginOrderAvatar.First());
            }

            public void OnDestroy()
            {
                if (Application.isQuitting)
                    return;

                StopAllCoroutines();
                _cancellationTokenSource.Cancel();
                SaveAvatars();
            }

            public void StartAdding(string plugin)
            {
                if (IsAdding) return;
                IsAdding = true;
                StartCoroutine(ProcessAddition(plugin, _cancellationTokenSource.Token));
            }

            private IEnumerator ProcessAddition(string plugin, CancellationToken token)
            {
                while (_pluginQueuesAvatars[plugin].TryDequeue(out KeyValuePair<string, string> user))
                {
                    List<KeyValuePair<string, string>> batch = new List<KeyValuePair<string, string>> { user };
                    while (batch.Count < 100 && _pluginQueuesAvatars[plugin].TryDequeue(out user))
                    {
                        batch.Add(user);
                    }

                    yield return Batch(batch, token);
                }

                Send(plugin);
                _pluginOrderAvatar.TryDequeue(out _);

                if (_pluginOrderAvatar.TryPeek(out string nextPlugin))
                    StartCoroutine(ProcessAddition(nextPlugin, token));
                else if (plugin != "LoadedAvatars")
                    Send("Added");

                if (_pluginOrderAvatar.IsEmpty)
                {
                    IsAdding = false;
                    SaveAvatars();
                    _pool.SavePool();
                }
            }

            private IEnumerator Batch(List<KeyValuePair<string, string>> batch, CancellationToken token)
            {
                string[] steamIds = batch.Select(player => player.Key).ToArray();
                string url = $"{BaseUrl}?key={_apiKey}&steamids={string.Join(",", steamIds)}";

                UnityWebRequest www = UnityWebRequest.Get(url);
                UnityWebRequestAsyncOperation operation = www.SendWebRequest();
                yield return new WaitUntil(() => operation.isDone);

                UnityWebRequest.Result status = www.result;
                if (status == UnityWebRequest.Result.ConnectionError || status == UnityWebRequest.Result.ProtocolError)
                {
                    LogError(CreateTable(null, "False", "True", "False", $"{status}", www.error));
                    www.Dispose();
                    yield break;
                }

                ProcessBatchResult(www.downloadHandler.text, token);
                www.Dispose();
            }

            private void ProcessBatchResult(string json, CancellationToken token)
            {
                JObject response = JObject.Parse(json);
                JToken players = response["response"]["players"];

                foreach (JToken player in players)
                {
                    ulong userId = ulong.Parse(player["steamid"].ToString());
                    string avatarUrl = player["avatarfull"].ToString();
                    _ = DownloadAvatar($"{userId}", avatarUrl, token);
                }
            }

            private async Task DownloadAvatar(string user, string url, CancellationToken token)
            {
                int retryCount = 0;
                const int maxRetries = 2;

                while (retryCount < maxRetries)
                {
                    using (UnityWebRequest www = UnityWebRequest.Get(url))
                    {
                        UnityWebRequestAsyncOperation operation = www.SendWebRequest();
                        Task timeout = Task.Delay(10000);
                        Task completedTask = await Task.WhenAny(WaitForRequest(operation, token), timeout);

                        if (completedTask == timeout || token.IsCancellationRequested)
                        {
                            LogError(CreateTable(null, "False", "True", "False", null, "Timed Out waiting for Download."));
                            retryCount++;
                            continue;
                        }

                        if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError)
                        {
                            LogError(CreateTable($"{user}", "False", "True", "False", $"{www.result}", www.error));
                            retryCount++;
                            continue;
                        }

                        Result(www, user);
                        return;
                    }
                }

                // try www
                retryCount = 0;
                while (retryCount < maxRetries)
                {
                    WWW www = new WWW(url);
                    float startTime = Time.time;
                    while (!www.isDone)
                    {
                        if (Time.time - startTime >= 10f || token.IsCancellationRequested)
                        {
                            LogError(CreateTable(null, "False", "True", "False", null, "Timed Out waiting for WWW Download."));
                            retryCount++;
                            break;
                        }
                        await Task.Yield();
                    }
                    if (!string.IsNullOrEmpty(www.error))
                    {
                        LogError(CreateTable($"{user}", "False", "True", "False", "Exception", www.error));
                        retryCount++;
                        continue;
                    }

                    WWWResult(www.bytes, user, url);
                    return;
                }

                _storedAvatars.TryRemove(user, out _); // Remove avatar if all retries fail
            }

            private void WWWResult(byte[] data, string user, string url)
            {
                if (data != null)
                {
                    Texture2D texture = new Texture2D(2, 2);
                    texture.LoadImage(data);
                    if (texture != null)
                    {
                        byte[] encodedData = texture.EncodeToJPG();

                        if (encodedData == null || encodedData.Length > 3145728)
                        {
                            LogError(CreateTable(null, "False", "False", "True", null, "Image was too large to store."));
                            DestroyImmediate(texture);
                            return;
                        }

                        uint id = _pool.GetNumber();
                        uint image = FileStorage.server.Store(encodedData, FileStorage.Type.jpg, _serverInstance, id);
                        _storedAvatars[user] = new Image { Display = $"{image}", ImageData = image, Identifier = id, Format = FileStorage.Type.jpg, Url = url, LastAccessed = DateTime.UtcNow };

                        if (user == AvatarKey)
                            _default = _storedAvatars[AvatarKey].Identifier;

                        DestroyImmediate(texture);
                    }
                }
            }

            private void Result(UnityWebRequest www, string user)
            {
                if (www.downloadHandler?.data != null)
                {
                    Texture2D texture = new Texture2D(2, 2);
                    texture.LoadImage(www.downloadHandler.data);
                    if (texture != null)
                    {
                        byte[] data = texture.EncodeToJPG();

                        if (data == null || data.Length > 3145728)
                        {
                            LogError(CreateTable(null, "False", "False", "True", null, "Image was too large to store."));
                            DestroyImmediate(texture);
                            return;
                        }

                        uint id = _pool.GetNumber();
                        uint image = FileStorage.server.Store(data, FileStorage.Type.jpg, _serverInstance, id);
                        _storedAvatars[user] = new Image { Display = $"{image}", ImageData = image, Identifier = id, Format = FileStorage.Type.jpg, Url = www.url, LastAccessed = DateTime.UtcNow };
                        if (user == AvatarKey)
                            _default = _storedAvatars[AvatarKey].Identifier;

                        DestroyImmediate(texture);
                    }
                }
            }

            private async Task WaitForRequest(UnityWebRequestAsyncOperation operation, CancellationToken token)
            {
                while (!operation.isDone)
                {
                    if (token.IsCancellationRequested)
                        return;

                    await Task.Yield();
                }
            }

            private void LogError(TextTable textTable) => _instance.PrintError($"{textTable}");

            public void StartRemoving(List<string> players)
            {
                foreach (string user in players)
                    _subtraction.Enqueue(user);

                if (IsRemoving) return;
                IsRemoving = true;
                StartCoroutine(RemoveAvatars());
            }

            private IEnumerator RemoveAvatars()
            {
                const float delayBetweenDeletions = 0.3f;

                while (_subtraction.TryDequeue(out string user))
                {
                    Image data = TryGetAvatar(user);
                    uint id = data.Identifier;
                    if (id == _default || !IsStored(data.ImageData, id))
                        continue;

                    FileStorage.server.RemoveExact(data.ImageData, FileStorage.Type.png, _serverInstance, id);
                    _pool.ReturnNumber(id);
                    _storedAvatars.TryRemove(user, out _);
                    yield return new WaitForSeconds(delayBetweenDeletions);
                }

                IsRemoving = false;
                Send("RemovedAvatars");
            }

            public void Send(string plugin) => Interface.CallHook($"ImageManager{plugin}", new Dictionary<string, string>(_storedAvatars.Select(x => new KeyValuePair<string, string>(x.Key, x.Value.Display)).ToDictionary(x => x.Key, x => x.Value)));
        }

        private void RemoveUnusedData(int days)
        {
            DateTime threshold = DateTime.UtcNow.AddDays(-days);
            List<string> keysToRemove = _storedAvatars
                .Where(kvp => kvp.Value.LastAccessed < threshold)
                .Select(kvp => kvp.Key)
                .ToList();

            if (keysToRemove.Count > 0)
                foreach (string key in keysToRemove)
                {
                    if (_storedAvatars.TryRemove(key, out Image image))
                        FileStorage.server.RemoveExact(image.ImageData, image.Format, _serverInstance, image.Identifier);
                }
        }

        #endregion

        #region Image Data

        private static int _maxConcurrentImageRequests = 30;
        static ConcurrentDictionary<string, Image> _storedImages = new ConcurrentDictionary<string, Image>();
        static ConcurrentDictionary<string, ConcurrentQueue<(string, FileStorage.Type)>> _pluginQueuesImages = new ConcurrentDictionary<string, ConcurrentQueue<(string, FileStorage.Type)>>();
        static ConcurrentQueue<string> _pluginOrderImage = new ConcurrentQueue<string>();
        static Picture _picture;
        private GameObject _pictureObject;
        private bool _isLoaded = false;

        private void PictureMono(bool remove = false)
        {
            if (remove)
            {
                if (_pictureObject != null)
                    UnityEngine.Object.Destroy(_pictureObject);
                return;
            }

            _pictureObject = new GameObject("ImageManagerPicture");
            _picture = _pictureObject.AddComponent<Picture>();
        }

        private void PreloadImages()
        {
            string plugin = "LoadedImages";
            ConcurrentQueue<(string, FileStorage.Type)> queue = _pluginQueuesImages.GetOrAdd(plugin, new ConcurrentQueue<(string, FileStorage.Type)>());
            if (!_storedImages.ContainsKey(NoImage))
                queue.Enqueue((NoImage, FileStorage.Type.png));

            _pluginOrderImage.Enqueue(plugin);
        }

        private static Image TryGetImage(string image) => _storedImages.TryGetValue(image, out Image data) ? data : _storedImages[NoImage];
        private static void SaveImages()
        {
            Dictionary<string, Image> temp = new Dictionary<string, Image>(_storedImages);
            Interface.Oxide.DataFileSystem.WriteObject(Images, temp);
        }

        private class Picture : MonoBehaviour
        {
            public bool IsAdding = false;
            public bool IsRemoving = false;
            private uint _default;
            private static ConcurrentQueue<string> _subtraction = new ConcurrentQueue<string>();
            private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

            private void Awake()
            {
                if (!_pluginOrderImage.IsEmpty)
                    StartAdding(_pluginOrderImage.First());
            }

            public void OnDestroy()
            {
                if (Application.isQuitting)
                    return;

                StopAllCoroutines();
                _cancellationTokenSource.Cancel();
                SaveImages();
            }

            public void StartAdding(string plugin)
            {
                if (IsAdding) return;
                IsAdding = true;
                StartCoroutine(ProcessAddition(plugin, _cancellationTokenSource.Token));
            }

            private IEnumerator ProcessAddition(string plugin, CancellationToken token)
            {
                while (_pluginQueuesImages.TryGetValue(plugin, out ConcurrentQueue<(string, FileStorage.Type)> queue) && queue.TryDequeue(out (string, FileStorage.Type format) image))
                {
                    List<(string, FileStorage.Type)> batch = new List<(string, FileStorage.Type)>();
                    while (batch.Count < _maxConcurrentImageRequests && queue.TryDequeue(out image))
                    {
                        batch.Add(image);
                    }

                    yield return Batch(batch, token);
                }

                Send(plugin);
                _pluginOrderImage.TryDequeue(out _);

                if (_pluginOrderImage.TryPeek(out string nextPlugin))
                    StartCoroutine(ProcessAddition(nextPlugin, token));
                else if (plugin != "LoadedImages")
                {
                    Send("Added");
                }

                if (_pluginOrderImage.IsEmpty)
                {
                    IsAdding = false;
                    SaveImages();
                    _pool.SavePool();
                }
            }

            private IEnumerator Batch(List<(string, FileStorage.Type)> batch, CancellationToken token)
            {
                List<Task> tasks = new List<Task>();

                foreach ((string url, FileStorage.Type format) in batch)
                    tasks.Add(Download(url, format, token));

                yield return new WaitUntil(() => tasks.All(t => t.IsCompleted));
            }

            private async Task Download(string url, FileStorage.Type format, CancellationToken token)
            {
                if (string.IsNullOrEmpty(url))
                    return;

                UnityWebRequest www = UnityWebRequest.Get(url);
                UnityWebRequestAsyncOperation operation = www.SendWebRequest();
                Task timeout = Task.Delay(10000, token);
                Task completedTask = await Task.WhenAny(WaitForRequest(operation, token), timeout);

                if (completedTask == timeout || token.IsCancellationRequested)
                {
                    LogError(CreateTable(null, "False", "True", "False", null, "Timed Out waiting for Download."));
                    www.Dispose();
                    return;
                }

                Result(www, url, format);
            }

            private async Task WaitForRequest(UnityWebRequestAsyncOperation operation, CancellationToken token)
            {
                while (!operation.isDone)
                {
                    if (token.IsCancellationRequested)
                        return;

                    await Task.Yield();
                }
            }

            private void Result(UnityWebRequest www, string url, FileStorage.Type format)
            {
                UnityWebRequest.Result status = www.result;
                if (status == UnityWebRequest.Result.ConnectionError || status == UnityWebRequest.Result.ProtocolError)
                {
                    LogError(CreateTable(null, "False", "True", "False", $"{status}", www.error));
                    www.Dispose();
                    return;
                }

                if (www.downloadHandler?.data != null)
                {
                    Texture2D texture = new Texture2D(2, 2);
                    texture.LoadImage(www.downloadHandler.data);
                    if (texture != null)
                    {
                        if (www.GetResponseHeader("Content-Type") == "image/gif")
                            format = FileStorage.Type.jpg;

                        byte[] data = format == FileStorage.Type.png ? texture.EncodeToPNG() : texture.EncodeToJPG();

                        if (data.Length > 3145728)
                        {
                            LogError(CreateTable(null, "False", "False", "True", null, "Image was too large to store."));
                            www.Dispose();
                            DestroyImmediate(texture);
                            return;
                        }

                        uint id = _pool.GetNumber();
                        uint image = FileStorage.server.Store(data, format, _serverInstance, id);
                        _storedImages[url] = new Image { Display = $"{image}", ImageData = image, Identifier = id, Format = format };
                        if (url == NoImage)
                            _default = id;
                        
                        DestroyImmediate(texture);
                    }
                }
                www.Dispose();
            }

            private void LogError(TextTable textTable) => _instance.PrintError($"{textTable}");

            public void StartRemoving(List<string> images)
            {
                foreach (string image in images)
                    _subtraction.Enqueue(image);

                if (IsRemoving) return;
                IsRemoving = true;
                StartCoroutine(RemoveImages(_cancellationTokenSource.Token));
            }

            private IEnumerator RemoveImages(CancellationToken token)
            {
                const float delayBetweenDeletions = 0.3f;

                while (_subtraction.TryDequeue(out string img))
                {
                    if (token.IsCancellationRequested)
                        yield break;

                    Image data = TryGetImage(img);
                    uint id = data.Identifier;
                    if (id == _default || !IsStored(data.ImageData, id))
                        continue;

                    FileStorage.server.RemoveExact(data.ImageData, data.Format, _serverInstance, id);
                    _pool.ReturnNumber(id);
                    _storedImages.TryRemove(img, out _);
                    yield return new WaitForSeconds(delayBetweenDeletions);
                }

                IsRemoving = false;
                Send("RemovedImages");
            }

            public void Send(string plugin) => Interface.CallHook($"ImageManager{plugin}", new Dictionary<string, string>(_storedImages.Select(x => new KeyValuePair<string, string>(x.Key, x.Value.Display)).ToDictionary(x => x.Key, x => x.Value)));
        }

        #endregion

        #region Hooks

        // hook order loaded > init > OnServerInitialized
        private bool _restart = false;
        private void Init()
        {
            if (string.IsNullOrEmpty(_config.SteamAPIKey))
            {
                PrintError("Steam API key is invalid or missing check config or create a new one at https://steamcommunity.com/dev/apikey");
                ForceUnload();
            }

            _maxConcurrentImageRequests = _config.Options.MaxConcurrentImageRequests;
            _pool = new NumberPool(Interface.Oxide.DataFileSystem.ReadObject<SortedSet<uint>>(Identifiers));

            Dictionary<string, Image> tempA = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, Image>>(SteamPics);
            _storedAvatars = new ConcurrentDictionary<string, Image>(tempA);

            Dictionary<string, Image> tempI = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, Image>>(Images);
            _storedImages = new ConcurrentDictionary<string, Image>(tempI);

            PreloadAvatar();
            PreloadImages();

            if (_config.ServerID != 0)
                _serverInstance = new NetworkableId(_config.ServerID);
            else
            {
                _restart = true;
                return;
            }

            ReLoad();
        }

        // does not trigger for sv.file deletions only wipes.
        private async void OnNewSave(string filename)
        {
            if (_config.Options.KeepData && _canRestore)
                await RestoreData();
        }

        // rewrite code to do image downloads OnServerInitialized because i don't think its actually downloading anything with unitys webrequest.
        private void OnServerInitialized()
        {
            _serverInstance = CommunityEntity.ServerInstance.net.ID; // not available before ServerInitialized on restarts.
            if (_config.ServerID == 0 || _config.ServerID != _serverInstance.Value)
            {
                _config.ServerID = _serverInstance.Value;
                if (!_config.Options.KeepData && _canRestore)
                {
                    AvatarMono(_avatarObject != null);
                    PictureMono(_pictureObject != null);
                    ResetData();
                }

                SaveConfig();
                Server.Command("o.reload ImageManager");
                Server.Command("c.reload ImageManager");
                return;
            }

            PreloadAvatar();
            if (!_restart && !_avatar.IsAdding)
                _avatar.StartAdding(_pluginOrderAvatar.First());

            if (_restart)
                ReLoad();

            Puts("Steam API Key is Valid, ImageManager data is loading.");
        }

        private void Unload()
        {
            _pool.SavePool();
            AvatarMono(_avatarObject != null);
            PictureMono(_pictureObject != null);
            if (Application.isQuitting)
                return;

            _pool = null;
            _instance = null;
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            string user = player.UserIDString;
            if (!user.IsSteamId() || player.userID == 0)
                return;

            if (_storedAvatars.TryGetValue(user, out Image image))
                image.LastAccessed = DateTime.UtcNow;
            else
                AddAvatar(user, "", "PlayerConnected");
        }

        #endregion

        #region Reset & Restore Data

        private async Task RestoreData()
        {
            List<string> failedAvatars = new List<string>();
            List<string> failedImages = new List<string>();

            foreach (KeyValuePair<string, Image> kvp in _storedAvatars)
            {
                bool success = await ReDownloadAndStoreImage("", kvp.Value);
                if (!success)
                {
                    failedAvatars.Add(kvp.Key);
                }
            }

            foreach (KeyValuePair<string, Image> kvp in _storedImages)
            {
                bool success = await ReDownloadAndStoreImage(kvp.Key, kvp.Value);
                if (!success)
                {
                    failedImages.Add(kvp.Key);
                }
            }

            foreach (string avatar in failedAvatars)
            {
                _storedAvatars.TryRemove(avatar, out _);
            }

            foreach (string image in failedImages)
            {
                _storedImages.TryRemove(image, out _);
            }

            _pool.SavePool();
            SaveAvatars();
            SaveImages();
        }

        private async Task<bool> ReDownloadAndStoreImage(string url, Image image)
        {
            if (string.IsNullOrEmpty(url))
                url = image.Url;

            using (UnityWebRequest www = UnityWebRequest.Get(url))
            {
                UnityWebRequestAsyncOperation operation = www.SendWebRequest();
                Task timeout = Task.Delay(10000);
                Task completedTask = await Task.WhenAny(WaitForRequest(operation), timeout);

                if (completedTask == timeout)
                {
                    _instance.PrintError($"{CreateTable(null, "False", "True", "False", null, "Timed Out waiting for Download.")}");
                    _pool.ReturnNumber(image.Identifier);
                    return false;
                }

                UnityWebRequest.Result status = www.result;
                if (status == UnityWebRequest.Result.ConnectionError || status == UnityWebRequest.Result.ProtocolError)
                {
                    _instance.PrintError($"{CreateTable(null, "False", "True", "False", $"{status}", www.error)}");
                    _pool.ReturnNumber(image.Identifier);
                    return false;
                }

                if (www.downloadHandler?.data != null)
                {
                    Texture2D texture = new Texture2D(2, 2);
                    texture.LoadImage(www.downloadHandler.data);
                    if (texture != null)
                    {
                        byte[] data;
                        if (image.Format == FileStorage.Type.jpg || www.GetResponseHeader("Content-Type") == "image/gif")
                        {
                            data = texture.EncodeToJPG();
                            image.Format = FileStorage.Type.jpg;
                        }
                        else
                            data = texture.EncodeToPNG();

                        uint newImage = FileStorage.server.Store(data, image.Format, _serverInstance, image.Identifier);
                        image.ImageData = newImage;
                        image.Display = $"{newImage}";
                        UnityEngine.Object.DestroyImmediate(texture, false);
                        return true;
                    }
                }
            }

            _pool.ReturnNumber(image.Identifier);
            return false;
        }

        private async Task WaitForRequest(UnityWebRequestAsyncOperation operation)
        {
            while (!operation.isDone)
            {
                await Task.Yield();
            }
        }

        private void ResetData()
        {
            _storedAvatars.Clear();
            _storedImages.Clear();
            _pool = new NumberPool(new SortedSet<uint>());
            SaveImages();
            SaveAvatars();
            _pool.SavePool();
        }

        [ConsoleCommand("imagemanager.reset")]
        private void ResetDataCmd(ConsoleSystem.Arg arg)
        {
            bool player = arg.IsServerside;
            if (!player) return;
            ResetData();
        }

        #endregion

        #region API Images

        [HookMethod("AddImage")]
        public void AddImage(string image, FileStorage.Type format, string plugin) => AddImages(new List<string>{ image }, format, plugin);

        [HookMethod("AddImages")]
        public void AddImages(List<string> images, FileStorage.Type format, string plugin)
        {
            ConcurrentQueue<(string, FileStorage.Type)> queue = _pluginQueuesImages.GetOrAdd(plugin, new ConcurrentQueue<(string, FileStorage.Type)>());
            foreach (string image in images)
            {
                if (_storedImages.ContainsKey(image))
                    continue;

                queue.Enqueue((image, format));
            }

            if (queue.IsEmpty)
            {
                _pluginQueuesImages.TryRemove(plugin, out _);
                _picture.Send(plugin);
                return;
            }

            if (!_pluginOrderImage.Contains(plugin))
                _pluginOrderImage.Enqueue(plugin);

            if (!_picture.IsAdding)
                _picture.StartAdding(plugin);
        }

        [HookMethod("AddImages")]
        public void AddImages(Dictionary<string, FileStorage.Type> images, string plugin)
        {
            ConcurrentQueue<(string, FileStorage.Type)> queue = _pluginQueuesImages.GetOrAdd(plugin, new ConcurrentQueue<(string, FileStorage.Type)>());
            foreach (var image in images)
            {
                if (_storedImages.ContainsKey(image.Key))
                    continue;

                queue.Enqueue((image.Key, image.Value));
            }

            if (queue.IsEmpty)
            {
                _pluginQueuesImages.TryRemove(plugin, out _);
                _picture.Send(plugin);
                return;
            }

            if (!_pluginOrderImage.Contains(plugin))
                _pluginOrderImage.Enqueue(plugin);

            if (!_picture.IsAdding)
                _picture.StartAdding(plugin);
        }

        [HookMethod("GetImage")]
        public string GetImage(string image) => TryGetImage(image).Display;

        [HookMethod("GetImages")]
        public Dictionary<string, string> GetImages(List<string> images)
        {
            Dictionary<string, string> send = new Dictionary<string, string>();
            foreach (string image in images)
            {
                send[image] = TryGetImage(image).Display;
            }

            return send;
        }

        [HookMethod("RemoveImage")]
        public void RemoveImage(string image) => _picture.StartRemoving(new List<string> { image });

        [HookMethod("RemoveImages")]
        public void RemoveImages(List<string> images) => _picture.StartRemoving(images);

        #endregion

        #region API Avatars

        [HookMethod("AddAvatar")]
        public void AddAvatar(ulong player, string url, string plugin) => AddAvatar(player.ToString(), url, plugin);

        [HookMethod("AddAvatar")]
        
// ... [Code preview truncated due to length. Full source in attached file] ...

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