// #define DEBUG using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Game.Rust.Cui; using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; // TODO: Re-implement visual backpack // TODO: Try to simulate a dropped item container as item container sourceEntity to customize the loot text. namespace Oxide.Plugins { [Info("Backpacks", "LaserHydra", "3.4.0")] [Description("Allows players to have a Backpack which provides them extra inventory space.")] internal class Backpacks : RustPlugin { #region Fields private const ushort MinSize = 1; private const ushort MaxSize = 7; private const ushort SlotsPerRow = 6; private const string GUIPanelName = "BackpacksUI"; private const string UsagePermission = "backpacks.use"; private const string GUIPermission = "backpacks.gui"; private const string FetchPermission = "backpacks.fetch"; private const string AdminPermission = "backpacks.admin"; private const string KeepOnDeathPermission = "backpacks.keepondeath"; private const string KeepOnWipePermission = "backpacks.keeponwipe"; private const string NoBlacklistPermission = "backpacks.noblacklist"; private const string BackpackPrefab = "assets/prefabs/misc/item drop/item_drop_backpack.prefab"; private readonly Dictionary _backpacks = new Dictionary(); private readonly Dictionary _openBackpacks = new Dictionary(); private readonly Dictionary _lastDroppedBackpacks = new Dictionary(); private Dictionary _backpackSizePermissions = new Dictionary(); private static Backpacks _instance; private Configuration _config; private StoredData _storedData; [PluginReference] private RustPlugin EventManager; #endregion #region Hooks private void Loaded() { _instance = this; permission.RegisterPermission(UsagePermission, this); permission.RegisterPermission(GUIPermission, this); permission.RegisterPermission(FetchPermission, this); permission.RegisterPermission(AdminPermission, this); permission.RegisterPermission(KeepOnDeathPermission, this); permission.RegisterPermission(KeepOnWipePermission, this); permission.RegisterPermission(NoBlacklistPermission, this); for (ushort size = MinSize; size <= MaxSize; size++) { var sizePermission = $"{UsagePermission}.{size}"; permission.RegisterPermission(sizePermission, this); _backpackSizePermissions.Add(sizePermission, size); } _backpackSizePermissions = _backpackSizePermissions .OrderByDescending(kvp => kvp.Value) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (!_config.DropOnDeath || !ConVar.Server.corpses) { Unsubscribe(nameof(OnPlayerCorpseSpawned)); } _storedData = StoredData.Load(); foreach (var player in BasePlayer.activePlayerList) CreateGUI(player); } private void Unload() { _storedData.Save(); foreach (var backpack in _backpacks.Values) { backpack.ForceCloseAllLooters(); backpack.SaveData(); backpack.KillContainer(); } foreach (var player in BasePlayer.activePlayerList) DestroyGUI(player); } private void OnNewSave(string filename) { // Ensure config is loaded LoadConfig(); if (!_config.ClearBackpacksOnWipe) return; _backpacks.Clear(); IEnumerable fileNames = Interface.Oxide.DataFileSystem.GetFiles(Name) .Select(fn => { return fn.Split(Path.DirectorySeparatorChar).Last() .Replace(".json", string.Empty); }); int skippedBackpacks = 0; foreach (var fileName in fileNames) { ulong userId; if (!ulong.TryParse(fileName, out userId)) continue; if (permission.UserHasPermission(fileName, KeepOnWipePermission)) { skippedBackpacks++; continue; } var backpack = new Backpack(userId); backpack.SaveData(); } string skippedBackpacksMessage = skippedBackpacks > 0 ? $", except {skippedBackpacks} due to being exempt" : string.Empty; PrintWarning($"New save created. All backpacks were cleared{skippedBackpacksMessage}. Players with the '{KeepOnWipePermission}' permission are exempt. Clearing backpacks can be disabled for all players in the configuration file."); } private void OnServerSave() { _storedData.Save(); if (_config.SaveBackpacksOnServerSave) { foreach (var backpack in _backpacks.Values) { backpack.ForceCloseAllLooters(); backpack.SaveData(); backpack.KillContainer(); } _backpacks.Clear(); } } private void OnPlayerDisconnected(BasePlayer player) { if (_openBackpacks.ContainsKey(player)) _openBackpacks[player].OnClose(player); if (!_config.SaveBackpacksOnServerSave && _backpacks.ContainsKey(player.userID)) { var backpack = _backpacks[player.userID]; backpack.ForceCloseAllLooters(); backpack.SaveData(); backpack.KillContainer(); _backpacks.Remove(player.userID); } } private object CanLootPlayer(BasePlayer looted, BasePlayer looter) { if (_openBackpacks.ContainsKey(looter) && (looter == looted || permission.UserHasPermission(looter.UserIDString, AdminPermission))) { return true; } return null; } private object CanAcceptItem(ItemContainer container, Item item) { if (!_config.UseBlacklist) return null; Backpack backpack = _backpacks.Values.FirstOrDefault(b => b.IsUnderlyingContainer(container)); if (backpack != null && !permission.UserHasPermission(backpack.OwnerIdString, NoBlacklistPermission)) { // Is the Item blacklisted if (_config.BlacklistedItems.Any(shortName => shortName == item.info.shortname)) return ItemContainer.CanAcceptResult.CannotAccept; object hookResult = Interface.CallHook("CanBackpackAcceptItem", backpack.OwnerId, container, item); if (hookResult is bool && (bool)hookResult == false) return ItemContainer.CanAcceptResult.CannotAccept; } return null; } private void OnPlayerLootEnd(PlayerLoot playerLoot) { var player = (BasePlayer) playerLoot.gameObject.ToBaseEntity(); if (_openBackpacks.ContainsKey(player)) { _openBackpacks[player].OnClose(player); } } private void OnEntityDeath(BaseCombatEntity victim, HitInfo info) { if (victim is BasePlayer && !victim.IsNpc) { var player = (BasePlayer) victim; DestroyGUI(player); if (Backpack.HasBackpackFile(player.userID)) { var backpack = Backpack.Get(player.userID); backpack.ForceCloseAllLooters(); if (permission.UserHasPermission(player.UserIDString, KeepOnDeathPermission)) return; if (_config.EraseOnDeath) backpack.EraseContents(); else if (_config.DropOnDeath) { DropBackpackWithReducedCorpseCollision(backpack, player.transform.position); } } } } private void OnPlayerCorpseSpawned(BasePlayer player, BaseCorpse corpse) { if (!_lastDroppedBackpacks.ContainsKey(player.userID)) return; var container = _lastDroppedBackpacks[player.userID]; if (container == null) return; var corpseCollider = corpse.GetComponent(); var containerCollider = _lastDroppedBackpacks[player.userID].GetComponent(); if (corpseCollider != null && containerCollider != null) Physics.IgnoreCollision(corpseCollider, containerCollider); _lastDroppedBackpacks.Remove(player.userID); } private void OnGroupPermissionGranted(string group, string perm) { if (perm.Equals(GUIPermission)) { foreach (IPlayer player in covalence.Players.Connected.Where(p => permission.UserHasGroup(p.Id, group))) { CreateGUI(player.Object as BasePlayer); } } } private void OnGroupPermissionRevoked(string group, string perm) { if (perm.Equals(GUIPermission)) { foreach (IPlayer player in covalence.Players.Connected.Where(p => permission.UserHasGroup(p.Id, group))) { DestroyGUI(player.Object as BasePlayer); } } } private void OnUserPermissionGranted(string userId, string perm) { if (perm.Equals(GUIPermission)) CreateGUI(BasePlayer.Find(userId)); } private void OnUserPermissionRevoked(string userId, string perm) { if (perm.Equals(GUIPermission)) DestroyGUI(BasePlayer.Find(userId)); } private void OnPlayerConnected(BasePlayer player) { CreateGUI(player); } private void OnPlayerSleepEnded(BasePlayer player) { CreateGUI(player); } #endregion #region Commands #if DEBUG [ChatCommand("c")] private void RunContainerDebugCommand(BasePlayer player) { RaycastHit hit; if (Physics.Raycast(player.eyes.HeadRay(), out hit)) { var entity = hit.GetEntity(); if (entity != null) { var storageContainer = entity.GetComponent(); if (storageContainer != null) { var data = new Dictionary { ["panelName"] = storageContainer.panelName, ["uid"] = storageContainer.inventory.uid, ["isServer"] = storageContainer.inventory.isServer, ["capacity"] = storageContainer.inventory.capacity, ["maxStackSize"] = storageContainer.inventory.maxStackSize, ["entityOwner"] = storageContainer.inventory.entityOwner, ["playerOwner"] = storageContainer.inventory.playerOwner, ["flags"] = storageContainer.inventory.flags, ["allowedContents"] = storageContainer.inventory.allowedContents, ["availableSlots"] = storageContainer.inventory.availableSlots.Count }; PrintToChat(string.Join(Environment.NewLine, data.Select(kvp => $"[{kvp.Key}] : {kvp.Value}").ToArray())); timer.In(0.5f, () => PlayerLootContainer(player, storageContainer.inventory)); } else PrintToChat("not a storage container"); } } } #endif [ChatCommand("backpack")] private void OpenBackpackChatCommand(BasePlayer player, string cmd, string[] args) { if (permission.UserHasPermission(player.UserIDString, UsagePermission)) timer.Once(0.5f, () => Backpack.Get(player.userID).Open(player)); else PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString)); } [ConsoleCommand("backpack.open")] private void OpenBackpackConsoleCommand(ConsoleSystem.Arg arg) { BasePlayer player = arg.Player(); if (player == null || !player.IsAlive()) return; if (!permission.UserHasPermission(player.UserIDString, UsagePermission)) { PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString)); return; } if (_openBackpacks.ContainsKey(player)) { // HACK: Send empty respawn information to fully close the player inventory (toggle backpack closed) player.ClientRPCPlayer(null, player, "OnRespawnInformation"); return; } player.EndLooting(); timer.Once(0.1f, () => Backpack.Get(player.userID).Open(player)); } [ConsoleCommand("backpack.fetch")] private void FetchBackpackItemConsoleCommand(ConsoleSystem.Arg arg) { BasePlayer player = arg.Player(); if (player == null || !player.IsAlive()) return; if (!permission.UserHasPermission(player.UserIDString, FetchPermission)) { PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString)); return; } if (!arg.HasArgs(2)) { PrintToConsole(player, lang.GetMessage("Backpack Fetch Syntax", this, player.UserIDString)); return; } if (!VerifyCanOpenBackpack(player, player.userID)) return; string[] args = arg.Args; string itemArg = args[0]; int itemID; ItemDefinition itemDefinition = ItemManager.FindItemDefinition(itemArg); if (itemDefinition != null) { itemID = itemDefinition.itemid; } else { // User may have provided an itemID instead of item short name if (!int.TryParse(itemArg, out itemID)) { PrintToChat(player, lang.GetMessage("Invalid Item", this, player.UserIDString)); return; } itemDefinition = ItemManager.FindItemDefinition(itemID); if (itemDefinition == null) { PrintToChat(player, lang.GetMessage("Invalid Item", this, player.UserIDString)); return; } } int desiredAmount; if (!int.TryParse(args[1], out desiredAmount)) { PrintToChat(player, lang.GetMessage("Invalid Item Amount", this, player.UserIDString)); return; } if (desiredAmount < 1) { PrintToChat(player, lang.GetMessage("Invalid Item Amount", this, player.UserIDString)); return; } string itemLocalizedName = itemDefinition.displayName.translated; Backpack backpack = Backpack.Get(player.userID); int quantityInBackpack = backpack.GetItemQuantity(itemID); if (quantityInBackpack == 0) { PrintToChat(player, lang.GetMessage("Item Not In Backpack", this, player.UserIDString), itemLocalizedName); return; } if (desiredAmount > quantityInBackpack) desiredAmount = quantityInBackpack; int amountTransferred = backpack.MoveItemsToPlayerInventory(player, itemID, desiredAmount); if (amountTransferred > 0) { PrintToChat(player, lang.GetMessage("Items Fetched", this, player.UserIDString), amountTransferred, itemLocalizedName); } else { PrintToChat(player, lang.GetMessage("Fetch Failed", this, player.UserIDString), itemLocalizedName); } } [ChatCommand("viewbackpack")] private void ViewBackpack(BasePlayer player, string cmd, string[] args) { if (!permission.UserHasPermission(player.UserIDString, AdminPermission)) { PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString)); return; } if (args.Length != 1) { PrintToChat(player, lang.GetMessage("View Backpack Syntax", this, player.UserIDString)); return; } string failureMessage; IPlayer targetPlayer = FindPlayer(args[0], out failureMessage); if (targetPlayer == null) { PrintToChat(player, failureMessage); return; } BasePlayer targetBasePlayer = targetPlayer.Object as BasePlayer; ulong id = targetBasePlayer?.userID ?? ulong.Parse(targetPlayer.Id); Backpack backpack = Backpack.Get(id); timer.Once(0.5f, () => backpack.Open(player)); } [ChatCommand("backpackgui")] private void ToggleBackpackGUI(BasePlayer player, string cmd, string[] args) { if (!permission.UserHasPermission(player.UserIDString, GUIPermission)) { PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString)); return; } if (_storedData.PlayersWithDisabledGUI.Contains(player.userID)) { _storedData.PlayersWithDisabledGUI.Remove(player.userID); CreateGUI(player); } else { _storedData.PlayersWithDisabledGUI.Add(player.userID); DestroyGUI(player); } PrintToChat(player, lang.GetMessage("Toggled Backpack GUI", this, player.UserIDString)); } #endregion #region Helper Methods // Data migration from v2.x.x to v3.x.x private static bool TryMigrateData(string fileName) { if (!Interface.Oxide.DataFileSystem.ExistsDatafile(fileName)) { return false; } Dictionary data; LoadData(out data, fileName); if (data.ContainsKey("ownerID") && data.ContainsKey("Inventory")) { var inventory = (JObject) data["Inventory"]; data["OwnerID"] = data["ownerID"]; data["Items"] = inventory.Value("Items"); data.Remove("ownerID"); data.Remove("Inventory"); data.Remove("Size"); SaveData(data, fileName); return true; } return false; } private static void PlayerLootContainer(BasePlayer player, ItemContainer container) { player.inventory.loot.Clear(); player.inventory.loot.PositionChecks = false; player.inventory.loot.entitySource = container.entityOwner ?? player; player.inventory.loot.itemSource = null; player.inventory.loot.MarkDirty(); player.inventory.loot.AddContainer(container); player.inventory.loot.SendImmediate(); player.ClientRPCPlayer(null, player, "RPC_OpenLootPanel", "genericlarge"); } private IPlayer FindPlayer(string nameOrID, out string failureMessage) { failureMessage = string.Empty; ulong userId; if (nameOrID.StartsWith("7656119") && nameOrID.Length == 17 && ulong.TryParse(nameOrID, out userId)) { IPlayer player = covalence.Players.All.FirstOrDefault(p => p.Id == nameOrID); if (player == null) failureMessage = string.Format(lang.GetMessage("User ID not Found", this), nameOrID); return player; } var foundPlayers = new List(); foreach (IPlayer player in covalence.Players.All) { if (player.Name.Equals(nameOrID, StringComparison.InvariantCultureIgnoreCase)) return player; if (player.Name.ToLower().Contains(nameOrID.ToLower())) foundPlayers.Add(player); } switch (foundPlayers.Count) { case 0: failureMessage = string.Format(lang.GetMessage("User Name not Found", this), nameOrID); return null; case 1: return foundPlayers[0]; default: string names = string.Join(", ", foundPlayers.Select(p => p.Name).ToArray()); failureMessage = string.Format(lang.GetMessage("Multiple Players Found", this), names); return null; } } private bool VerifyCanOpenBackpack(BasePlayer looter, ulong ownerId) { if (IsPlayingEvent(looter)) { PrintToChat(looter, lang.GetMessage("May Not Open Backpack In Event", this, looter.UserIDString)); return false; } var hookResult = Interface.Oxide.CallHook("CanOpenBackpack", looter, ownerId); if (hookResult != null && hookResult is string) { _instance.PrintToChat(looter, hookResult as string); return false; } return true; } private bool IsPlayingEvent(BasePlayer player) { if (EventManager == null) return false; // EventManager 3.x var isPlayingResult = EventManager.Call("isPlaying", player); if (isPlayingResult != null) return isPlayingResult is bool && (bool)isPlayingResult; // EventManager 4.x var isEventPlayerResult = EventManager.Call("IsEventPlayer", player); return isEventPlayerResult is bool && (bool)isEventPlayerResult; } private DroppedItemContainer DropBackpackWithReducedCorpseCollision(Backpack backpack, Vector3 position) { var droppedContainer = backpack.Drop(position); if (droppedContainer != null && ConVar.Server.corpses) { if (_lastDroppedBackpacks.ContainsKey(backpack.OwnerId)) _lastDroppedBackpacks[backpack.OwnerId] = droppedContainer; else _lastDroppedBackpacks.Add(backpack.OwnerId, droppedContainer); } return droppedContainer; } private void CreateGUI(BasePlayer player) { if (player == null || player.IsNpc || !player.IsAlive()) return; if (!permission.UserHasPermission(player.UserIDString, GUIPermission)) return; if (_storedData.PlayersWithDisabledGUI.Contains(player.userID)) return; CuiHelper.DestroyUi(player, GUIPanelName); var elements = new CuiElementContainer(); var BackpacksUIPanel = elements.Add(new CuiPanel { Image = { Color = _instance._config.GUI.Color }, RectTransform = { AnchorMin = _config.GUI.GUIButtonPosition.AnchorsMin, AnchorMax = _config.GUI.GUIButtonPosition.AnchorsMax, OffsetMin = _config.GUI.GUIButtonPosition.OffsetsMin, OffsetMax = _config.GUI.GUIButtonPosition.OffsetsMax }, CursorEnabled = false }, "Overlay", GUIPanelName); elements.Add(new CuiElement { Parent = GUIPanelName, Components = { new CuiRawImageComponent { Url = _instance._config.GUI.Image }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); elements.Add(new CuiButton { Button = { Command = "backpack.open", Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }, Text = { Text = "" } }, BackpacksUIPanel); CuiHelper.AddUi(player, elements); } private void DestroyGUI(BasePlayer player) { CuiHelper.DestroyUi(player, GUIPanelName); } private static void LoadData(out T data, string filename = null) => data = Interface.Oxide.DataFileSystem.ReadObject(filename ?? _instance.Name); private static void SaveData(T data, string filename = null) => Interface.Oxide.DataFileSystem.WriteObject(filename ?? _instance.Name, data); #endregion #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["No Permission"] = "You don't have permission to use this command.", ["May Not Open Backpack In Event"] = "You may not open a backpack while participating in an event!", ["View Backpack Syntax"] = "Syntax: /viewbackpack ", ["User ID not Found"] = "Could not find player with ID '{0}'", ["User Name not Found"] = "Could not find player with name '{0}'", ["Multiple Players Found"] = "Multiple matching players found:\n{0}", ["Backpack Over Capacity"] = "Your backpack was over capacity. Overflowing items were added to your inventory or dropped.", ["Backpack Fetch Syntax"] = "Syntax: backpack.fetch ", ["Invalid Item"] = "Invalid Item Name or ID.", ["Invalid Item Amount"] = "Item amount must be an integer greater than 0.", ["Item Not In Backpack"] = "Item \"{0}\" not found in backpack.", ["Items Fetched"] = "Fetched {0} \"{1}\" from backpack.", ["Fetch Failed"] = "Couldn't fetch \"{0}\" from backpack. Inventory may be full.", ["Toggled Backpack GUI"] = "Toggled backpack GUI button.", }, this); } #endregion #region Configuration protected override void LoadConfig() { base.LoadConfig(); _config = Config.ReadObject(); SaveConfig(); } protected override void SaveConfig() => Config.WriteObject(_config); protected override void LoadDefaultConfig() { _config = new Configuration { BlacklistedItems = new HashSet { "autoturret", "lmg.m249" } }; SaveConfig(); } private class Configuration { private ushort _backpackSize = 1; [JsonProperty("Backpack Size (1-7 Rows)")] public ushort BackpackSize { get { return _backpackSize; } set { _backpackSize = (ushort) Mathf.Clamp(value, MinSize, MaxSize); } } [JsonProperty("Drop on Death (true/false)")] public bool DropOnDeath = true; [JsonProperty("Erase on Death (true/false)")] public bool EraseOnDeath = false; [JsonProperty("Use Blacklist (true/false)")] public bool UseBlacklist = false; [JsonProperty("Clear Backpacks on Map-Wipe (true/false)")] public bool ClearBackpacksOnWipe = false; [JsonProperty("Only Save Backpacks on Server-Save (true/false)")] public bool SaveBackpacksOnServerSave = false; [JsonProperty("Blacklisted Items (Item Shortnames)")] public HashSet BlacklistedItems; [JsonProperty(PropertyName = "GUI Button")] public GUIButton GUI = new GUIButton(); public class GUIButton { [JsonProperty(PropertyName = "Image")] public string Image = "https://i.imgur.com/CyF0QNV.png"; [JsonProperty(PropertyName = "Background color (RGBA format)")] public string Color = "1 0.96 0.88 0.15"; [JsonProperty(PropertyName = "GUI Button Position")] public Position GUIButtonPosition = new Position(); public class Position { [JsonProperty(PropertyName = "Anchors Min")] public string AnchorsMin = "0.5 0.0"; [JsonProperty(PropertyName = "Anchors Max")] public string AnchorsMax = "0.5 0.0"; [JsonProperty(PropertyName = "Offsets Min")] public string OffsetsMin = "185 18"; [JsonProperty(PropertyName = "Offsets Max")] public string OffsetsMax = "245 78"; } } } #endregion #region Stored Data private class StoredData { public static StoredData Load() { return Interface.Oxide.DataFileSystem.ExistsDatafile(_instance.Name) ? Interface.Oxide.DataFileSystem.ReadObject(_instance.Name) : new StoredData(); } [JsonProperty("PlayersWithDisabledGUI")] public HashSet PlayersWithDisabledGUI = new HashSet(); public void Save() => Interface.Oxide.DataFileSystem.WriteObject(_instance.Name, this); } #endregion #region Backpack private DroppedItemContainer API_DropBackpack(BasePlayer player) { if (!Backpack.HasBackpackFile(player.userID)) return null; var backpack = Backpack.Get(player.userID); backpack.ForceCloseAllLooters(); return DropBackpackWithReducedCorpseCollision(backpack, player.transform.position); } private class Backpack { private bool _initialized = false; private ItemContainer _itemContainer = new ItemContainer(); private List _looters = new List(); [JsonIgnore] public string OwnerIdString { get; private set; } [JsonProperty("OwnerID")] public ulong OwnerId { get; private set; } [JsonProperty("Items")] private List _itemDataCollection = new List(); public Backpack(ulong ownerId) : base() { OwnerId = ownerId; } ~Backpack() { ForceCloseAllLooters(); KillContainer(); } public IPlayer FindOwnerPlayer() => _instance.covalence.Players.FindPlayerById(OwnerIdString); public bool IsUnderlyingContainer(ItemContainer itemContainer) => _itemContainer == itemContainer; public ushort GetAllowedSize() { foreach(var kvp in _instance._backpackSizePermissions) { if (_instance.permission.UserHasPermission(OwnerIdString, kvp.Key)) return kvp.Value; } return _instance._config.BackpackSize; } private int GetAllowedCapacity() => GetAllowedSize() * SlotsPerRow; public void Initialize() { if (!_initialized) { OwnerIdString = OwnerId.ToString(); } else { // Force-close since we are re-initializing ForceCloseAllLooters(); } var ownerPlayer = FindOwnerPlayer()?.Object as BasePlayer; _itemContainer.entityOwner = ownerPlayer; if (_initialized) return; _itemContainer.isServer = true; _itemContainer.allowedContents = ItemContainer.ContentsType.Generic; _itemContainer.GiveUID(); _itemContainer.capacity = GetAllowedCapacity(); if (_itemDataCollection.Count != 0 && _itemDataCollection.Max(item => item.Position) >= _itemContainer.capacity) { // Temporarily increase the capacity to allow all items to fit // Extra items will be addressed when the backpack is opened by the owner // If an admin views the backpack in the meantime, it will appear as max capacity _itemContainer.capacity = MaxSize * SlotsPerRow; } foreach (var backpackItem in _itemDataCollection) { var item = backpackItem.ToItem(); if (item != null) { item.MoveToContainer(_itemContainer, item.position); } } _initialized = true; } public void KillContainer() { _initialized = false; _itemContainer.Kill(); _itemContainer = null; ItemManager.DoRemoves(); } public void Open(BasePlayer looter) { if (_instance._openBackpacks.ContainsKey(looter)) return; _instance._openBackpacks.Add(looter, this); if (!_initialized) { Initialize(); } // The entityOwner may no longer be valid if it was instantiated while the player was offline (due to player death) if (_itemContainer.entityOwner == null) { var ownerPlayer = FindOwnerPlayer()?.Object as BasePlayer; if (ownerPlayer != null) _itemContainer.entityOwner = ownerPlayer; } // Container can't be looted for some reason. // We should cancel here and remove the looter from the open backpacks again. if (looter.inventory.loot.IsLooting() || !(_itemContainer.entityOwner?.CanBeLooted(looter) ?? looter.CanBeLooted(looter))) { _instance._openBackpacks.Remove(looter); return; } if (!_instance.VerifyCanOpenBackpack(looter, OwnerId)) { _instance._openBackpacks.Remove(looter); return; } // Only handle overflow when the owner is opening the backpack if (looter.userID == OwnerId) MaybeAdjustCapacityAndHandleOverflow(looter); if (!_looters.Contains(looter)) _looters.Add(looter); PlayerLootContainer(looter, _itemContainer); Interface.CallHook("OnBackpackOpened", looter, OwnerId, _itemContainer); } private void MaybeAdjustCapacityAndHandleOverflow(BasePlayer receiver) { var allowedCapacity = GetAllowedCapacity(); if (_itemContainer.capacity <= allowedCapacity) { // Increasing or maintaining capacity is always safe to do _itemContainer.capacity = allowedCapacity; return; } // Close for all looters since we are going to alter the capacity ForceCloseAllLooters(); // Item order is preserved so that compaction is more deterministic // Basically, items earlier in the backpack are more likely to stay in the backpack var extraItems = _itemContainer.itemList .OrderBy(item => item.position) .Where(item => item.position >= allowedCapacity) .ToArray(); // Remove the extra items from the container so the capacity can be reduced foreach (var item in extraItems) { item.RemoveFromContainer(); } // Capacity must be reduced before attempting to move overflowing items or they will be placed in the extra slots _itemContainer.capacity = allowedCapacity; var itemsDroppedOrGivenToPlayer = 0; foreach (var item in extraItems) { // Try to move the item to a vacant backpack slot or add to an existing stack in the backpack // If the item cannot be completely compacted into the backpack, the remainder is given to the player // If the item does not completely fit in the player inventory, the remainder is automatically dropped if (!item.MoveToContainer(_itemContainer)) { itemsDroppedOrGivenToPlayer++; receiver.GiveItem(item); } } if (itemsDroppedOrGivenToPlayer > 0) { _instance.PrintToChat(receiver, _instance.lang.GetMessage("Backpack Over Capacity", _instance, receiver.UserIDString)); } } public void ForceCloseAllLooters() { foreach (BasePlayer looter in _looters.ToArray()) { ForceCloseLooter(looter); } } private void ForceCloseLooter(BasePlayer looter) { looter.inventory.loot.Clear(); looter.inventory.loot.MarkDirty(); looter.inventory.loot.SendImmediate(); OnClose(looter); } public void OnClose(BasePlayer looter) { _looters.Remove(looter); _instance._openBackpacks.Remove(looter); Interface.CallHook("OnBackpackClosed", looter, OwnerId, _itemContainer); if (!_instance._config.SaveBackpacksOnServerSave) { SaveData(); } } public DroppedItemContainer Drop(Vector3 position) { object hookResult = Interface.CallHook("CanDropBackpack", OwnerId, position); if (hookResult is bool && (bool)hookResult == false) return null; if (_itemContainer.itemList.Count == 0) return null; BaseEntity entity = GameManager.server.CreateEntity(BackpackPrefab, position, Quaternion.identity); DroppedItemContainer container = entity as DroppedItemContainer; // This needs to be set to "genericlarge" to allow up to 7 rows to be displayed. container.lootPanelName = "genericlarge"; // The player name is being ignore due to the panelName being "genericlarge". // TODO: Try to figure out a way to have 7 rows with custom name. container.playerName = $"{FindOwnerPlayer()?.Name ?? "Somebody"}'s Backpack"; container.playerSteamID = OwnerId; container.inventory = new ItemContainer(); container.inventory.ServerInitialize(null, _itemContainer.itemList.Count); container.inventory.GiveUID(); container.inventory.entityOwner = container; container.inventory.SetFlag(ItemContainer.Flag.NoItemInput, true); foreach (Item item in _itemContainer.itemList.ToArray()) { if (!item.MoveToContainer(container.inventory)) { item.Remove(); item.DoRemove(); } } container.ResetRemovalTime(); container.Spawn(); ItemManager.DoRemoves(); if (!_instance._config.SaveBackpacksOnServerSave) { SaveData(); } return container; } public void EraseContents() { object hookResult = Interface.CallHook("CanEraseBackpack", OwnerId); if (hookResult is bool && (bool)hookResult == false) return; foreach (var item in _itemContainer.itemList.ToList()) { item.Remove(); item.DoRemove(); } ItemManager.DoRemoves(); if (!_instance._config.SaveBackpacksOnServerSave) { SaveData(); } } public void SaveData() { _itemDataCollection = _itemContainer.itemList .Select(ItemData.FromItem) .ToList(); Backpacks.SaveData(this, $"{_instance.Name}/{OwnerId}"); } public int GetItemQuantity(int itemID) => _itemContainer.FindItemsByItemID(itemID).Sum(item => item.amount); public int MoveItemsToPlayerInventory(BasePlayer player, int itemID, int desiredAmount) { List matchingItemStacks = _itemContainer.FindItemsByItemID(itemID); int amountTransferred = 0; foreach (Item itemStack in matchingItemStacks) { int remainingDesiredAmount = desiredAmount - amountTransferred; Item itemToTransfer = (itemStack.amount > remainingDesiredAmount) ? itemStack.SplitItem(remainingDesiredAmount) : itemStack; int initialStackAmount = itemToTransfer.amount; bool transferFullySucceeded = player.inventory.GiveItem(itemToTransfer); amountTransferred += initialStackAmount; if (!transferFullySucceeded) { int amountRemainingInStack = itemToTransfer.amount; // Decrement the amountTransferred by the amount remaining in the stack // Since earlier we incremented it by the full stack amount amountTransferred -= amountRemainingInStack; if (itemToTransfer != itemStack) { // Add the remaining items from the split stack back to the original stack itemStack.amount += amountRemainingInStack; itemStack.MarkDirty(); } break; } if (amountTransferred >= desiredAmount) break; } if (amountTransferred > 0 && !_instance._config.SaveBackpacksOnServerSave) SaveData(); return amountTransferred; } public static bool HasBackpackFile(ulong id) { var fileName = $"{_instance.Name}/{id}"; return Interface.Oxide.DataFileSystem.ExistsDatafile(fileName); } public static Backpack Get(ulong id) { if (id == 0) _instance.PrintWarning("Accessing backpack for ID 0! Please report this to the author with as many details as possible."); if (_instance._backpacks.ContainsKey(id)) return _instance._backpacks[id]; var fileName = $"{_instance.Name}/{id}"; TryMigrateData(fileName); Backpack backpack; if (Interface.Oxide.DataFileSystem.ExistsDatafile(fileName)) { LoadData(out backpack, fileName); } else { backpack = new Backpack(id); Backpacks.SaveData(backpack, fileName); } Interface.Oxide.DataFileSystem.GetDatafile(fileName).Settings = new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore }; _instance._backpacks.Add(id, backpack); backpack.Initialize(); return backpack; } } public class ItemData { public int ID; public int Position = -1; public int Amount; public bool IsBlueprint; public int BlueprintTarget; public ulong Skin; public float Fuel; public int FlameFuel; public float Condition; public float MaxCondition = -1; public int Ammo; public int AmmoType; public int DataInt; public string Name; public string Text; public List Contents = new List(); public Item ToItem() { if (Amount == 0) return null; Item item = ItemManager.CreateByItemID(ID, Amount, Skin); item.position = Position; if (IsBlueprint) { item.blueprintTarget = BlueprintTarget; return item; } item.fuel = Fuel; item.condition = Condition; if (MaxCondition != -1) item.maxCondition = MaxCondition; if (Contents != null) foreach (var contentItem in Contents) contentItem.ToItem().MoveToContainer(item.contents); else item.contents = null; BaseProjectile.Magazine magazine = item.GetHeldEntity()?.GetComponent()?.primaryMagazine; FlameThrower flameThrower = item.GetHeldEntity()?.GetComponent(); if (magazine != null) { magazine.contents = Ammo; magazine.ammoType = ItemManager.FindItemDefinition(AmmoType); } if (flameThrower != null) flameThrower.ammo = FlameFuel; if (DataInt > 0) { item.instanceData = new ProtoBuf.Item.InstanceData { ShouldPool = false, dataInt = DataInt }; } item.text = Text; if (Name != null) item.name = Name; return item; } public static ItemData FromItem(Item item) => new ItemData { ID = item.info.itemid, Position = item.position, Ammo = item.GetHeldEntity()?.GetComponent()?.primaryMagazine?.contents ?? 0, AmmoType = item.GetHeldEntity()?.GetComponent()?.primaryMagazine?.ammoType?.itemid ?? 0, Amount = item.amount, Condition = item.condition, MaxCondition = item.maxCondition, Fuel = item.fuel, Skin = item.skin, Contents = item.contents?.itemList?.Select(FromItem).ToList(), FlameFuel = item.GetHeldEntity()?.GetComponent()?.ammo ?? 0, IsBlueprint = item.IsBlueprint(), BlueprintTarget = item.blueprintTarget, DataInt = item.instanceData?.dataInt ?? 0, Name = item.name, Text = item.text }; } #endregion } }