C#:
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Serialization;
using UnityEngine;
using Time = Oxide.Core.Libraries.Time;
namespace Oxide.Plugins {
[Info("SkinBox", "k1lly0u", "2.1.25"), Description("Allows you to reskin item's by placing it in the SkinBox and selecting a new skin")]
class SkinBox : RustPlugin {
#region Fields
[PluginReference]
private Plugin ServerRewards, Economics;
private Hash<ulong, string> _skinPermissions = new Hash<ulong, string>();
private readonly Hash<string, HashSet<ulong>> _skinList = new Hash<string, HashSet<ulong>>();
private readonly Hash<ulong, LootHandler> _activeSkinBoxes = new Hash<ulong, LootHandler>();
private readonly Hash<string, string> _shortnameToDisplayname = new Hash<string, string>();
private readonly Hash<ulong, string> _skinSearchLookup = new Hash<ulong, string>();
private readonly Hash<ulong, string> _skinNameLookup = new Hash<ulong, string>();
private readonly Hash<ulong, double> _cooldownTimes = new Hash<ulong, double>();
private bool _apiKeyMissing = false;
private bool _skinsLoaded = false;
private Timer _approvedTimeout;
private CostType _costType;
private SortBy _sorting;
private static SkinBox Instance {
get;
set;
}
private static Func<string, ulong, string> GetMessage;
private const string COFFIN_PREFAB = "assets/prefabs/misc/halloween/coffin/coffinstorage.prefab";
private const string SPRAYCAN_ITEM = "spraycan";
private const ulong SPRAYCAN_SKIN = 2937962221UL;
private const string LOOT_PANEL = "generic_resizable";
private const int SCRAP_ITEM_ID = -932201673;
private enum CostType {
Scrap, ServerRewards, Economics
}
public enum SortBy {
Config = 0, ConfigReversed = 1, Alphabetical = 2
}
#endregion
#region Oxide Hooks
private void Loaded() {
Instance = this;
Configuration.Permission.RegisterPermissions(permission, this);
Configuration.Command.RegisterCommands(cmd, this);
_costType = ParseType<CostType>(Configuration.Cost.Currency);
_sorting = ParseType<SortBy>(Configuration.Skins.Sorting);
if (!Configuration.Command.SprayCan) {
Unsubscribe(nameof(OnSprayCreate));
Unsubscribe(nameof(OnActiveItemChanged));
}
if (Configuration.Favourites.Enabled) StoredData.Load();
GetMessage = (string key, ulong userId) => lang.GetMessage(key, this, userId.ToString());
}
protected override void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
["NoAPIKey"]
= "The server owner has not entered a Steam API key in the config. Unable to continue!", ["SkinsLoading"] = "SkinBox is still gathering skins. Please try again soon", ["NoPermission"] = "You don't have permission to use the SkinBox", ["NoBuildingAuth"] = "You must have building privilege to use the SkinBox", ["ToNearPlayer"] = "The SkinBox is currently not usable at this place", ["CooldownTime"] = "You need to wait {0} seconds to use the SkinBox again", ["NotEnoughBalanceOpen"] = "You need at least {0} {1} to open the SkinBox", ["NotEnoughBalanceUse"] = "You would need at least {0} {1} to skin {2}", ["NotEnoughBalanceTake"] = "{0} was not skinned. You do not have enough {1}", ["CostToUse"] = "Skinning a item is not free!\n{0} {3} to skin a deployable\n{1} {3} to skin a weapon\n{2} {3} to skin attire", ["TargetSkinNotInList"] = "That skin is not defined in the skin list for that item", ["TargetSkinIsCurrentSkin"] = "The target item is already skinned with that skin ID", ["ApplyingSkin"] = "Applying skin {0}", ["NoItemInHands"] = "You must have a item in your hands to use the quick skin method", ["NoSkinsForItem"] = "There are no skins setup for that item", ["BrokenItem"] = "You can not skin broken items", ["HasItem"] = "The skin box already contains an item", ["RedirectsDisabled"] = "Redirected skins are disabled on this server", ["InsufficientItemPermission"] = "You do not have permission to skin this type of item", ["Cost.Scrap"] = "Scrap", ["Cost.ServerRewards"] = "RP", ["Cost.Economics"] = "Eco", ["ReskinError.InvalidResourcePath"] = "Failed to find resource path for deployed item", ["ReskinError.TargetNull"] = "The target deployable has been destroyed", ["ReskinError.MountBlocked"] = "You can not skin this while a player is mounted", ["ReskinError.IOConnected"] = "You can not skin this while it is connected", ["ReskinError.NoAuth"] = "You need building auth to reskin deployed items", ["ReskinError.NoTarget"] = "Unable to find a valid deployed item", ["NoDefinitionForEntity"] = "Failed to find the definition for the target item", ["FavouritesEnabled"] = "Favourites system enabled\nYour most used skins will be displayed first", ["ClearFavourites"] = "Clear favourites for {0}"
}
, this);
}
private void OnServerInitialized() {
UnsubscribeHooks();
if (string.IsNullOrEmpty(Configuration.Skins.APIKey)) {
_apiKeyMissing = true;
SendAPIMissingWarning();
return;
}
if ((Steamworks.SteamInventory.Definitions?.Length ?? 0) == 0 && Configuration.Skins.UseApproved) {
PrintWarning("Waiting for Steamworks to update item definitions....");
Steamworks.SteamInventory.OnDefinitionsUpdated += StartSkinRequest;
if (Configuration.Skins.ApprovedTimeout > 0) _approvedTimeout = timer.Once(Configuration.Skins.ApprovedTimeout, AbortApprovedSkinRequest);
}
else StartSkinRequest();
}
private object CanAcceptItem(ItemContainer container, Item item) {
if (container.entityOwner == null) return null;
LootHandler lootHandler = container.entityOwner.GetComponent<LootHandler>();
if (lootHandler == null) return null;
if (item.isBroken) {
lootHandler.PopupMessage(GetMessage("BrokenItem", lootHandler.Looter.userID));
return ItemContainer.CanAcceptResult.CannotAccept;
}
if (lootHandler.HasItem) {
lootHandler.PopupMessage(GetMessage("HasItem", lootHandler.Looter.userID));
return ItemContainer.CanAcceptResult.CannotAccept;
}
if (!HasItemPermissions(lootHandler.Looter, item)) {
lootHandler.PopupMessage(GetMessage("InsufficientItemPermission", lootHandler.Looter.userID));
return ItemContainer.CanAcceptResult.CannotAccept;
}
string shortname = GetRedirectedShortname(item.info.shortname);
if (!Configuration.Skins.UseRedirected && !shortname.Equals(item.info.shortname, StringComparison.OrdinalIgnoreCase)) {
lootHandler.PopupMessage(GetMessage("RedirectsDisabled", lootHandler.Looter.userID));
return ItemContainer.CanAcceptResult.CannotAccept;
}
HashSet<ulong> skins;
if (!_skinList.TryGetValue(shortname, out skins) || skins.Count == 0) {
lootHandler.PopupMessage(GetMessage("NoSkinsForItem", lootHandler.Looter.userID));
return ItemContainer.CanAcceptResult.CannotAccept;
}
int reskinCost = GetReskinCost(item);
if (reskinCost > 0 && !CanAffordToUse(lootHandler.Looter, reskinCost)) {
lootHandler.PopupMessage(string.Format(GetMessage("NotEnoughBalanceUse", lootHandler.Looter.userID), reskinCost, GetCostType(lootHandler.Looter.userID), item.info.displayName.english));
return ItemContainer.CanAcceptResult.CannotAccept;
}
string result = Interface.Call<string>("SB_CanAcceptItem", lootHandler.Looter, item);
if (!string.IsNullOrEmpty(result)) {
lootHandler.PopupMessage(result);
return ItemContainer.CanAcceptResult.CannotAccept;
}
return null;
}
private void OnItemSplit(Item item, int amount) {
LootHandler lootHandler = item.parent?.entityOwner?.GetComponent<LootHandler>();
if (lootHandler != null) lootHandler.CheckItemHasSplit(item);
}
private object CanMoveItem(Item item, PlayerInventory inventory, ItemContainerId targetContainerID, int targetSlot, int amount) {
if (item.parent?.entityOwner != null) {
LootHandler lootHandler = item.parent != null && item.parent.entityOwner != null ? item.parent.entityOwner.GetComponent<LootHandler>() : null;
if (lootHandler != null && lootHandler.InputAmount > 1) {
if (targetContainerID == default(ItemContainerId) || targetSlot == -1) return false;
ItemContainer targetContainer = inventory.FindContainer(targetContainerID);
if (targetContainer != null && targetContainer.GetSlot(targetSlot) != null) return false;
}
}
return null;
}
private object OnItemAction(Item item, string action, BasePlayer player) {
LootHandler lootHandler;
if (!_activeSkinBoxes.TryGetValue(player.userID, out lootHandler)) return null;
const string DROP_ACTION = "drop";
if (!action.Equals(DROP_ACTION, StringComparison.OrdinalIgnoreCase)) return null;
if (lootHandler.Entity.inventory?.itemList?.Contains(item) ?? false) return true;
return null;
}
private void OnItemAddedToContainer(ItemContainer container, Item item) {
if (container?.entityOwner == null || container.entityOwner.IsDestroyed) return;
LootHandler lootHandler = container.entityOwner.GetComponent<LootHandler>();
if (lootHandler != null) lootHandler.OnItemAdded(item);
}
private void OnItemRemovedFromContainer(ItemContainer container, Item item) {
if (container?.entityOwner == null || container.entityOwner.IsDestroyed) return;
LootHandler lootHandler = container.entityOwner.GetComponent<LootHandler>();
if (lootHandler != null) lootHandler.OnItemRemoved(item);
}
private void OnLootEntityEnd(BasePlayer player, StorageContainer storageContainer) {
LootHandler lootHandler;
if (_activeSkinBoxes.TryGetValue(player.userID, out lootHandler)) UnityEngine.Object.DestroyImmediate(lootHandler);
}
private void OnPlayerDeath(BasePlayer player, HitInfo hitInfo) {
UnityEngine.Object.Destroy(player.GetComponent<SprayCanWatcher>());
LootHandler lootHandler;
if (!_activeSkinBoxes.TryGetValue(player.userID, out lootHandler)) return;
lootHandler.ReturnItemInstantly();
player.EndLooting();
UnityEngine.Object.DestroyImmediate(lootHandler);
}
private void OnPlayerDisconnected(BasePlayer player) {
UnityEngine.Object.Destroy(player.GetComponent<SprayCanWatcher>());
}
private void OnServerSave() {
if (Configuration.Favourites.Enabled) StoredData.Save();
}
private void Unload() {
SprayCanWatcher[] sprayCanWatchers = UnityEngine.Object.FindObjectsOfType<SprayCanWatcher>();
for (int i = 0; i < sprayCanWatchers.Length; i++) UnityEngine.Object.Destroy(sprayCanWatchers[i]);
LootHandler[] lootHandlers = UnityEngine.Object.FindObjectsOfType<LootHandler>();
for (int i = 0; i < lootHandlers.Length; i++) {
LootHandler lootHandler = lootHandlers[i];
if (lootHandler.Looter != null) lootHandler.Looter.EndLooting();
UnityEngine.Object.Destroy(lootHandler);
}
Configuration = null;
Instance = null;
}
#endregion
#region Spraycan Shit
private object OnSprayCreate(SprayCan sprayCan, Vector3 position, Quaternion rotation) {
Item item = sprayCan.GetItem();
if (item.skin == SPRAYCAN_SKIN) return false;
return null;
}
private void OnActiveItemChanged(BasePlayer player, Item oldItem, Item newItem) {
if (oldItem?.skin == SPRAYCAN_SKIN) UnityEngine.Object.Destroy(player.GetComponent<SprayCanWatcher>());
if (newItem?.skin == SPRAYCAN_SKIN) player.gameObject.AddComponent<SprayCanWatcher>();
}
#endregion
#region Functions
private void SendAPIMissingWarning() {
Debug.LogWarning("You must enter a Steam API key in the config!\nYou can get a API key here -> https://steamcommunity.com/dev/apikey \nOnce you have your API key copy it to the 'Skin Options/Steam API Key' field in your SkinBox.json config file");
}
private void ChatMessage(BasePlayer player, string key, params object[] args) {
if (args == null) player.ChatMessage(lang.GetMessage(key, this, player.UserIDString));
else player.ChatMessage(string.Format(lang.GetMessage(key, this, player.UserIDString), args));
}
private LootHandler CreateSkinBox(BasePlayer player, DeployableHandler.ReskinTarget reskinTarget) {
StorageContainer container = GameManager.server.CreateEntity(COFFIN_PREFAB, player.transform.position + (Vector3.down * 250f)) as StorageContainer;
container.limitNetworking = true;
container.enableSaving = false;
UnityEngine.Object.Destroy(container.GetComponent<DestroyOnGroundMissing>());
UnityEngine.Object.Destroy(container.GetComponent<GroundWatch>());
container.Spawn();
LootHandler lootHandler;
if (_activeSkinBoxes.TryGetValue(player.userID, out lootHandler)) {
player.EndLooting();
UnityEngine.Object.Destroy(lootHandler);
}
if (reskinTarget != null) {
lootHandler = container.gameObject.AddComponent<DeployableHandler>();
lootHandler.Looter = player;
(lootHandler as DeployableHandler).Target = reskinTarget;
}
else {
lootHandler = container.gameObject.AddComponent<LootHandler>();
lootHandler.Looter = player;
}
player.inventory.loot.Clear();
player.inventory.loot.PositionChecks = false;
player.inventory.loot.entitySource = container;
player.inventory.loot.itemSource = null;
player.inventory.loot.MarkDirty();
player.inventory.loot.AddContainer(container.inventory);
player.inventory.loot.SendImmediate();
player.ClientRPCPlayer(null, player, "RPC_OpenLootPanel", LOOT_PANEL);
container.SendNetworkUpdate(BasePlayer.NetworkQueue.Update);
if (Configuration.Cost.Enabled && !permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCost)) {
lootHandler.PopupMessage(string.Format(GetMessage("CostToUse", lootHandler.Looter.userID), Configuration.Cost.Deployable, Configuration.Cost.Weapon, Configuration.Cost.Attire, GetCostType(player.userID)));
}
_activeSkinBoxes[player.userID] = lootHandler;
ToggleHooks();
return lootHandler;
}
#endregion
#region Hook Subscriptions
private void ToggleHooks() {
if (_activeSkinBoxes.Count > 0) SubscribeHooks();
else UnsubscribeHooks();
}
private void SubscribeHooks() {
Subscribe(nameof(OnLootEntityEnd));
Subscribe(nameof(OnItemRemovedFromContainer));
Subscribe(nameof(OnItemAddedToContainer));
Subscribe(nameof(CanMoveItem));
Subscribe(nameof(OnItemSplit));
Subscribe(nameof(CanAcceptItem));
}
private void UnsubscribeHooks() {
Unsubscribe(nameof(OnLootEntityEnd));
Unsubscribe(nameof(OnItemRemovedFromContainer));
Unsubscribe(nameof(OnItemAddedToContainer));
Unsubscribe(nameof(CanMoveItem));
Unsubscribe(nameof(OnItemSplit));
Unsubscribe(nameof(CanAcceptItem));
}
#endregion
#region Helpers
private void GetSkinsFor(BasePlayer player, string shortname, ref List<ulong> list) {
list.Clear();
List<ulong> skinOverrides = Interface.Call<List<ulong>>("SB_GetSkinOverrides", player, shortname);
if (skinOverrides != null && skinOverrides.Count > 0) {
list.AddRange(skinOverrides);
goto SORT_FAVOURITES;
}
HashSet<ulong> skins;
if (_skinList.TryGetValue(shortname, out skins)) {
foreach(ulong skinId in skins) {
string perm;
if (_skinPermissions.TryGetValue(skinId, out perm) && !permission.UserHasPermission(player.UserIDString, perm)) continue;
if (Configuration.Blacklist.Contains(skinId) && player.net.connection.authLevel < Configuration.Other.BlacklistAuth) continue;
list.Add(skinId);
}
}
SORT_FAVOURITES : if (Configuration.Favourites.Enabled) Favourites.SortForPlayer(player, shortname, ref list);
}
private bool HasItemPermissions(BasePlayer player, Item item) {
switch (item.info.category) {
case ItemCategory.Weapon : case ItemCategory.Tool : return permission.UserHasPermission(player.UserIDString, Configuration.Permission.Weapon);
case ItemCategory.Construction : case ItemCategory.Items : return permission.UserHasPermission(player.UserIDString, Configuration.Permission.Deployable);
case ItemCategory.Attire : return permission.UserHasPermission(player.UserIDString, Configuration.Permission.Attire);
default : return true;
}
}
private T ParseType<T>(string type) {
try {
return (T)Enum.Parse(typeof(T), type, true);
}
catch {
return default(T);
}
}
private double CurrentTime => DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds;
#endregion
#region Usage Costs
private string GetCostType(ulong playerId) => GetMessage($"Cost.{_costType}", playerId);
private int GetReskinCost(Item item) {
if (!Configuration.Cost.Enabled) return 0;
switch (item.info.category) {
case ItemCategory.Weapon : case ItemCategory.Tool : return Configuration.Cost.Weapon;
case ItemCategory.Construction : case ItemCategory.Items : return Configuration.Cost.Deployable;
case ItemCategory.Attire : return Configuration.Cost.Attire;
default : return 0;
}
}
private bool CanAffordToUse(BasePlayer player, int amount) {
if (!Configuration.Cost.Enabled || amount == 0 || permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCost)) return true;
switch (_costType) {
case CostType.Scrap : return player.inventory.GetAmount(SCRAP_ITEM_ID) >= amount;
case CostType.ServerRewards : return (int)ServerRewards?.Call("CheckPoints", player.userID) >= amount;
case CostType.Economics : return (double)Economics?.Call("Balance", player.UserIDString) >= amount;
}
return false;
}
private bool ChargePlayer(BasePlayer player, ItemCategory itemCategory) {
if (!Configuration.Cost.Enabled || permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCost)) return true;
int amount = itemCategory == ItemCategory.Weapon || itemCategory == ItemCategory.Tool ? Configuration.Cost.Weapon : itemCategory == ItemCategory.Items || itemCategory == ItemCategory.Construction ? Configuration.Cost.Deployable : itemCategory == ItemCategory.Attire ? Configuration.Cost.Attire : 0;
return ChargePlayer(player, amount);
}
private bool ChargePlayer(BasePlayer player, int amount) {
if (amount == 0 || !Configuration.Cost.Enabled || permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCost)) return true;
switch (_costType) {
case CostType.Scrap : if (amount <= player.inventory.GetAmount(SCRAP_ITEM_ID)) {
player.inventory.Take(null, SCRAP_ITEM_ID, amount);
return true;
}
return false;
case CostType.ServerRewards : return (bool)ServerRewards?.Call("TakePoints", player.userID, amount);
case CostType.Economics : return (bool)Economics?.Call("Withdraw", player.UserIDString, (double)amount);
}
return false;
}
#endregion
#region Cooldown
private void ApplyCooldown(BasePlayer player) {
if (!Configuration.Cooldown.Enabled || permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCooldown)) return;
_cooldownTimes[player.userID] = CurrentTime + Configuration.Cooldown.Time;
}
private bool IsOnCooldown(BasePlayer player) {
if (!Configuration.Cooldown.Enabled || permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCooldown)) return false;
double time;
if (_cooldownTimes.TryGetValue(player.userID, out time) && time > CurrentTime) return true;
return false;
}
private bool IsOnCooldown(BasePlayer player, out double remaining) {
remaining = 0;
if (!Configuration.Cooldown.Enabled || permission.UserHasPermission(player.UserIDString, Configuration.Permission.NoCooldown)) return false;
double time;
if (_cooldownTimes.TryGetValue(player.userID, out time) && time > CurrentTime) {
remaining = time - CurrentTime;
return true;
}
return false;
}
#endregion
#region Approved Skins
private void AbortApprovedSkinRequest() {
Debug.LogWarning("[SkinBox] - Aborting approved skin processing. Server has not yet downloaded approved manifest. Only workshop skins will be available");
Steamworks.SteamInventory.OnDefinitionsUpdated -= StartSkinRequest;
if (!Configuration.Skins.UseWorkshop) {
PrintError("You have workshop skins disabled. This leaves no skins available to use in SkinBox!");
return;
}
VerifyWorkshopSkins();
}
private void StartSkinRequest() {
_approvedTimeout?.Destroy();
Steamworks.SteamInventory.OnDefinitionsUpdated -= StartSkinRequest;
UpdateWorkshopNameConversionList();
FindItemRedirects();
FixLR300InvalidShortname();
if (!Configuration.Skins.UseApproved && !Configuration.Skins.UseWorkshop) {
PrintError("You have approved skins and workshop skins disabled. This leaves no skins available to use in SkinBox!");
return;
}
if (!Configuration.Skins.UseApproved && Configuration.Skins.UseWorkshop) {
VerifyWorkshopSkins();
return;
}
PrintWarning("Retrieving approved skin lists...");
CollectApprovedSkins();
}
private void CollectApprovedSkins() {
int count = 0;
bool addApprovedPermission = Configuration.Permission.Approved != Configuration.Permission.Use;
bool updateConfig = false;
List<ulong> list = Facepunch.Pool.GetList<ulong>();
List<int> itemSkinDirectory = Facepunch.Pool.GetList<int>();
itemSkinDirectory.AddRange(ItemSkinDirectory.Instance.skins.Select(x => x.id));
foreach (ItemDefinition itemDefinition in ItemManager.itemList) {
list.Clear();
foreach (Steamworks.InventoryDef item in Steamworks.SteamInventory.Definitions) {
string shortname = item.GetProperty("itemshortname");
if (string.IsNullOrEmpty(shortname) || item.Id < 100) continue;
if (_workshopNameToShortname.ContainsKey(shortname)) shortname = _workshopNameToShortname[shortname];
if (!shortname.Equals(itemDefinition.shortname, StringComparison.OrdinalIgnoreCase)) continue;
ulong skinId;
if (itemSkinDirectory.Contains(item.Id)) skinId = (ulong)item.Id;
else if (!ulong.TryParse(item.GetProperty("workshopid"), out skinId)) continue;
if (list.Contains(skinId) || Configuration.Skins.ApprovedLimit > 0 && list.Count >= Configuration.Skins.ApprovedLimit) continue;
list.Add(skinId);
_skinNameLookup[skinId] = item.Name;
_skinSearchLookup[skinId] = $"{skinId} {item.Name}";
}
if (list.Count > 1) {
count += list.Count;
HashSet<ulong> skins;
if (!_skinList.TryGetValue(itemDefinition.shortname, out skins)) skins = _skinList[itemDefinition.shortname] = new HashSet<ulong>();
int removeCount = 0;
list.ForEach((ulong skin) => {
if (Configuration.Skins.RemoveApproved && Configuration.SkinList.ContainsKey(itemDefinition.shortname) && Configuration.SkinList[itemDefinition.shortname].Contains(skin)) {
Configuration.SkinList[itemDefinition.shortname].Remove(skin);
removeCount++;
updateConfig = true;
}
skins.Add(skin);
if (addApprovedPermission) _skinPermissions[skin] = Configuration.Permission.Approved;
}
);
if (removeCount > 0) Debug.Log($"[SkinBox] Removed {removeCount} approved skin ID's for {itemDefinition.shortname} from the config skin list");
}
}
if (updateConfig) SaveConfig();
Facepunch.Pool.FreeList(ref list);
Facepunch.Pool.FreeList(ref itemSkinDirectory);
Debug.Log($"[SkinBox] - Loaded {count} approved skins");
if (Configuration.Skins.UseWorkshop && Configuration.SkinList.Sum(x => x.Value.Count) > 0) VerifyWorkshopSkins();
else {
SortSkinLists();
_skinsLoaded = true;
Hash<string, HashSet<ulong>> skinList = new Hash<string, HashSet<ulong>>();
foreach (KeyValuePair<string, HashSet<ulong>> kvp in _skinList) {
HashSet<ulong> skins = new HashSet<ulong>();
foreach (ulong skinId in kvp.Value) skins.Add(skinId);
skinList.Add(kvp.Key, skins);
}
Configuration.Permission.ReverseCustomSkinPermissions(ref _skinPermissions);
Interface.Oxide.CallHook("OnSkinBoxSkinsLoaded", skinList);
Debug.Log($"[SkinBox] - SkinBox has loaded all required skins and is ready to use! ({_skinList.Values.Sum(x => x.Count)} skins acrosss {_skinList.Count} items)");
}
}
private void SortSkinLists() {
List<ulong> list = Facepunch.Pool.GetList<ulong>();
foreach (KeyValuePair<string, HashSet<ulong>> kvp in _skinList) {
list.AddRange(kvp.Value);
SortSkinList(kvp.Key, ref list);
kvp.Value.Clear();
list.ForEach((ulong skinId) => kvp.Value.Add(skinId));
list.Clear();
}
Facepunch.Pool.FreeList(ref list);
}
private void SortSkinList(string shortname, ref List<ulong> list) {
if (_sorting == SortBy.Alphabetical) {
list.Sort((ulong a, ulong b) => {
string nameA = string.Empty;
string nameB = string.Empty;
_skinNameLookup.TryGetValue(a, out nameA);
_skinNameLookup.TryGetValue(b, out nameB);
return nameA.CompareTo(nameB);
}
);
return;
}
else {
HashSet<ulong> configList;
Configuration.SkinList.TryGetValue(shortname, out configList);
if (configList != null) {
List<ulong> l = configList.ToList();
list.Sort((ulong a, ulong b) => {
int indexA = l.IndexOf(a);
int indexB = l.IndexOf(b);
return _sorting == SortBy.Config ? indexA.CompareTo(indexB) : indexA.CompareTo(indexB) * -1;
}
);
}
}
}
#endregion
#region Workshop Skins
private List<ulong> _skinsToVerify = new List<ulong>();
private Queue<ulong> _collectionsToVerify = new Queue<ulong>();
private const string PUBLISHED_FILE_DETAILS = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/";
private const string COLLECTION_DETAILS = "https://api.steampowered.com/ISteamRemoteStorage/GetCollectionDetails/v1/";
private const string ITEMS_BODY = "?key={0}&itemcount={1}";
private const string ITEM_ENTRY = "&publishedfileids[{0}]={1}";
private const string COLLECTION_BODY = "?key={0}&collectioncount=1&publishedfileids[0]={1}";
private void VerifyWorkshopSkins() {
foreach (HashSet<ulong> list in Configuration.SkinList.Values) _skinsToVerify.AddRange(list);
SendWorkshopQuery();
}
private void SendWorkshopQuery(int page = 0, int success = 0, ConsoleSystem.Arg arg = null, string perm = null) {
int totalPages = Mathf.CeilToInt((float)_skinsToVerify.Count / 100f);
int index = page * 100;
int limit = Mathf.Min((page + 1) * 100, _skinsToVerify.Count);
string details = string.Format(ITEMS_BODY, Configuration.Skins.APIKey, (limit - index));
for (int i = index; i < limit; i++) {
details += string.Format(ITEM_ENTRY, i - index, _skinsToVerify[i]);
}
try {
webrequest.Enqueue(PUBLISHED_FILE_DETAILS, details, (code, response) => ServerMgr.Instance.StartCoroutine(ValidateRequiredSkins(code, response, page + 1, totalPages, success, arg, perm)), this, RequestMethod.POST);
}
catch {
}
}
public enum CollectionAction {
AddSkin, RemoveSkin, ExcludeSkin, RemoveExludeSkin
}
private JsonSerializerSettings m_ErrorHandling = new JsonSerializerSettings() {
Error = delegate(object sender, ErrorEventArgs args) {
Debug.Log($"[SkinBox] Steam response JSON deserialization error;\n{args.ErrorContext.Error.Message}\nThis message can be ignored");
args.ErrorContext.Handled = true;
}
}
;
private void SendWorkshopCollectionQuery(ulong collectionId, CollectionAction action, int success, ConsoleSystem.Arg arg = null, string perm = null) {
string details = string.Format(COLLECTION_BODY, Configuration.Skins.APIKey, collectionId);
try {
webrequest.Enqueue(COLLECTION_DETAILS, details, (code, response) => ServerMgr.Instance.StartCoroutine(ProcessCollectionRequest(collectionId, code, response, action, success, arg, perm)), this, RequestMethod.POST);
}
catch {
}
}
private IEnumerator ValidateRequiredSkins(int code, string response, int page, int totalPages, int success, ConsoleSystem.Arg arg, string perm) {
bool hasChanged = false;
int newSkins = 0;
if (response != null && code == 200) {
QueryResponse queryRespone = JsonConvert.DeserializeObject<QueryResponse>(response, m_ErrorHandling);
if (queryRespone != null && queryRespone.response != null && queryRespone.response.publishedfiledetails?.Length > 0) {
SendResponse($"Processing workshop response. Page: {page} / {totalPages}", arg);
foreach (PublishedFileDetails publishedFileDetails in queryRespone.response.publishedfiledetails) {
if (publishedFileDetails.tags != null) {
foreach (PublishedFileDetails.Tag tag in publishedFileDetails.tags) {
if (string.IsNullOrEmpty(tag.tag)) continue;
ulong workshopid = Convert.ToUInt64(publishedFileDetails.publishedfileid);
string adjTag = tag.tag.ToLower().Replace("skin", "").Replace(" ", "").Replace("-", "").Replace(".item", "");
if (_workshopNameToShortname.ContainsKey(adjTag)) {
string shortname = _workshopNameToShortname[adjTag];
if (shortname == "ammo.snowballgun") continue;
HashSet<ulong> skins;
if (!_skinList.TryGetValue(shortname, out skins)) skins = _skinList[shortname] = new HashSet<ulong>();
if (!skins.Contains(workshopid)) {
skins.Add(workshopid);
_skinNameLookup[workshopid] = publishedFileDetails.title;
_skinSearchLookup[workshopid] = $"{workshopid} {publishedFileDetails.title}";
}
HashSet<ulong> configSkins;
if (!Configuration.SkinList.TryGetValue(shortname, out configSkins)) configSkins = Configuration.SkinList[shortname] = new HashSet<ulong>();
if (!configSkins.Contains(workshopid)) {
hasChanged = true;
configSkins.Add(workshopid);
newSkins += 1;
}
if (!string.IsNullOrEmpty(perm) && !Configuration.Permission.Custom[perm].Contains(workshopid)) {
hasChanged = true;
Configuration.Permission.Custom[perm].Add(workshopid);
}
}
}
}
}
}
}
yield return CoroutineEx.waitForEndOfFrame;
yield return CoroutineEx.waitForEndOfFrame;
if (hasChanged) SaveConfig();
if (page < totalPages) SendWorkshopQuery(page, success + newSkins);
else {
if (_collectionsToVerify.Count != 0) {
SendWorkshopCollectionQuery(_collectionsToVerify.Dequeue(), CollectionAction.AddSkin, success + newSkins, arg, perm);
yield break;
}
if (!_skinsLoaded) {
SortSkinLists();
_skinsLoaded = true;
Hash<string, HashSet<ulong>> skinList = new Hash<string, HashSet<ulong>>();
foreach (KeyValuePair<string, HashSet<ulong>> kvp in _skinList) {
HashSet<ulong> skins = new HashSet<ulong>();
foreach (ulong skinId in kvp.Value) skins.Add(skinId);
skinList.Add(kvp.Key, skins);
}
Configuration.Permission.ReverseCustomSkinPermissions(ref _skinPermissions);
Interface.Oxide.CallHook("OnSkinBoxSkinsLoaded", skinList);
Debug.Log($"[SkinBox] - SkinBox has loaded all required skins and is ready to use! ({_skinList.Values.Sum(x => x.Count)} skins acrosss {_skinList.Count} items)");
}
else SendResponse($"{success + newSkins} new skins have been added!", arg);
}
}
private IEnumerator ProcessCollectionRequest(ulong collectionId, int code, string response, CollectionAction action, int success, ConsoleSystem.Arg arg, string perm) {
if (response != null && code == 200) {
SendResponse($"Processing response for collection {collectionId}", arg);
CollectionQueryResponse collectionQuery = JsonConvert.DeserializeObject<CollectionQueryResponse>(response, m_ErrorHandling);
if (collectionQuery == null || !(collectionQuery is CollectionQueryResponse)) {
SendResponse("Failed to receive a valid workshop collection response", arg);
yield break;
}
if (collectionQuery.response.resultcount == 0 || collectionQuery.response.collectiondetails == null || collectionQuery.response.collectiondetails.Length == 0 || collectionQuery.response.collectiondetails[0].result != 1) {
SendResponse("Failed to receive a valid workshop collection response", arg);
yield break;
}
_skinsToVerify.Clear();
foreach (CollectionChild child in collectionQuery.response.collectiondetails[0].children) {
try {
switch (child.filetype) {
case 1 : _skinsToVerify.Add(Convert.ToUInt64(child.publishedfileid));
break;
case 2 : _collectionsToVerify.Enqueue(Convert.ToUInt64(child.publishedfileid));
SendResponse($"Collection {collectionId} contains linked collection {child.publishedfileid}", arg);
break;
default : break;
}
}
catch {
}
}
if (_skinsToVerify.Count == 0) {
if (_collectionsToVerify.Count != 0) {
SendWorkshopCollectionQuery(_collectionsToVerify.Dequeue(), action, success, arg, perm);
yield break;
}
SendResponse("No valid skin ID's in the specified collection", arg);
yield break;
}
switch (action) {
case CollectionAction.AddSkin : SendWorkshopQuery(0, success, arg, perm);
break;
case CollectionAction.RemoveSkin : RemoveSkins(arg, perm);
break;
case CollectionAction.ExcludeSkin : AddSkinExclusions(arg);
break;
case CollectionAction.RemoveExludeSkin : RemoveSkinExclusions(arg);
break;
}
}
else SendResponse($"[SkinBox] Collection response failed. Error code {code}", arg);
}
private void RemoveSkins(ConsoleSystem.Arg arg, string perm = null) {
int removedCount = 0;
for (int y = _skinList.Count - 1; y >= 0; y--) {
KeyValuePair<string, HashSet<ulong>> skin = _skinList.ElementAt(y);
for (int i = 0; i < _skinsToVerify.Count; i++) {
ulong skinId = _skinsToVerify[i];
if (skin.Value.Contains(skinId)) {
skin.Value.Remove(skinId);
Configuration.SkinList[skin.Key].Remove(skinId);
removedCount++;
if (!string.IsNullOrEmpty(perm)) Configuration.Permission.Custom[perm].Remove(skinId);
}
}
}
if (removedCount > 0) SaveConfig();
SendReply(arg, $"[SkinBox] - Removed {removedCount} skins");
if (_collectionsToVerify.Count != 0) SendWorkshopCollectionQuery(_collectionsToVerify.Dequeue(), CollectionAction.RemoveSkin, 0, arg, perm);
}
private void AddSkinExclusions(ConsoleSystem.Arg arg) {
int count = 0;
foreach (ulong skinId in _skinsToVerify) {
if (!Configuration.Blacklist.Contains(skinId)) {
Configuration.Blacklist.Add(skinId);
count++;
}
}
if (count > 0) {
SendResponse($"Added {count} new skin ID's to the excluded list", arg);
SaveConfig();
}
}
private void RemoveSkinExclusions(ConsoleSystem.Arg arg) {
int count = 0;
foreach (ulong skinId in _skinsToVerify) {
if (Configuration.Blacklist.Contains(skinId)) {
Configuration.Blacklist.Remove(skinId);
count++;
}
}
if (count > 0) {
SendResponse($"Removed {count} skin ID's from the excluded list", arg);
SaveConfig();
}
}
private void SendResponse(string message, ConsoleSystem.Arg arg) {
if (arg != null) SendReply(arg, message);
else Debug.Log($"[SkinBox] - {message}");
}
#endregion
#region Workshop Name Conversions
private 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"
}
, {
"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"
}
, {
"burlapgloves", "burlap.gloves"
}
, {
"burlap.gloves", "burlap.gloves"
}
, {
"leather.gloves", "burlap.gloves"
}
, {
"python", "pistol.python"
}
, {
"m39", "rifle.m39"
}
, {
"l96", "rifle.l96"
}
, {
"woodendoubledoor", "door.double.hinged.wood"
}
}
;
private void UpdateWorkshopNameConversionList() {
foreach (ItemDefinition item in ItemManager.itemList) {
_shortnameToDisplayname[item.shortname] = item.displayName.english;
string workshopName = item.displayName.english.ToLower().Replace("skin", "").Replace(" ", "").Replace("-", "");
if (!_workshopNameToShortname.ContainsKey(workshopName)) _workshopNameToShortname[workshopName] = item.shortname;
if (!_workshopNameToShortname.ContainsKey(item.shortname)) _workshopNameToShortname[item.shortname] = item.shortname;
if (!_workshopNameToShortname.ContainsKey(item.shortname.Replace(".", ""))) _workshopNameToShortname[item.shortname.Replace(".", "")] = item.shortname;
}
foreach (Skinnable skin in Skinnable.All.ToList()) {
if (string.IsNullOrEmpty(skin.Name) || string.IsNullOrEmpty(skin.ItemName) || _workshopNameToShortname.ContainsKey(skin.Name.ToLower())) continue;
_workshopNameToShortname[skin.Name.ToLower()] = skin.ItemName.ToLower();
}
}
private void FixLR300InvalidShortname() {
const string LR300_ITEM = "lr300.item";
const string LR300 = "rifle.lr300";
HashSet<ulong> list;
if (Configuration.SkinList.TryGetValue(LR300_ITEM, out list)) {
Configuration.SkinList.Remove(LR300_ITEM);
Configuration.SkinList[LR300] = list;
SaveConfig();
}
}
#endregion
#region Item Skin Redirects
private Hash<string, string> _itemSkinRedirects = new Hash<string, string>();
private void FindItemRedirects() {
bool addApprovedPermission = Configuration.Permission.Approved != Configuration.Permission.Use;
foreach (ItemSkinDirectory.Skin skin in ItemSkinDirectory.Instance.skins) {
ItemSkin itemSkin = skin.invItem as ItemSkin;
if (itemSkin == null || itemSkin.Redirect == null) continue;
_itemSkinRedirects[itemSkin.Redirect.shortname] = itemSkin.itemDefinition.shortname;
if (Configuration.Skins.UseRedirected) {
// ... [Code preview truncated due to length. Full source in attached file] ...