C#:
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace Oxide.Plugins {
[Info("SkinBox", "k1lly0u", "2.0.14"), 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<string, HashSet<ulong>> _skinList = new Hash<string, HashSet<ulong>>();
private Hash<ulong, string> _skinPermissions = new Hash<ulong, string>();
private Hash<ulong, LootHandler> _activeSkinBoxes = new Hash<ulong, LootHandler>();
private Hash<string, string> _shortnameToDisplayname = new Hash<string, string>();
private Hash<ulong, string> _skinNameLookup = new Hash<ulong, string>();
private Hash<ulong, double> _cooldownTimes = new Hash<ulong, double>();
private bool _apiKeyMissing = false;
private bool _skinsLoaded = false;
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 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.Permission.ReverseCustomSkinPermissions(ref _skinPermissions);
Configuration.Command.RegisterCommands(cmd, this);
_costType = ParseType<CostType>(Configuration.Cost.Currency);
_sorting = ParseType<SortBy>(Configuration.Skins.Sorting);
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", ["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", ["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",
}
, this);
}
private void OnServerInitialized() {
UnsubscribeHooks();
if (string.IsNullOrEmpty(Configuration.Skins.APIKey)) {
_apiKeyMissing = true;
SendAPIMissingWarning();
return;
}
if ((Steamworks.SteamInventory.Definitions?.Length ?? 0) == 0) {
PrintWarning("Waiting for Steamworks to update item definitions....");
Steamworks.SteamInventory.OnDefinitionsUpdated += StartSkinRequest;
}
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;
}
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, uint 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 == 0 || 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.Destroy(lootHandler);
}
private void Unload() {
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 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 void CreateSkinBox(BasePlayer player) {
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);
}
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();
}
#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();
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);
}
}
}
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 StartSkinRequest() {
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;
}
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;
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 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 {
}
}
private void SendWorkshopCollectionQuery(ulong collectionId, bool add, 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(code, response, add, 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);
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;
}
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 (!_skinsLoaded) {
SortSkinLists();
_skinsLoaded = true;
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(int code, string response, bool add, int success, ConsoleSystem.Arg arg, string perm) {
if (response != null && code == 200) {
SendResponse("Processing collection response", arg);
CollectionQueryResponse collectionQuery = JsonConvert.DeserializeObject<CollectionQueryResponse>(response);
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 {
_skinsToVerify.Add(Convert.ToUInt64(child.publishedfileid));
}
catch {
}
}
if (_skinsToVerify.Count == 0) {
SendResponse("No valid skin ID's in the specified collection", arg);
yield break;
}
if (add) SendWorkshopQuery(0, success, arg, perm);
else RemoveSkins(arg, perm);
}
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");
}
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) {
HashSet<ulong> skins;
if (!_skinList.TryGetValue(itemSkin.itemDefinition.shortname, out skins)) skins = _skinList[itemSkin.itemDefinition.shortname] = new HashSet<ulong>();
skins.Add((ulong)skin.id);
if (addApprovedPermission) _skinPermissions[(ulong)skin.id] = Configuration.Permission.Approved;
}
}
}
private string GetRedirectedShortname(string shortname) {
string redirectedName;
if (_itemSkinRedirects.TryGetValue(shortname, out redirectedName)) return redirectedName;
return shortname;
}
#endregion
#region SkinBox Component
private class LootHandler : MonoBehaviour {
internal StorageContainer Entity {
get;
private set;
}
internal BasePlayer Looter {
get;
set;
}
internal bool HasItem {
get;
private set;
}
internal int InputAmount => inputItem?.amount ?? 0;
private InputItem inputItem;
private int _currentPage = 0;
private int _maximumPages = 0;
private int _itemsPerPage;
private List<ulong> _availableSkins;
private bool _isFillingContainer;
private void Awake() {
_availableSkins = Facepunch.Pool.GetList<ulong>();
Entity = GetComponent<StorageContainer>();
if (!Configuration.Other.AllowStacks) {
Entity.maxStackSize = 1;
Entity.inventory.maxStackSize = 1;
}
Entity.SetFlag(BaseEntity.Flags.Open, true, false);
}
private void OnDestroy() {
CuiHelper.DestroyUi(Looter, UI_PANEL);
Instance?._activeSkinBoxes?.Remove(Looter.userID);
if (HasItem && inputItem != null) Looter.GiveItem(inputItem.Create(), BaseEntity.GiveItemReason.PickedUp);
Facepunch.Pool.FreeList(ref _availableSkins);
if (Entity != null && !Entity.IsDestroyed) {
if (Entity.inventory.itemList.Count > 0) ClearContainer();
Entity.Kill(BaseNetworkable.DestroyMode.None);
}
Instance?.ToggleHooks();
}
internal void OnItemAdded(Item item) {
if (HasItem) return;
HasItem = true;
string shortname = Instance.GetRedirectedShortname(item.info.shortname);
Instance.GetSkinsFor(Looter, shortname, ref _availableSkins);
_availableSkins.Remove(0UL);
if (item.skin != 0UL) _availableSkins.Remove(item.skin);
inputItem = new InputItem(shortname, item);
_itemsPerPage = inputItem.skin == 0UL ? 41 : 40;
_currentPage = 0;
_maximumPages = Mathf.Min(Configuration.Skins.MaximumPages, Mathf.CeilToInt((float)_availableSkins.Count / (float)_itemsPerPage));
if (_currentPage > 0 || _maximumPages > 1) CreateOverlay();
RemoveItem(item);
ClearContainer();
StartCoroutine(FillContainer());
}
internal void OnItemRemoved(Item item) {
if (!HasItem) return;
CuiHelper.DestroyUi(Looter, UI_PANEL);
bool skinChanged = item.skin != 0UL && item.skin != inputItem.skin;
inputItem.CloneTo(item);
if (skinChanged && !Instance.ChargePlayer(Looter, inputItem.itemDefinition.category)) {
item.skin = inputItem.skin;
if (item.GetHeldEntity() != null) item.GetHeldEntity().skinID = inputItem.skin;
PopupMessage(string.Format(GetMessage("NotEnoughBalanceTake", Looter.userID), item.info.displayName.english, Instance.GetCostType(Looter.userID)));
}
if (item.skin != 0UL && Configuration.Skins.ShowSkinIDs) Instance._skinNameLookup.TryGetValue(item.skin, out item.name);
item.MarkDirty();
ClearContainer();
Entity.inventory.MarkDirty();
inputItem.Dispose();
inputItem = null;
HasItem = false;
if (Configuration.Cooldown.ActivateOnTake) Instance.ApplyCooldown(Looter);
if (Instance.IsOnCooldown(Looter)) Looter.EndLooting();
}
internal void ChangePage(int change) {
if (_isFillingContainer) return;
_currentPage = Mathf.Clamp(_currentPage + change, 0, _maximumPages);
StartCoroutine(RefillContainer());
}
private IEnumerator RefillContainer() {
ClearContainer();
yield return StartCoroutine(FillContainer());
if (HasItem) CreateOverlay();
}
private IEnumerator FillContainer() {
_isFillingContainer = true;
CreateItem(0UL);
if (inputItem.skin != 0UL) CreateItem(inputItem.skin);
for (int i = _currentPage * _itemsPerPage; i < Mathf.Min(_availableSkins.Count, (_currentPage + 1) * _itemsPerPage);
i++) {
if (!HasItem) break;
CreateItem(_availableSkins[i]);
if (i % 2 == 0) yield return null;
}
_isFillingContainer = false;
}
private void ClearContainer() {
for (int i = Entity.inventory.itemList.Count - 1; i >= 0; i--) RemoveItem(Entity.inventory.itemList[i]);
}
private Item CreateItem(ulong skinId) {
Item item = ItemManager.Create(inputItem.itemDefinition, 1, skinId);
item.contents?.SetFlag(ItemContainer.Flag.IsLocked, true);
item.contents?.SetFlag(ItemContainer.Flag.NoItemInput, true);
if (skinId != 0UL) {
Instance._skinNameLookup.TryGetValue(skinId, out item.name);
if (Configuration.Skins.ShowSkinIDs && Looter.IsAdmin) item.name = $"{item.name} ({skinId})";
}
if (!InsertItem(item)) item.Remove(0f);
else item.MarkDirty();
return item;
}
private bool InsertItem(Item item) {
if (Entity.inventory.itemList.Contains(item)) return false;
if (Entity.inventory.IsFull()) return false;
Entity.inventory.itemList.Add(item);
item.parent = Entity.inventory;
if (!Entity.inventory.FindPosition(item)) return false;
Entity.inventory.MarkDirty();
Entity.inventory.onItemAddedRemoved?.Invoke(item, true);
return true;
}
private void RemoveItem(Item item) {
if (!Entity.inventory.itemList.Contains(item)) return;
Entity.inventory.onPreItemRemove?.Invoke(item);
Entity.inventory.itemList.Remove(item);
item.parent = null;
Entity.inventory.MarkDirty();
Entity.inventory.onItemAddedRemoved?.Invoke(item, false);
item.Remove(0f);
}
internal void CheckItemHasSplit(Item item) => StartCoroutine(CheckItemHasSplit(item, item.amount));
// Item split dupe solution?
private IEnumerator CheckItemHasSplit(Item item, int originalAmount) {
yield return null;
if (item != null && item.amount != originalAmount) {
int splitAmount = originalAmount - item.amount;
Looter.inventory.Take(null, item.info.itemid, splitAmount);
item.amount += splitAmount;
}
}
private class InputItem {
public ItemDefinition itemDefinition;
public int amount;
public ulong skin;
public float condition;
public float maxCondition;
public int magazineContents;
public int magazineCapacity;
public ItemDefinition ammoType;
public List<InputItem> contents;
internal InputItem(string shortname, Item item) {
if (!item.info.shortname.Equals(shortname)) itemDefinition = ItemManager.FindItemDefinition(shortname);
else itemDefinition = item.info;
amount = item.amount;
skin = item.skin;
if (item.hasCondition) {
condition = item.condition;
maxCondition = item.maxCondition;
}
BaseProjectile baseProjectile = item.GetHeldEntity() as BaseProjectile;
if (baseProjectile != null) {
magazineContents = baseProjectile.primaryMagazine.contents;
magazineCapacity = baseProjectile.primaryMagazine.capacity;
ammoType = baseProjectile.primaryMagazine.ammoType;
// ... [Code preview truncated due to length. Full source in attached file] ...