//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 ToDictionary() => JsonConvert.DeserializeObject>(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 currentWithDefaults, Dictionary currentRaw) { bool changed = false; foreach (var key in currentWithDefaults.Keys) { object currentRawValue; if (currentRaw.TryGetValue(key, out currentRawValue)) { var defaultDictValue = currentWithDefaults[key] as Dictionary; var currentDictValue = currentRawValue as Dictionary; 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 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(); 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(); 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 Pooled; private uint _min = 0; private uint _max = 200000; public NumberPool(SortedSet stored) { Pooled = stored.IsNullOrEmpty() ? GenerateRange(_min, _max) : stored; } private SortedSet GenerateRange(uint min, uint max) { SortedSet range = new SortedSet(); 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>(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 _storedAvatars = new ConcurrentDictionary(); static ConcurrentDictionary>> _pluginQueuesAvatars = new ConcurrentDictionary>>(); static ConcurrentQueue _pluginOrderAvatar = new ConcurrentQueue(); 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(); } private void PreloadAvatar() { string plugin = "LoadedAvatars"; ConcurrentQueue> queue = _pluginQueuesAvatars.GetOrAdd(plugin, new ConcurrentQueue>()); if (!_storedAvatars.ContainsKey(AvatarKey)) queue.Enqueue(new KeyValuePair(AvatarKey, NoAvatar)); foreach (BasePlayer p in BasePlayer.allPlayerList) { if (!p.userID.IsSteamId() || _storedAvatars.ContainsKey(p.UserIDString)) continue; queue.Enqueue(new KeyValuePair(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 temp = new Dictionary(_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 _subtraction = new ConcurrentQueue(); 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 user)) { List> batch = new List> { 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> 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 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(_storedAvatars.Select(x => new KeyValuePair(x.Key, x.Value.Display)).ToDictionary(x => x.Key, x => x.Value))); } private void RemoveUnusedData(int days) { DateTime threshold = DateTime.UtcNow.AddDays(-days); List 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 _storedImages = new ConcurrentDictionary(); static ConcurrentDictionary> _pluginQueuesImages = new ConcurrentDictionary>(); static ConcurrentQueue _pluginOrderImage = new ConcurrentQueue(); 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(); } 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 temp = new Dictionary(_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 _subtraction = new ConcurrentQueue(); 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 tasks = new List(); 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 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(_storedImages.Select(x => new KeyValuePair(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>(Identifiers)); Dictionary tempA = Interface.Oxide.DataFileSystem.ReadObject>(SteamPics); _storedAvatars = new ConcurrentDictionary(tempA); Dictionary tempI = Interface.Oxide.DataFileSystem.ReadObject>(Images); _storedImages = new ConcurrentDictionary(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 failedAvatars = new List(); List failedImages = new List(); foreach (KeyValuePair kvp in _storedAvatars) { bool success = await ReDownloadAndStoreImage("", kvp.Value); if (!success) { failedAvatars.Add(kvp.Key); } } foreach (KeyValuePair 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 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()); 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{ image }, format, plugin); [HookMethod("AddImages")] public void AddImages(List 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 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 GetImages(List images) { Dictionary send = new Dictionary(); foreach (string image in images) { send[image] = TryGetImage(image).Display; } return send; } [HookMethod("RemoveImage")] public void RemoveImage(string image) => _picture.StartRemoving(new List { image }); [HookMethod("RemoveImages")] public void RemoveImages(List 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")] public void AddAvatar(string player, string url, string plugin) { ConcurrentQueue> queue = _pluginQueuesAvatars.GetOrAdd(plugin, new ConcurrentQueue>()); queue.Enqueue(new KeyValuePair(player, url)); if (queue.IsEmpty) { _pluginQueuesAvatars.TryRemove(plugin, out _); _avatar.Send(plugin); return; } if (!_pluginOrderAvatar.Contains(plugin)) _pluginOrderAvatar.Enqueue(plugin); if (!_avatar.IsAdding) _avatar.StartAdding(plugin); } [HookMethod("AddAvatars")] public void AddAvatars(Dictionary players, string plugin) { ConcurrentQueue> queue = _pluginQueuesAvatars.GetOrAdd(plugin, new ConcurrentQueue>()); foreach (KeyValuePair player in players) { if (_storedAvatars.ContainsKey(player.Key)) continue; queue.Enqueue(new KeyValuePair(player.Key, player.Value)); } if (queue.IsEmpty) { _pluginQueuesAvatars.TryRemove(plugin, out _); _avatar.Send(plugin); return; } if (!_pluginOrderAvatar.Contains(plugin)) _pluginOrderAvatar.Enqueue(plugin); if (!_avatar.IsAdding) _avatar.StartAdding(plugin); } [HookMethod("GetAvatar")] public string GetAvatar(string player) => TryGetAvatar(player).Display; [HookMethod("GetAvatars")] public Dictionary GetAvatars(List players) { Dictionary send = new Dictionary(); foreach (string user in players) { send[user] = TryGetAvatar(user).Display; } return send; } [HookMethod("RemoveAvatar")] public void RemoveAvatar(string player) => _avatar.StartRemoving(new List{player}); [HookMethod("RemoveAvatars")] public void RemoveAvatars(List players) => _avatar.StartRemoving(players); #endregion } }