//Reference: Facepunch.Sqlite
//Reference: UnityEngine.UnityWebRequestModule
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Steamworks;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
namespace Oxide.Plugins
{
[Info("Image Library", "Absolut & K1lly0u", "2.0.62")]
[Description("Plugin API for downloading and managing images")]
class ImageLibrary : RustPlugin
{
#region Fields
private ImageIdentifiers imageIdentifiers;
private ImageURLs imageUrls;
private SkinInformation skinInformation;
private DynamicConfigFile identifiers;
private DynamicConfigFile urls;
private DynamicConfigFile skininfo;
private static ImageLibrary il;
private ImageAssets assets;
private Queue<LoadOrder> loadOrders = new Queue<LoadOrder>();
private bool orderPending;
private bool isInitialized;
private JsonSerializerSettings errorHandling = new JsonSerializerSettings { Error = (se, ev) => { ev.ErrorContext.Handled = true; } };
private const string STEAM_API_URL = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/";
private const string STEAM_AVATAR_URL = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={0}&steamids={1}";
private string[] itemShortNames;
#endregion Fields
#region Oxide Hooks
private void Loaded()
{
identifiers = Interface.Oxide.DataFileSystem.GetFile("ImageLibrary/image_data");
urls = Interface.Oxide.DataFileSystem.GetFile("ImageLibrary/image_urls");
skininfo = Interface.Oxide.DataFileSystem.GetFile("ImageLibrary/skin_data");
il = this;
LoadData();
}
private void OnServerInitialized()
{
itemShortNames = ItemManager.itemList.Select(x => x.shortname).ToArray();
foreach (ItemDefinition item in ItemManager.itemList)
{
string workshopName = item.displayName.english.ToLower().Replace("skin", "").Replace(" ", "").Replace("-", "");
if (!workshopNameToShortname.ContainsKey(workshopName))
workshopNameToShortname.Add(workshopName, item.shortname);
}
AddDefaultUrls();
CheckForRefresh();
foreach (BasePlayer player in BasePlayer.activePlayerList)
OnPlayerConnected(player);
}
private void OnPlayerConnected(BasePlayer player) => GetPlayerAvatar(player?.UserIDString);
private void Unload()
{
SaveData();
UnityEngine.Object.Destroy(assets);
il = null;
}
#endregion Oxide Hooks
#region Functions
private IEnumerator ProcessLoadOrders()
{
yield return new WaitWhile(() => !isInitialized);
if (loadOrders.Count > 0)
{
if (orderPending)
yield break;
LoadOrder nextLoad = loadOrders.Dequeue();
if (!nextLoad.loadSilent)
Puts("Starting order " + nextLoad.loadName);
if (nextLoad.imageList != null && nextLoad.imageList.Count > 0)
{
foreach (KeyValuePair<string, string> item in nextLoad.imageList)
assets.Add(item.Key, item.Value);
}
if (nextLoad.imageData != null && nextLoad.imageData.Count > 0)
{
foreach (KeyValuePair<string, byte[]> item in nextLoad.imageData)
assets.Add(item.Key, null, item.Value);
}
orderPending = true;
assets.RegisterCallback(nextLoad.callback);
assets.BeginLoad(nextLoad.loadSilent ? string.Empty : nextLoad.loadName);
}
}
private void GetPlayerAvatar(string userId)
{
if (!configData.StoreAvatars || string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(configData.SteamAPIKey) || HasImage(userId, 0))
return;
webrequest.Enqueue(string.Format(STEAM_AVATAR_URL, configData.SteamAPIKey, userId), null, (code, response) =>
{
if (response != null && code == 200)
{
try
{
AvatarRoot rootObject = JsonConvert.DeserializeObject<AvatarRoot>(response, errorHandling);
if (rootObject?.response?.players?.Length > 0)
{
string avatarUrl = rootObject.response.players[0].avatarmedium;
if (!string.IsNullOrEmpty(avatarUrl))
AddImage(avatarUrl, userId, 0);
}
}
catch { }
}
}, this);
}
private void RefreshImagery()
{
imageIdentifiers.imageIds.Clear();
imageIdentifiers.lastCEID = CommunityEntity.ServerInstance.net.ID.Value;
AddImage("http://i.imgur.com/sZepiWv.png", "NONE", 0);
AddImage("http://i.imgur.com/lydxb0u.png", "LOADING", 0);
foreach (KeyValuePair<string, string> image in configData.UserImages)
{
if (!string.IsNullOrEmpty(image.Value))
AddImage(image.Value, image.Key, 0);
}
if ((Steamworks.SteamInventory.Definitions?.Length ?? 0) == 0)
{
PrintWarning("Waiting for Steamworks to update item definitions....");
Steamworks.SteamInventory.OnDefinitionsUpdated += GetItemSkins;
}
else GetItemSkins();
}
private void CheckForRefresh()
{
if (assets == null)
assets = new GameObject("WebObject").AddComponent<ImageAssets>();
isInitialized = true;
if (imageIdentifiers.lastCEID != CommunityEntity.ServerInstance.net.ID.Value)
{
if (imageIdentifiers.imageIds.Count < 2)
{
RefreshImagery();
}
else
{
PrintWarning("The CommunityEntity instance ID has changed! Due to the way CUI works in Rust all previously stored images must be removed and re-stored using the new ID as reference so clients can find the images. These images will be added to a new load order. Interupting this process will result in being required to re-download these images from the web");
RestoreLoadedImages();
}
}
}
private void RestoreLoadedImages()
{
orderPending = true;
try
{
Facepunch.Sqlite.Database db = new Facepunch.Sqlite.Database();
db.Open(string.Concat(ConVar.Server.rootFolder, "/", "sv.files.", Rust.Protocol.save - 1, ".db"));
if (db.TableExists("data"))
{
Dictionary<string, byte[]> oldFiles = new Dictionary<string, byte[]>();
int failed = 0;
for (int i = imageIdentifiers.imageIds.Count - 1; i >= 0; i--)
{
KeyValuePair<string, string> image = imageIdentifiers.imageIds.ElementAt(i);
uint imageId;
if (!uint.TryParse(image.Value, out imageId))
continue;
byte[] bytes = db.Query<byte[], int, int, int>("SELECT data FROM data WHERE crc = ? AND filetype = ? AND entid = ? LIMIT 1", (int)imageId, 0, (int)imageIdentifiers.lastCEID );
if (bytes != null)
oldFiles.Add(image.Key, bytes);
else
{
failed++;
imageIdentifiers.imageIds.Remove(image.Key);
}
}
if (oldFiles.Count > 0)
{
loadOrders.Enqueue(new LoadOrder("Image restoration from previous database", oldFiles));
PrintWarning($"{imageIdentifiers.imageIds.Count - failed} images queued for restoration from previous image db, {failed} images failed");
}
}
db.Close();
}
catch
{
PrintError("Failed to open previous image database. Unable to clone previous image data");
}
//Facepunch.Sqlite.Database db = new Facepunch.Sqlite.Database();
//try
//{
// db.Open($"{ConVar.Server.rootFolder}/sv.files.0.db");
// db.Execute("DELETE FROM data WHERE entid = ?", imageIdentifiers.lastCEID);
// db.Close();
//}
//catch { }
//loadOrders.Enqueue(new LoadOrder("Image restoration from previous database", oldFiles));
//PrintWarning($"{imageIdentifiers.imageIds.Count - failed} images queued for restoration, {failed} images failed");
imageIdentifiers.lastCEID = CommunityEntity.ServerInstance.net.ID.Value;
SaveData();
orderPending = false;
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
#endregion Functions
#region Workshop Names and Image URLs
private void AddDefaultUrls()
{
foreach (ItemDefinition itemDefinition in ItemManager.itemList)
{
string identifier = $"{itemDefinition.shortname}_0";
if (!imageUrls.URLs.ContainsKey(identifier))
imageUrls.URLs.Add(identifier, $"{configData.ImageURL}{itemDefinition.shortname}.png");
else imageUrls.URLs[identifier] = $"{configData.ImageURL}{itemDefinition.shortname}.png";
}
SaveUrls();
LoadInbuiltSkinLookup();
}
private void LoadInbuiltSkinLookup()
{
const string LOOKUP_TABLE = "https://raw.githubusercontent.com/k1lly0u/Oxide/master/il_inbuilt_skins.json";
try
{
Debug.Log("Loading inbuilt skin manifest from GitHub...");
webrequest.Enqueue(LOOKUP_TABLE, string.Empty, (int code, string response) =>
{
Dictionary<string, string> collection = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
foreach (ItemSkinDirectory.Skin skin in ItemSkinDirectory.Instance.skins)
{
if (skin.invItem == null || string.IsNullOrEmpty(skin.invItem.itemname))
continue;
string filename;
if (collection.TryGetValue(skin.name, out filename))
{
string identifier = $"{skin.invItem.itemname}_{skin.id}";
if (!imageUrls.URLs.ContainsKey(identifier))
imageUrls.URLs.Add(identifier, $"{configData.ImageURL}{filename}.png");
else imageUrls.URLs[identifier] = $"{configData.ImageURL}{filename}.png";
}
}
Debug.Log("Skin manifest imported successfully");
SaveUrls();
}, this);
}
catch
{
Debug.LogError("Failed to download inbuilt skin manifest from GitHub. Unable to gather inbuilt skin list");
}
}
private readonly Dictionary<string, string> workshopNameToShortname = new Dictionary<string, string>
{
{"longtshirt", "tshirt.long" },
{"cap", "hat.cap" },
{"beenie", "hat.beenie" },
{"boonie", "hat.boonie" },
{"balaclava", "mask.balaclava" },
{"pipeshotgun", "shotgun.waterpipe" },
{"woodstorage", "box.wooden" },
{"ak47", "rifle.ak" },
{"bearrug", "rug.bear" },
{"boltrifle", "rifle.bolt" },
{"bandana", "mask.bandana" },
{"hideshirt", "attire.hide.vest" },
{"snowjacket", "jacket.snow" },
{"buckethat", "bucket.helmet" },
{"semiautopistol", "pistol.semiauto" },
{"burlapgloves", "burlap.gloves" },
{"roadsignvest", "roadsign.jacket" },
{"roadsignpants", "roadsign.kilt" },
{"burlappants", "burlap.trousers" },
{"collaredshirt", "shirt.collared" },
{"mp5", "smg.mp5" },
{"sword", "salvaged.sword" },
{"workboots", "shoes.boots" },
{"vagabondjacket", "jacket" },
{"hideshoes", "attire.hide.boots" },
{"deerskullmask", "deer.skull.mask" },
{"minerhat", "hat.miner" },
{"lr300", "rifle.lr300" },
{"lr300.item", "rifle.lr300" },
{"burlap.gloves", "burlap.gloves.new"},
{"leather.gloves", "burlap.gloves"},
{"python", "pistol.python" },
{"m39", "rifle.m39"},
{"woodendoubledoor", "door.double.hinged.wood"}
};
#endregion Workshop Names and Image URLs
#region API
[HookMethod("AddImage")]
public bool AddImage(string url, string imageName, ulong imageId, Action callback = null)
{
loadOrders.Enqueue(new LoadOrder(imageName, new Dictionary<string, string> { { $"{imageName}_{imageId}", url } }, true, callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
return true;
}
[HookMethod("AddImageData")]
public bool AddImageData(string imageName, byte[] array, ulong imageId, Action callback = null)
{
loadOrders.Enqueue(new LoadOrder(imageName, new Dictionary<string, byte[]> { { $"{imageName}_{imageId}", array } }, true, callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
return true;
}
[HookMethod("GetImageURL")]
public string GetImageURL(string imageName, ulong imageId = 0)
{
string identifier = $"{imageName}_{imageId}";
string value;
if (imageUrls.URLs.TryGetValue(identifier, out value))
return value;
return string.Empty;
}
[HookMethod("GetImage")]
public string GetImage(string imageName, ulong imageId = 0, bool returnUrl = false)
{
string identifier = $"{imageName}_{imageId}";
string value;
if (imageIdentifiers.imageIds.TryGetValue(identifier, out value))
return value;
else
{
if (imageUrls.URLs.TryGetValue(identifier, out value))
{
AddImage(value, imageName, imageId);
return imageIdentifiers.imageIds["LOADING_0"];
}
}
if (returnUrl && !string.IsNullOrEmpty(value))
return value;
return imageIdentifiers.imageIds["NONE_0"];
}
[HookMethod("GetImageList")]
public List<ulong> GetImageList(string name)
{
List<ulong> skinIds = new List<ulong>();
string[] matches = imageUrls.URLs.Keys.Where(x => x.StartsWith(name)).ToArray();
for (int i = 0; i < matches.Length; i++)
{
int index = matches[i].IndexOf("_");
if (matches[i].Substring(0, index) == name)
{
ulong skinID;
if (ulong.TryParse(matches[i].Substring(index + 1), out skinID))
skinIds.Add(ulong.Parse(matches[i].Substring(index + 1)));
}
}
return skinIds;
}
[HookMethod("GetSkinInfo")]
public Dictionary<string, object> GetSkinInfo(string name, ulong id)
{
Dictionary<string, object> skinInfo;
if (skinInformation.skinData.TryGetValue($"{name}_{id}", out skinInfo))
return skinInfo;
return null;
}
[HookMethod("HasImage")]
public bool HasImage(string imageName, ulong imageId)
{
string key = $"{imageName}_{imageId}";
string value;
if (imageIdentifiers.imageIds.TryGetValue(key, out value) && IsInStorage(uint.Parse(value)))
return true;
return false;
}
public bool IsInStorage(uint crc) => FileStorage.server.Get(crc, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID) != null;
[HookMethod("IsReady")]
public bool IsReady() => loadOrders.Count == 0 && !orderPending;
[HookMethod("ImportImageList")]
public void ImportImageList(string title, Dictionary<string, string> imageList, ulong imageId = 0, bool replace = false, Action callback = null)
{
Dictionary<string, string> newLoadOrder = new Dictionary<string, string>();
foreach (KeyValuePair<string, string> image in imageList)
{
if (!replace && HasImage(image.Key, imageId))
continue;
newLoadOrder[$"{image.Key}_{imageId}"] = image.Value;
}
if (newLoadOrder.Count > 0)
{
loadOrders.Enqueue(new LoadOrder(title, newLoadOrder, false, callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
else
{
if (callback != null)
callback.Invoke();
}
}
[HookMethod("ImportItemList")]
public void ImportItemList(string title, Dictionary<string, Dictionary<ulong, string>> itemList, bool replace = false, Action callback = null)
{
Dictionary<string, string> newLoadOrder = new Dictionary<string, string>();
foreach (KeyValuePair<string, Dictionary<ulong, string>> image in itemList)
{
foreach (KeyValuePair<ulong, string> skin in image.Value)
{
if (!replace && HasImage(image.Key, skin.Key))
continue;
newLoadOrder[$"{image.Key}_{skin.Key}"] = skin.Value;
}
}
if (newLoadOrder.Count > 0)
{
loadOrders.Enqueue(new LoadOrder(title, newLoadOrder, false, callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
else
{
if (callback != null)
callback.Invoke();
}
}
[HookMethod("ImportImageData")]
public void ImportImageData(string title, Dictionary<string, byte[]> imageList, ulong imageId = 0, bool replace = false, Action callback = null)
{
Dictionary<string, byte[]> newLoadOrder = new Dictionary<string, byte[]>();
foreach (KeyValuePair<string, byte[]> image in imageList)
{
if (!replace && HasImage(image.Key, imageId))
continue;
newLoadOrder[$"{image.Key}_{imageId}"] = image.Value;
}
if (newLoadOrder.Count > 0)
{
loadOrders.Enqueue(new LoadOrder(title, newLoadOrder, false, callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
else
{
if (callback != null)
callback.Invoke();
}
}
[HookMethod("LoadImageList")]
public void LoadImageList(string title, List<KeyValuePair<string, ulong>> imageList, Action callback = null)
{
Dictionary<string, string> newLoadOrderURL = new Dictionary<string, string>();
List<KeyValuePair<string, ulong>> workshopDownloads = new List<KeyValuePair<string, ulong>>();
foreach (KeyValuePair<string, ulong> image in imageList)
{
if (HasImage(image.Key, image.Value))
continue;
string identifier = $"{image.Key}_{image.Value}";
if (imageUrls.URLs.ContainsKey(identifier) && !newLoadOrderURL.ContainsKey(identifier))
{
newLoadOrderURL.Add(identifier, imageUrls.URLs[identifier]);
}
else
{
workshopDownloads.Add(new KeyValuePair<string, ulong>(image.Key, image.Value));
}
}
if (workshopDownloads.Count > 0)
{
QueueWorkshopDownload(title, newLoadOrderURL, workshopDownloads, 0, callback);
return;
}
if (newLoadOrderURL.Count > 0)
{
loadOrders.Enqueue(new LoadOrder(title, newLoadOrderURL, null, false, callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
else
{
if (callback != null)
callback.Invoke();
}
}
[HookMethod("RemoveImage")]
public void RemoveImage(string imageName, ulong imageId)
{
if (!HasImage(imageName, imageId))
return;
uint crc = uint.Parse(GetImage(imageName, imageId));
FileStorage.server.Remove(crc, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID);
}
[HookMethod("SendImage")]
public void SendImage(BasePlayer player, string imageName, ulong imageId = 0)
{
if (!HasImage(imageName, imageId) || player?.net?.connection == null)
return;
uint crc = uint.Parse(GetImage(imageName, imageId));
byte[] array = FileStorage.server.Get(crc, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID);
if (array == null)
return;
CommunityEntity.ServerInstance.ClientRPCEx<uint, uint, byte[]>(new Network.SendInfo(player.net.connection)
{
channel = 2,
method = Network.SendMethod.Reliable
}, null, "CL_ReceiveFilePng", crc, (uint)array.Length, array);
}
#endregion API
#region Steam API
private List<ulong> BuildApprovedItemList()
{
List<ulong> list = new List<ulong>();
foreach (InventoryDef item in Steamworks.SteamInventory.Definitions)
{
string shortname = item.GetProperty("itemshortname");
ulong workshopid;
if (item == null || string.IsNullOrEmpty(shortname))
continue;
if (workshopNameToShortname.ContainsKey(shortname))
shortname = workshopNameToShortname[shortname];
if (item.Id < 100)
continue;
if (!ulong.TryParse(item.GetProperty("workshopid"), out workshopid))
continue;
if (HasImage(shortname, workshopid))
continue;
list.Add(workshopid);
}
return list;
}
private string BuildDetailsString(List<ulong> list, int page)
{
int totalPages = Mathf.CeilToInt((float)list.Count / 100f);
int index = page * 100;
int limit = Mathf.Min((page + 1) * 100, list.Count);
string details = string.Format("?key={0}&itemcount={1}", configData.SteamAPIKey, (limit - index));
for (int i = index; i < limit; i++)
details += string.Format("&publishedfileids[{0}]={1}", i - index, list[i]);
return details;
}
private string BuildDetailsString(List<ulong> list)
{
string details = string.Format("?key={0}&itemcount={1}", configData.SteamAPIKey, list.Count);
for (int i = 0; i < list.Count; i++)
details += string.Format("&publishedfileids[{0}]={1}", i, list[i]);
return details;
}
private bool IsValid(PublishedFileDetails item)
{
if (string.IsNullOrEmpty(item.preview_url))
return false;
if (item.tags == null)
return false;
return true;
}
private void GetItemSkins()
{
Steamworks.SteamInventory.OnDefinitionsUpdated -= GetItemSkins;
PrintWarning("Retrieving item skin lists...");
GetApprovedItemSkins(BuildApprovedItemList(), 0);
}
private void QueueFileQueryRequest(string details, Action<PublishedFileDetails[]> callback)
{
webrequest.Enqueue(STEAM_API_URL, details, (code, response) =>
{
try
{
QueryResponse query = JsonConvert.DeserializeObject<QueryResponse>(response, errorHandling);
if (query == null || query.response == null || query.response.publishedfiledetails.Length == 0)
{
if (code != 200)
PrintError($"There was a error querying Steam for workshop item data : Code ({code})\n{details}");
return;
}
else
{
if (query?.response?.publishedfiledetails?.Length > 0)
callback.Invoke(query.response.publishedfiledetails);
}
}
catch { }
}, this, Core.Libraries.RequestMethod.POST);
}
private void GetApprovedItemSkins(List<ulong> itemsToDownload, int page)
{
if (itemsToDownload.Count < 1)
{
Puts("Approved skins loaded");
SaveUrls();
SaveSkinInfo();
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
return;
}
int totalPages = Mathf.CeilToInt((float)itemsToDownload.Count / 100f) - 1;
string details = BuildDetailsString(itemsToDownload, page);
QueueFileQueryRequest(details, (PublishedFileDetails[] items) =>
{
ServerMgr.Instance.StartCoroutine(ProcessApprovedBlock(itemsToDownload, items, page, totalPages));
});
}
private IEnumerator ProcessApprovedBlock(List<ulong> itemsToDownload, PublishedFileDetails[] items, int page, int totalPages)
{
PrintWarning($"Processing approved skins; Page {page + 1}/{totalPages + 1}");
Dictionary<string, Dictionary<ulong, string>> loadOrder = new Dictionary<string, Dictionary<ulong, string>>();
foreach (PublishedFileDetails item in items)
{
if (!IsValid(item))
continue;
foreach (PublishedFileDetails.Tag tag in item.tags)
{
if (string.IsNullOrEmpty(tag.tag))
continue;
ulong workshopid = Convert.ToUInt64(item.publishedfileid);
string adjTag = tag.tag.ToLower().Replace("skin", "").Replace(" ", "").Replace("-", "").Replace(".item", "");
if (workshopNameToShortname.ContainsKey(adjTag))
{
string shortname = workshopNameToShortname[adjTag];
string identifier = $"{shortname}_{workshopid}";
if (!imageUrls.URLs.ContainsKey(identifier))
imageUrls.URLs.Add(identifier, item.preview_url.Replace("https", "http"));
skinInformation.skinData[identifier] = new Dictionary<string, object>
{
{"title", item.title },
{"votesup", 0 },
{"votesdown", 0 },
{"description", item.file_description },
{"score", 0 },
{"views", 0 },
{"created", new DateTime() },
};
}
}
}
yield return CoroutineEx.waitForEndOfFrame;
yield return CoroutineEx.waitForEndOfFrame;
if (page < totalPages)
GetApprovedItemSkins(itemsToDownload, page + 1);
else
{
itemsToDownload.Clear();
Puts("Approved skins loaded");
SaveUrls();
SaveSkinInfo();
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
}
private void QueueWorkshopDownload(string title, Dictionary<string, string> newLoadOrderURL, List<KeyValuePair<string, ulong>> workshopDownloads, int page = 0, Action callback = null)
{
int rangeMin = page * 100;
int rangeMax = (page + 1) * 100;
if (rangeMax > workshopDownloads.Count)
rangeMax = workshopDownloads.Count;
List<ulong> requestedSkins = workshopDownloads.GetRange(rangeMin, rangeMax - rangeMin).Select(x => x.Value).ToList();
int totalPages = Mathf.CeilToInt((float)workshopDownloads.Count / 100f) - 1;
string details = BuildDetailsString(requestedSkins);
try
{
webrequest.Enqueue(STEAM_API_URL, details, (code, response) =>
{
QueryResponse query = JsonConvert.DeserializeObject<QueryResponse>(response, errorHandling);
if (query == null || query.response == null || query.response.publishedfiledetails.Length == 0)
{
if (code != 200)
PrintError($"There was a error querying Steam for workshop item data : Code ({code})");
if (page < totalPages)
QueueWorkshopDownload(title, newLoadOrderURL, workshopDownloads, page + 1, callback);
else
{
if (newLoadOrderURL.Count > 0)
{
loadOrders.Enqueue(new LoadOrder(title, newLoadOrderURL, null, false, page < totalPages ? null : callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
else
{
if (callback != null)
callback.Invoke();
}
}
return;
}
else
{
if (query.response.publishedfiledetails.Length > 0)
{
Dictionary<string, Dictionary<ulong, string>> loadOrder = new Dictionary<string, Dictionary<ulong, string>>();
foreach (PublishedFileDetails item in query.response.publishedfiledetails)
{
if (!string.IsNullOrEmpty(item.preview_url))
{
ulong skinId = Convert.ToUInt64(item.publishedfileid);
KeyValuePair<string, ulong>? kvp = workshopDownloads.Find(x => x.Value == skinId);
if (kvp.HasValue)
{
string identifier = $"{kvp.Value.Key}_{kvp.Value.Value}";
if (!newLoadOrderURL.ContainsKey(identifier))
newLoadOrderURL.Add(identifier, item.preview_url);
if (!imageUrls.URLs.ContainsKey(identifier))
imageUrls.URLs.Add(identifier, item.preview_url);
skinInformation.skinData[identifier] = new Dictionary<string, object>
{
{"title", item.title },
{"votesup", 0 },
{"votesdown", 0 },
{"description", item.file_description },
{"score", 0 },
{"views", item.views },
{"created", new DateTime(item.time_created) },
};
requestedSkins.Remove(skinId);
}
}
}
SaveUrls();
SaveSkinInfo();
if (requestedSkins.Count != 0)
{
Puts($"{requestedSkins.Count} workshop skin ID's for image batch ({title}) are invalid! They may have been removed from the workshop\nIDs: {requestedSkins.ToSentence()}");
}
}
if (page < totalPages)
QueueWorkshopDownload(title, newLoadOrderURL, workshopDownloads, page + 1, callback);
else
{
if (newLoadOrderURL.Count > 0)
{
loadOrders.Enqueue(new LoadOrder(title, newLoadOrderURL, null, false, page < totalPages ? null : callback));
if (!orderPending)
ServerMgr.Instance.StartCoroutine(ProcessLoadOrders());
}
else
{
if (callback != null)
callback.Invoke();
}
}
}
},
this,
Core.Libraries.RequestMethod.POST);
}
catch { }
}
#region JSON Response Classes
public class QueryResponse
{
public Response response;
}
public class Response
{
public int total;
public PublishedFileDetails[] publishedfiledetails;
}
public class PublishedFileDetails
{
public int result;
public string publishedfileid;
public string creator;
public int creator_appid;
public int consumer_appid;
public int consumer_shortcutid;
public string filename;
public string file_size;
public string preview_file_size;
public string file_url;
public string preview_url;
public string url;
public string hcontent_file;
public string hcontent_preview;
public string title;
public string file_description;
public int time_created;
public int time_updated;
public int visibility;
public int flags;
public bool workshop_file;
public bool workshop_accepted;
public bool show_subscribe_all;
public int num_comments_public;
public bool banned;
public string ban_reason;
public string banner;
public bool can_be_deleted;
public string app_name;
public int file_type;
public bool can_subscribe;
public int subscriptions;
public int favorited;
public int followers;
public int lifetime_subscriptions;
public int lifetime_favorited;
public int lifetime_followers;
public string lifetime_playtime;
public string lifetime_playtime_sessions;
public int views;
public int num_children;
public int num_reports;
public Preview[] previews;
public Tag[] tags;
public int language;
public bool maybe_inappropriate_sex;
public bool maybe_inappropriate_violence;
public class Tag
{
public string tag;
public bool adminonly;
}
}
public class Preview
{
public string previewid;
public int sortorder;
public string url;
public int size;
public string filename;
public int preview_type;
public string youtubevideoid;
public string external_reference;
}
#endregion
#endregion
#region Commands
[ConsoleCommand("cancelstorage")]
private void cmdCancelStorage(ConsoleSystem.Arg arg)
{
if (arg.Connection == null || arg.Connection.authLevel > 0)
{
if (!orderPending)
PrintWarning("No images are currently being downloaded");
else
{
assets.ClearList();
loadOrders.Clear();
PrintWarning("Pending image downloads have been cancelled!");
}
}
}
private List<ulong> pendingAnswers = new List<ulong>();
[ConsoleCommand("refreshallimages")]
private void cmdRefreshAllImages(ConsoleSystem.Arg arg)
{
if (arg.Connection == null || arg.Connection.authLevel > 0)
{
SendReply(arg, "Running this command will wipe all of your ImageLibrary data, meaning every registered image will need to be re-downloaded. Are you sure you wish to continue? (type yes or no)");
ulong userId = arg.Connection == null || arg.IsRcon ? 0U : arg.Connection.userid;
if (!pendingAnswers.Contains(userId))
{
pendingAnswers.Add(userId);
timer.In(5, () =>
{
if (pendingAnswers.Contains(userId))
pendingAnswers.Remove(userId);
});
}
}
}
[ConsoleCommand("yes")]
private void cmdRefreshAllImagesYes(ConsoleSystem.Arg arg)
{
if (arg.Connection == null || arg.Connection.authLevel > 0)
{
ulong userId = arg.Connection == null || arg.IsRcon ? 0U : arg.Connection.userid;
if (pendingAnswers.Contains(userId))
{
PrintWarning("Wiping ImageLibrary data and redownloading ImageLibrary specific images. All plugins that have registered images via ImageLibrary will need to be re-loaded!");
RefreshImagery();
pendingAnswers.Remove(userId);
}
}
}
[ConsoleCommand("no")]
private void cmdRefreshAllImagesNo(ConsoleSystem.Arg arg)
{
if (arg.Connection == null || arg.Connection.authLevel > 0)
{
ulong userId = arg.Connection == null || arg.IsRcon ? 0U : arg.Connection.userid;
if (pendingAnswers.Contains(userId))
{
SendReply(arg, "ImageLibrary data wipe aborted!");
pendingAnswers.Remove(userId);
}
}
}
#endregion Commands
#region Image Storage
private struct LoadOrder
{
public string loadName;
public bool loadSilent;
public Dictionary<string, string> imageList;
public Dictionary<string, byte[]> imageData;
public Action callback;
public LoadOrder(string loadName, Dictionary<string, string> imageList, bool loadSilent = false, Action callback = null)
{
this.loadName = loadName;
this.imageList = imageList;
this.imageData = null;
this.loadSilent = loadSilent;
this.callback = callback;
}
public LoadOrder(string loadName, Dictionary<string, byte[]> imageData, bool loadSilent = false, Action callback = null)
{
this.loadName = loadName;
this.imageList = null;
this.imageData = imageData;
this.loadSilent = loadSilent;
this.callback = callback;
}
public LoadOrder(string loadName, Dictionary<string, string> imageList, Dictionary<string, byte[]> imageData, bool loadSilent = false, Action callback = null)
{
this.loadName = loadName;
this.imageList = imageList;
this.imageData = imageData;
this.loadSilent = loadSilent;
this.callback = callback;
}
}
private class ImageAssets : MonoBehaviour
{
private Queue<QueueItem> queueList = new Queue<QueueItem>();
private bool isLoading;
private double nextUpdate;
private int listCount;
private string request;
private Action callback;
private void OnDestroy()
{
queueList.Clear();
}
public void Add(string name, string url = null, byte[] bytes = null)
{
queueList.Enqueue(new QueueItem(name, url, bytes));
}
public void RegisterCallback(Action callback) => this.callback = callback;
public void BeginLoad(string request)
{
this.request = request;
nextUpdate = UnityEngine.Time.time + il.configData.UpdateInterval;
listCount = queueList.Count;
Next();
}
public void ClearList()
{
queueList.Clear();
il.orderPending = false;
}
private void Next()
{
if (queueList.Count == 0)
{
il.orderPending = false;
il.SaveData();
if (!string.IsNullOrEmpty(request))
print($"Image batch ({request}) has been stored successfully");
request = string.Empty;
listCount = 0;
if (callback != null)
callback.Invoke();
StartCoroutine(il.ProcessLoadOrders());
return;
}
if (il.configData.ShowProgress && listCount > 1)
{
float time = UnityEngine.Time.time;
if (time > nextUpdate)
{
int amountDone = listCount - queueList.Count;
print($"{request} storage process at {Math.Round((amountDone / (float)listCount) * 100, 0)}% ({amountDone}/{listCount})");
nextUpdate = time + il.configData.UpdateInterval;
}
}
isLoading = true;
QueueItem queueItem = queueList.Dequeue();
if (!string.IsNullOrEmpty(queueItem.url))
StartCoroutine(DownloadImage(queueItem));
else StoreByteArray(queueItem.bytes, queueItem.name);
}
private IEnumerator DownloadImage(QueueItem info)
{
UnityWebRequest www = UnityWebRequest.Get(info.url);
yield return www.SendWebRequest();
if (il == null) yield break;
if (www.isNetworkError || www.isHttpError)
{
print(string.Format("Image failed to download! Error: {0} - Image Name: {1} - Image URL: {2}", www.error, info.name, info.url));
www.Dispose();
isLoading = false;
Next();
yield break;
}
if (www?.downloadHandler?.data != null)
{
Texture2D texture = new Texture2D(2, 2);
texture.LoadImage(www.downloadHandler.data);
if (texture != null)
{
bool shouldStore = true;
byte[] bytes = texture.EncodeToPNG();
if (bytes.Length > 3145728)
{
Debug.Log($"[ImageLibrary] Failed to store image data for image : {info.name} for equest {request}\nURL: {info.url}\n{bytes.Length} bytes is larger then the allowed transferable size of 3145728 bytes");
shouldStore = false;
}
DestroyImmediate(texture);
if (shouldStore)
StoreByteArray(bytes, info.name);
}
}
www.Dispose();
}
private void StoreByteArray(byte[] bytes, string name)
{
if (bytes != null)
il.imageIdentifiers.imageIds[name] = FileStorage.server.Store(bytes, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID).ToString();
isLoading = false;
Next();
}
private class QueueItem
{
public byte[] bytes;
public string url;
public string name;
public QueueItem(string name, string url = null, byte[] bytes = null)
{
this.bytes = bytes;
this.url = url;
this.name = name;
}
}
}
#endregion Image Storage
#region Config
private ConfigData configData;
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; }
}
protected override void LoadConfig()
{
base.LoadConfig();
configData = Config.ReadObject<ConfigData>();
if (configData.Version < Version)
UpdateConfigValues();
Config.WriteObject(configData, true);
}
protected override void LoadDefaultConfig() => configData = GetBaseConfig();
private ConfigData GetBaseConfig()
{
return new ConfigData
{
ShowProgress = true,
SteamAPIKey = string.Empty,
StoreAvatars = false,
UpdateInterval = 20,
ImageURL = "https://www.rustedit.io/images/imagelibrary/",
UserImages = new Dictionary<string, string>(),
Version = Version
};
}
protected override void SaveConfig() => Config.WriteObject(configData, true);
private void UpdateConfigValues()
{
PrintWarning("Config update detected! Updating config values...");
ConfigData baseConfig = GetBaseConfig();
if (configData.Version < new VersionNumber(2, 0, 47))
configData = baseConfig;
if (configData.Version < new VersionNumber(2, 0, 53))
configData.StoreAvatars = false;
if (configData.Version < new VersionNumber(2, 0, 55))
configData.ImageURL = baseConfig.ImageURL;
configData.Version = Version;
PrintWarning("Config update completed!");
}
#endregion Config
#region Data Management
private void SaveData() => identifiers.WriteObject(imageIdentifiers);
private void SaveSkinInfo() => skininfo.WriteObject(skinInformation);
private void SaveUrls() => urls.WriteObject(imageUrls);
private void LoadData()
{
try
{
imageIdentifiers = identifiers.ReadObject<ImageIdentifiers>();
}
catch
{
imageIdentifiers = new ImageIdentifiers();
}
try
{
skinInformation = skininfo.ReadObject<SkinInformation>();
}
catch
{
skinInformation = new SkinInformation();
}
try
{
imageUrls = urls.ReadObject<ImageURLs>();
}
catch
{
imageUrls = new ImageURLs();
}
if (skinInformation == null)
skinInformation = new SkinInformation();
if (imageIdentifiers == null)
imageIdentifiers = new ImageIdentifiers();
if (imageUrls == null)
imageUrls = new ImageURLs();
}
private class ImageIdentifiers
{
public ulong lastCEID;
public Hash<string, string> imageIds = new Hash<string, string>();
}
private class SkinInformation
{
public Hash<string, Dictionary<string, object>> skinData = new Hash<string, Dictionary<string, object>>();
}
private class ImageURLs
{
public Hash<string, string> URLs = new Hash<string, string>();
}
public class AvatarRoot
{
public Response response { get; set; }
public class Response
{
public Player[] players { get; set; }
public class Player
{
public string steamid { get; set; }
public int communityvisibilitystate { get; set; }
public int profilestate { get; set; }
public string personaname { get; set; }
public int lastlogoff { get; set; }
public string profileurl { get; set; }
public string avatar { get; set; }
public string avatarmedium { get; set; }
public string avatarfull { get; set; }
public int personastate { get; set; }
public string realname { get; set; }
public string primaryclanid { get; set; }
public int timecreated { get; set; }
public int personastateflags { get; set; }
}
}
}
#endregion Data Management
}
}