RustServer/plugins/Backpacks.cs

1358 lines
48 KiB
C#

// #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<ulong, Backpack> _backpacks = new Dictionary<ulong, Backpack>();
private readonly Dictionary<BasePlayer, Backpack> _openBackpacks = new Dictionary<BasePlayer, Backpack>();
private readonly Dictionary<ulong, DroppedItemContainer> _lastDroppedBackpacks = new Dictionary<ulong, DroppedItemContainer>();
private Dictionary<string, ushort> _backpackSizePermissions = new Dictionary<string, ushort>();
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<string> 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<Collider>();
var containerCollider = _lastDroppedBackpacks[player.userID].GetComponent<Collider>();
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<StorageContainer>();
if (storageContainer != null)
{
var data = new Dictionary<string, object>
{
["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<string, object> 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<object>("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<IPlayer>();
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<T>(out T data, string filename = null) =>
data = Interface.Oxide.DataFileSystem.ReadObject<T>(filename ?? _instance.Name);
private static void SaveData<T>(T data, string filename = null) =>
Interface.Oxide.DataFileSystem.WriteObject(filename ?? _instance.Name, data);
#endregion
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["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 <name or id>",
["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 <item short name or id> <amount>",
["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<Configuration>();
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config);
protected override void LoadDefaultConfig()
{
_config = new Configuration
{
BlacklistedItems = new HashSet<string>
{
"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<string> 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<StoredData>(_instance.Name) :
new StoredData();
}
[JsonProperty("PlayersWithDisabledGUI")]
public HashSet<ulong> PlayersWithDisabledGUI = new HashSet<ulong>();
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<BasePlayer> _looters = new List<BasePlayer>();
[JsonIgnore]
public string OwnerIdString { get; private set; }
[JsonProperty("OwnerID")]
public ulong OwnerId { get; private set; }
[JsonProperty("Items")]
private List<ItemData> _itemDataCollection = new List<ItemData>();
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<Item> 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<ItemData> Contents = new List<ItemData>();
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<BaseProjectile>()?.primaryMagazine;
FlameThrower flameThrower = item.GetHeldEntity()?.GetComponent<FlameThrower>();
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<BaseProjectile>()?.primaryMagazine?.contents ?? 0,
AmmoType = item.GetHeldEntity()?.GetComponent<BaseProjectile>()?.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<FlameThrower>()?.ammo ?? 0,
IsBlueprint = item.IsBlueprint(),
BlueprintTarget = item.blueprintTarget,
DataInt = item.instanceData?.dataInt ?? 0,
Name = item.name,
Text = item.text
};
}
#endregion
}
}