C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
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", "para/umod", "3.8.1")]
[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 = 8;
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 CoffinPrefab = "assets/prefabs/misc/halloween/coffin/coffinstorage.prefab";
private const string BackpackPrefab = "assets/prefabs/misc/item drop/item_drop_backpack.prefab";
private const string ResizableLootPanelName = "generic_resizable";
private readonly BackpackManager _backpackManager = new BackpackManager();
private Dictionary<string, ushort> _backpackSizePermissions = new Dictionary<string, ushort>();
private ProtectionProperties _immortalProtection;
private string _cachedUI;
private static Backpacks _instance;
private Configuration _config;
private StoredData _storedData;
private int _wipeNumber;
[PluginReference]
private Plugin Arena, EventManager;
#endregion
private void CmdMenuHide(IPlayer cov, string command, string[] args) {
var player = cov?.Object as BasePlayer;
if (player == null) return;
_data.ChangeStatus(player);
CreateGUI(player);
}
#region Data
private PluginData _data;
private void SaveData() {
Interface.Oxide.DataFileSystem.WriteObject(Name, _data);
}
private void LoadData() {
try {
_data = Interface.Oxide.DataFileSystem.ReadObject<PluginData>(Name);
}
catch (Exception e) {
PrintError(e.ToString());
}
if (_data == null) _data = new PluginData();
}
private class PluginData {
[JsonProperty(PropertyName = "Hided Players", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<ulong> HidedPlayers = new List<ulong>();
public bool IsHided(BasePlayer player) {
if (player != null) return HidedPlayers.Contains(player.userID);
return false;
}
public bool ChangeStatus(BasePlayer player) {
if (player == null) return false;
if (IsHided(player)) {
HidedPlayers.Remove(player.userID);
return false;
}
HidedPlayers.Add(player.userID);
return true;
}
}
#endregion
#region Hooks
private void Init() {
_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);
_storedData = StoredData.Load();
Unsubscribe(nameof(OnPlayerSleep));
Unsubscribe(nameof(OnPlayerSleepEnded));
}
private void OnServerInitialized() {
_wipeNumber = DetermineWipeNumber();
_immortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
_immortalProtection.name = "BackpacksProtection";
_immortalProtection.Add(1);
LoadData();
foreach (var player in BasePlayer.activePlayerList) CreateGUI(player);
Subscribe(nameof(OnPlayerSleep));
Subscribe(nameof(OnPlayerSleepEnded));
AddCovalenceCommand("hidebp", nameof(CmdMenuHide));
}
private void Unload() {
UnityEngine.Object.Destroy(_immortalProtection);
_storedData.Save();
_backpackManager.SaveAndKillCachedBackpacks();
_backpackManager.CleanupAllNetworkControllers();
foreach (var player in BasePlayer.activePlayerList) DestroyGUI(player);
_instance = null;
SaveData();
}
private void OnNewSave(string filename) {
// Ensure config is loaded
LoadConfig();
if (!_config.ClearBackpacksOnWipe) return;
_backpackManager.ClearCache();
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(ignoreDirty : true);
}
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) {
_backpackManager.SaveAndKillCachedBackpacks();
_backpackManager.CleanupAllNetworkControllers();
_backpackManager.ClearCache();
}
}
private void OnPlayerDisconnected(BasePlayer player) {
var backpack = _backpackManager.GetCachedBackpack(player.userID);
if (backpack == null) return;
backpack.NetworkController?.Unsubscribe(player);
if (!_config.SaveBackpacksOnServerSave) {
backpack.SaveAndKill();
_backpackManager.RemoveFromCache(backpack);
}
}
// Handle player death by normal means.private void OnEntityDeath(BasePlayer player, HitInfo info) =>
OnEntityKill(player);
// Handle player death while sleeping in a safe zone.private void OnEntityKill(BasePlayer player)
{
if (player.IsNpc) return;
DestroyGUI(player);
if (!_backpackManager.HasBackpackFile(player.userID) || permission.UserHasPermission(player.UserIDString, KeepOnDeathPermission)) return;
if (_config.EraseOnDeath) _backpackManager.EraseContents(player.userID);
else if (_config.DropOnDeath) _backpackManager.Drop(player.userID, player.transform.position);
}
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))) {
if (!permission.UserHasPermission(player.Id, GUIPermission)) {
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) && !permission.UserHasPermission(userId, GUIPermission)) DestroyGUI(BasePlayer.Find(userId));
}
private void OnPlayerConnected(BasePlayer player) => CreateGUI(player);
private void OnPlayerRespawned(BasePlayer player) => CreateGUI(player);
private void OnPlayerSleepEnded(BasePlayer player) => CreateGUI(player);
private void OnPlayerSleep(BasePlayer player) => DestroyGUI(player);
private void OnNetworkSubscriptionsUpdate(Network.Networkable networkable, List<Network.Visibility.Group> groupsToAdd, List<Network.Visibility.Group> groupsToRemove) {
if (groupsToRemove == null) return;
for (var i = groupsToRemove.Count - 1; i >= 0; i--) {
var group = groupsToRemove[i];
if (_backpackManager.IsBackpackNetworkGroup(group)) {
// Prevent automatically unsubscribing from backpack network groups.// This allows the subscriptions to persist while players move around.groupsToRemove.Remove(group);
}
}
}
#endregion
#region API
private Dictionary<ulong, ItemContainer> API_GetExistingBackpacks() {
return _backpackManager.GetAllCachedContainers();
}
private void API_EraseBackpack(ulong userId) {
_backpackManager.TryEraseForPlayer(userId);
}
private DroppedItemContainer API_DropBackpack(BasePlayer player) {
var backpack = _backpackManager.GetBackpackIfExists(player.userID);
if (backpack == null) return null;
return _backpackManager.Drop(player.userID, player.transform.position);
}
private ulong API_GetBackpackOwnerId(ItemContainer container) {
return _backpackManager.GetCachedBackpackForContainer(container)?.OwnerId ?? 0;
}
private ItemContainer API_GetBackpackContainer(ulong ownerId) {
return _backpackManager.GetBackpackIfExists(ownerId)?.GetContainer(ensureContainer : true);
}
private int API_GetBackpackItemAmount(ulong ownerId, int itemId) {
return _backpackManager.GetBackpackIfExists(ownerId)?.GetItemQuantity(itemId) ?? 0;
}
#endregion
#region Commands
[ChatCommand("backpack")]
private void OpenBackpackChatCommand(BasePlayer player, string cmd, string[] args) {
if (!player.CanInteract()) return;
if (!permission.UserHasPermission(player.UserIDString, UsagePermission)) {
PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString));
return;
}
player.EndLooting();
// Must delay opening in case the chat is still closing or the loot panel may close instantly.timer.Once(0.5f, () => _backpackManager.OpenBackpack(player.userID, player));
}
[ConsoleCommand("backpack.open")]
private void OpenBackpackConsoleCommand(ConsoleSystem.Arg arg) {
BasePlayer player = arg.Player();
if (player == null || !player.CanInteract()) return;
if (!permission.UserHasPermission(player.UserIDString, UsagePermission)) {
PrintToChat(player, lang.GetMessage("No Permission", this, player.UserIDString));
return;
}
var lootingContainer = player.inventory.loot.containers.FirstOrDefault();
if (lootingContainer != null && _backpackManager.GetCachedBackpackForContainer(lootingContainer) != null) {
player.EndLooting();
// HACK: Send empty respawn information to fully close the player inventory (toggle backpack closed)
player.ClientRPCPlayer(null, player, "OnRespawnInformation");
return;
}
var wasLooting = player.inventory.loot.IsLooting();
if (wasLooting) {
player.EndLooting();
player.inventory.loot.SendImmediate();
}
// Need a short delay when looting so the client doesn't reuse the previously drawn generic_resizable loot panel.var delaySeconds = wasLooting
? 0.1f // Key binds automatically pass the "True" argument at the end.// Can open instantly since not looting and chat is assumed to be closed.: arg.Args?.LastOrDefault() == "True"
? 0f // Not opening via key bind, so the chat window may be open.// Must delay in case the chat is still closing or else the loot panel may close instantly.: 0.1f;
timer.Once(delaySeconds, () => _backpackManager.OpenBackpack(player.userID, player));
}
[ConsoleCommand("backpack.fetch")]
private void FetchBackpackItemConsoleCommand(ConsoleSystem.Arg arg) {
BasePlayer player = arg.Player();
if (player == null || !player.CanInteract()) 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 = _backpackManager.GetBackpack(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);
}
}
[ConsoleCommand("backpack.erase")]
private void EraseBackpackServerCommand(ConsoleSystem.Arg arg) {
BasePlayer player = arg.Player();
if (player != null) {
// Only allowed as a server command.return;
}
var args = arg.Args;
ulong userId;
if (!arg.HasArgs(1) || !ulong.TryParse(args[0], out userId)) {
PrintWarning($"Syntax: {arg.cmd.FullName} <id>");
return;
}
if (_backpackManager.TryEraseForPlayer(userId)) PrintWarning($"Erased backpack for player {userId}.");
else PrintWarning($"Player {userId} has no backpack to erase.");
}
[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 backpackOwnerId = targetBasePlayer?.userID ?? ulong.Parse(targetPlayer.Id);
timer.Once(0.5f, () => _backpackManager.OpenBackpack(backpackOwnerId, 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
private int DetermineWipeNumber() {
var saveName = World.SaveFileName;
var lastDotIndex = saveName.LastIndexOf('.');
var secondToLastDotIndex = saveName.LastIndexOf('.', lastDotIndex - 1);
var wipeNumberString = saveName.Substring(secondToLastDotIndex + 1, lastDotIndex - secondToLastDotIndex - 1);
int wipeNumber;
return int.TryParse(wipeNumberString, out wipeNumber) ? wipeNumber : 0;
}
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) {
// Multiple event/arena plugins define the isEventPlayer method as a standard.var isPlaying = Interface.Call("isEventPlayer", player);
if (isPlaying is bool && (bool)isPlaying) return true;
if (EventManager != null) {
// EventManager 3.x
isPlaying = EventManager.Call("isPlaying", player);
if (isPlaying is bool && (bool)isPlaying) return true;
}
if (Arena != null) {
isPlaying = Arena.Call("IsEventPlayer", player);
if (isPlaying is bool && (bool)isPlaying) return true;
}
return false;
}
private void CreateGUI(BasePlayer player) {
if (player == null || player.IsNpc || !player.IsAlive() || player.IsSleeping()) return;
if (!permission.UserHasPermission(player.UserIDString, GUIPermission)) return;
if (_storedData.PlayersWithDisabledGUI.Contains(player.userID)) return;
var elements = new CuiElementContainer();
if (!_data.IsHided(player)) {
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);
elements.Add(new CuiButton {
Button = {
Command = "hidebp", Color = "1 0.96 0.88 0.15"
}
, RectTransform = {
AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "2 0", OffsetMax = "18 15"
}
, Text = {
Text = "<", Align = TextAnchor.MiddleCenter, FontSize = 12
}
}
, GUIPanelName);
}
else {
var BackpacksUIPanel = elements.Add(new CuiPanel {
Image = {
Color = "0 0 0 0"
}
, 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 CuiButton {
Button = {
Command = "hidebp", Color = "1 0.96 0.88 0.15"
}
, RectTransform = {
AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = "0 0", OffsetMax = "15 15"
}
, Text = {
Text = ">", Align = TextAnchor.MiddleCenter, FontSize = 12
}
}
, GUIPanelName);
}
CuiHelper.DestroyUi(player, GUIPanelName);
CuiHelper.AddUi(player, elements);
}
private void DestroyGUI(BasePlayer player) {
CuiHelper.DestroyUi(player, GUIPanelName);
}
private static void LoadData<T>(out T data, string filename) => data = Interface.Oxide.DataFileSystem.ReadObject<T>(filename);
private static void SaveData<T>(T data, string filename) => Interface.Oxide.DataFileSystem.WriteObject(filename, 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.", ["Blacklisted Items Removed"] = "Your backpack contained blacklisted items.They have been 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-8 Rows)")]
public ushort BackpackSize {
get {
return _backpackSize;
}
set {
_backpackSize = (ushort) Mathf.Clamp(value, MinSize, MaxSize);
}
}
[JsonProperty("Backpack Size (1-7 Rows)")]
public ushort BackpackSizeDeprecated {
set {
BackpackSize = value;
}
}
[JsonProperty("Drop on Death (true/false)")]
public bool DropOnDeath = true;
[JsonProperty("Erase on Death (true/false)")]
public bool EraseOnDeath = 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("Use Blacklist (true/false)")]
public bool UseBlacklist = false;
[JsonProperty("Blacklisted Items (Item Shortnames)")]
public HashSet<string> BlacklistedItems = new HashSet<string>();
[JsonProperty("Use Whitelist (true/false)")]
public bool UseWhitelist = false;
[JsonProperty("Whitelisted Items (Item Shortnames)")]
public HashSet<string> WhitelistedItems = new HashSet<string>();
[JsonProperty("Minimum Despawn Time (Seconds)")]
public float MinimumDespawnTime = 300;
[JsonProperty("GUI Button")]
public GUIButton GUI = new GUIButton();
[JsonProperty("Softcore")]
public SoftcoreOptions Softcore = new SoftcoreOptions();
public class GUIButton {
[JsonProperty(PropertyName = "Image")]
public string Image = "https://i.ibb.co/DhwLtGd/image.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";
}
}
public class SoftcoreOptions {
[JsonProperty("Reclaim Fraction")]
public float ReclaimFraction = 0.5f;
}
[JsonIgnore]
public bool ItemRestrictionEnabled => UseWhitelist || UseBlacklist;
public bool IsRestrictedItem(Item item) {
if (UseWhitelist) return !WhitelistedItems.Contains(item.info.shortname);
if (UseBlacklist) return BlacklistedItems.Contains(item.info.shortname);
return false;
}
}
#endregion
#region Stored Data
private class StoredData {
public static StoredData Load() {
var data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(_instance.Name);
if (data == null) {
_instance.PrintWarning($"Data file {_instance.Name}.json is invalid.Creating new data file.");
data = new StoredData();
data.Save();
}
return data;
}
[JsonProperty("PlayersWithDisabledGUI")]
public HashSet<ulong> PlayersWithDisabledGUI = new HashSet<ulong>();
public void Save() => Interface.Oxide.DataFileSystem.WriteObject(_instance.Name, this);
}
#endregion
#region Backpack Manager
private class BackpackManager {
private const uint StartNetworkGroupId = 10000000;
private uint _nextNetworkGroupId = StartNetworkGroupId;
private readonly Dictionary<ulong, Backpack> _cachedBackpacks = new Dictionary<ulong, Backpack>();
private readonly Dictionary<ItemContainer, Backpack> _backpackContainers = new Dictionary<ItemContainer, Backpack>();
public bool IsBackpackNetworkGroup(Network.Visibility.Group group) {
return group.ID >= StartNetworkGroupId && group.ID < _nextNetworkGroupId;
}
public BackpackNetworkController CreateNetworkController() {
return new BackpackNetworkController(_nextNetworkGroupId++);
}
private static string GetBackpackPath(ulong userId) => $"{_instance.Name}/{userId}";
// 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 == null) return false;
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;
}
public bool HasBackpackFile(ulong userId) {
return Interface.Oxide.DataFileSystem.ExistsDatafile(GetBackpackPath(userId));
}
public Backpack GetCachedBackpack(ulong userId) {
Backpack backpack;
return _cachedBackpacks.TryGetValue(userId, out backpack) ? backpack : null;
}
public Backpack GetBackpack(ulong userId) {
var backpack = GetCachedBackpack(userId);
if (backpack != null) return backpack;
return Load(userId);
}
public Backpack GetBackpackIfExists(ulong userId) {
return HasBackpackFile(userId) ? GetBackpack(userId) : null;
}
public void RegisterContainer(ItemContainer container, Backpack backpack) {
_backpackContainers[container] = backpack;
}
public void UnregisterContainer(ItemContainer container) {
_backpackContainers.Remove(container);
}
public Backpack GetCachedBackpackForContainer(ItemContainer container) {
Backpack backpack;
return _backpackContainers.TryGetValue(container, out backpack) ? backpack : null;
}
public Dictionary<ulong, ItemContainer> GetAllCachedContainers() {
var cachedContainersByUserId = new Dictionary<ulong, ItemContainer>();
foreach (var entry in _cachedBackpacks) {
var container = entry.Value.GetContainer();
if (container != null) cachedContainersByUserId[entry.Key] = container;
}
return cachedContainersByUserId;
}
public void EraseContents(ulong userId) {
GetBackpackIfExists(userId)?.EraseContents();
}
public DroppedItemContainer Drop(ulong userId, Vector3 position) {
return GetBackpackIfExists(userId)?.Drop(position);
}
public void OpenBackpack(ulong backpackOwnerId, BasePlayer looter) {
GetBackpack(backpackOwnerId).Open(looter);
}
private Backpack Load(ulong userId) {
var fileName = GetBackpackPath(userId);
TryMigrateData(fileName);
Backpack backpack = null;
if (HasBackpackFile(userId)) {
LoadData(out backpack, fileName);
}
// Note: Even if the user has a backpack file, the file contents may be null in some edge cases.if (backpack == null)
{
backpack = new Backpack(userId);
}
// Ensure the backpack always has an owner id.// This improves compatibility with plugins such as Wipe Data Cleaner which reset the file to `{}`.backpack.OwnerId = userId;
// Forget about associated entities from previous wipes.if (backpack.WipeNumber != _instance._wipeNumber)
{
foreach (var itemData in backpack.ItemDataCollection) {
itemData.AssociatedEntityId = 0;
}
}
backpack.WipeNumber = _instance._wipeNumber;
_cachedBackpacks[userId] = backpack;
return backpack;
}
public bool TryEraseForPlayer(ulong userId) {
var backpack = GetBackpackIfExists(userId);
if (backpack == null) return false;
backpack.EraseContents(force : true);
return true;
}
public void SaveAndKillCachedBackpacks() {
foreach (var backpack in _cachedBackpacks.Values) {
backpack.SaveAndKill();
}
}
public void RemoveFromCache(Backpack backpack) {
_cachedBackpacks.Remove(backpack.OwnerId);
}
public void ClearCache() {
_cachedBackpacks.Clear();
}
public void CleanupAllNetworkControllers() {
foreach (var backpack in _cachedBackpacks.Values) {
backpack.NetworkController?.UnsubscribeAll();
}
_nextNetworkGroupId = StartNetworkGroupId;
}
}
#endregion
#region Backpack Networking
private class BackpackNetworkController {
public Network.Visibility.Group NetworkGroup {
get;
private set;
}
private List<BasePlayer> _subscribers = new List<BasePlayer>(1);
public BackpackNetworkController(uint networkGroupId) {
NetworkGroup = new Network.Visibility.Group(null, networkGroupId);
}
public void Subscribe(BasePlayer player) {
if (player.Connection == null || _subscribers.Contains(player)) return;
_subscribers.Add(player);
// Send the client a message letting them know they are subscribed to the group.ServerMgr.OnEnterVisibility(player.Connection, NetworkGroup);
// Send the client a snapshot of every entity currently in the group.// Don't use the entity queue for this because it could be cleared which could cause updates to be missed.foreach (var networkable in NetworkGroup.networkables)
{
(networkable.handler as BaseNetworkable).SendAsSnapshot(player.Connection);
}
if (!NetworkGroup.subscribers.Contains(player.Connection)) {
// Register the client with the group so that entities added to it will be automatically sent to the client.NetworkGroup.subscribers.Add(player.Connection);
}
var subscriber = player.net.subscriber;
if (!subscriber.subscribed.Contains(NetworkGroup)) {
// Register the group with the client so that ShouldNetworkTo() returns true in SendNetworkUpdate().// This covers cases such as toggling a pager's silent mode.subscriber.subscribed.Add(NetworkGroup);
}
}
public void Unsubscribe(BasePlayer player) {
if (!_subscribers.Remove(player)) return;
if (player.Connection == null) return;
// Unregister the client from the group so they don't get future entity updates.NetworkGroup.subscribers.Remove(player.Connection);
player.net.subscriber.subscribed.Remove(NetworkGroup);
// Send the client a message so they kill all client-side entities in the group.ServerMgr.OnLeaveVisibility(player.Connection, NetworkGroup);
}
public void UnsubscribeAll() {
for (var i = _subscribers.Count - 1; i >= 0; i--) {
Unsubscribe(_subscribers[i]);
}
}
}
#endregion
#region Backpack
private class NoRagdollCollision : FacepunchBehaviour {
private Collider _collider;
private void Awake() {
_collider = GetComponent<Collider>();
}
private void OnCollisionEnter(Collision collision) {
if (collision.collider.IsOnLayer(Rust.Layer.Ragdoll)) {
Physics.IgnoreCollision(_collider, collision.collider);
}
}
}
private class BackpackCloseListener : EntityComponent<StorageContainer> {
private Backpack _backpack;
public void SetBackpack(Backpack backpack) {
_backpack = backpack;
}
// Called via `entity.SendMessage("PlayerStoppedLooting", player)` in PlayerLoot.Clear().private void PlayerStoppedLooting(BasePlayer looter)
{
_instance?.TrackStart();
_backpack.OnClosed(looter);
Interface.CallHook("OnBackpackClosed", looter, _backpack.OwnerId, _backpack.GetContainer());
if (_instance != null && !_instance._config.SaveBackpacksOnServerSave) {
_backpack.SaveData();
}
_instance?.TrackEnd();
}
}
private class Backpack {
private StorageContainer _storageContainer;
private ItemContainer _itemContainer;
private List<BasePlayer> _looters = new List<BasePlayer>();
private bool _processedRestrictedItems;
private bool _dirty = false;
[JsonIgnore]
public BackpackNetworkController NetworkController {
get;
private set;
}
[JsonIgnore]
public string OwnerIdString {
get;
private set;
}
[JsonProperty("OwnerID")]
public ulong OwnerId {
get;
set;
}
[JsonProperty("WipeNumber", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int WipeNumber;
[JsonProperty("Items")]
public List<ItemData> ItemDataCollection = new List<ItemData>();
public Backpack(ulong ownerId) : base() {
OwnerId = ownerId;
OwnerIdString = OwnerId.ToString();
}
~Backpack() {
KillContainer();
NetworkController?.UnsubscribeAll();
}
public IPlayer FindOwnerPlayer() => _instance.covalence.Players.FindPlayerById(OwnerIdString);
public ushort GetAllowedSize() {
foreach(var kvp in _instance._backpackSizePermissions) {
if (_instance.permission.UserHasPermission(OwnerIdString, kvp.Key)) return kvp.Value;
}
return _instance._config.BackpackSize;
}
public ItemContainer GetContainer(bool ensureContainer = false) {
if (ensureContainer) EnsureContainer();
return _itemContainer;
}
public void Open(BasePlayer looter) {
if (!_instance.VerifyCanOpenBackpack(looter, OwnerId)) return;
EnsureContainer();
NetworkController.Subscribe(looter);
// Only drop items when the owner is opening the backpack.if (looter.userID == OwnerId)
{
MaybeRemoveRestrictedItems(looter);
MaybeAdjustCapacityAndHandleOverflow(looter);
}
if (!_looters.Contains(looter)) _looters.Add(looter);
_storageContainer.PlayerOpenLoot(looter, _storageContainer.panelName, doPositionChecks : false);
Interface.CallHook("OnBackpackOpened", looter, OwnerId, _storageContainer.inventory);
}
public void OnClosed(BasePlayer looter) {
_looters.Remove(looter);
// Clean up the subscription immediately if admin stopped looting.// This avoids having to clean up the admin subscriptions some other way which would add complexity.if (looter.userID != OwnerId)
{
NetworkController?.Unsubscribe(looter);
}
}
public DroppedItemContainer Drop(Vector3 position) {
// Optimization: If no container and no stored data, don't bother with the rest of the logic.if (_storageContainer == null && ItemDataCollection.Count == 0)
return null;
object hookResult = Interface.CallHook("CanDropBackpack", OwnerId, position);
if (hookResult is bool && (bool)hookResult == false) return null;
EnsureContainer();
ForceCloseAllLooters();
ReclaimItemsForSoftcore();
// Check again since the items may have all been reclaimed for Softcore.if (_itemContainer.itemList.Count == 0)
return null;
DroppedItemContainer container = GameManager.server.CreateEntity(BackpackPrefab, position, Quaternion.identity) as DroppedItemContainer;
container.gameObject.AddComponent<NoRagdollCollision>();
container.lootPanelName = ResizableLootPanelName;
// ... [Code preview truncated due to length. Full source in attached file] ...