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] ...