C#:
#region Software License
/*
Copyright (c) 2023 Lurch/.lurch. SignalWriter@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion Software License
#region Release notes
// Originally Intended to be used with Metaphysics plugin, but that has gone the way of the Dodo bird
//
//
// VER Date Dev Notes
// ======= ========== =========== ================================================================================================================
//
// 1.0.57 09/22/2023 Lurch Added debugs on all hooks for testing strange problem
//
// 1.0.56 09/22/2023 Lurch Removed '.json' from default data store name (resulted in name.json.json)
// Fixed chat commands -- difference between RustPlugin and CovalencePlugin (ChatCommand -vs- Command)
//
// 1.0.55 09/22/2023 Lurch Fixed io.reload to actually perform a true reload.
//
// Added two new chat commands: (Must have admin perm)
// io.debug which is a Debug mode toggle --> in chat /io.debug will toggle debug on and off
// io.changeowner or io.co which will change EVERY item a specified player owns to another player id.
// Takes TWO arguments - first is the FROM player's Steam ID or name (accepts one or the other)
// Second argument (optional) is to TO player name or Steam ID.
//
// Examples:
//
// io.co 76561197970521596 Thagmeister
// The above command will change everything Lurch owns to Thagmeister
//
// io.co Thagmeister
// The above command will change everything Thatgmeister owns to player id ZERO (no owner)
//
// 1.0.54 08/21/2023 Lurch Cleaned out some old plugin refs (no longer needed) and wrote documentation for public distribution.
//
// 1.0.53 08/08/2023 Lurch Patched the CanEquipItem() hook as it was still using old methods.
//
// 1.0.52 08/02/2023 Lurch Added CanMoveItem() check so that items don't get droppped to ground if not owned
//
// 1.0.50 07/31/2023 Lurch Changed 1.0.47's update to use OnItemPickup hook and added OnItemAddedToContainer hook
//
// 1.0.47 07/30/2023 Lurch Added inventory rejection. An unusual exploit was discovered by @Beee & @Iron - this fix addresses that.
//
// 1.0.46 07/14/2023 Lurch Cleanup for distro to UMOD
//
// 0.0.45 05/02/2023 Beee Converted all Item.UID's to ULONG from UINT per May 4th FP update
//
// 0.0.44 04/06/2023 Lurch Attempt to subdue NRE when CanWearItem() is called with possible null iplayer object
//
// 0.0.43 04/04/2023 Lurch Quickly added user messages for inability to Wear/Equip items - TODO: Make the messages configurable
//
// 0.0.42 03/27/2023 Lurch Removed some debug spam
//
// 0.0.41 03/26/2023 Lurch Attempts to prevent NRE's when XDQuest reloads
//
// 0.0.40 03/23/2023 Lurch Added timer to call TimedEvaluation() every 15 seconds to remove any orphaned items
// caught by watching deleted items in the OnItemRemove() hook.
//
// 0.0.39 03/21/2023 Lurch Fixed NRE if no items in store & trying to add new item
//
// 0.0.38 03/20/2023 Lurch Updated IsItemOwner() to return bool? - null if no registered owner or owner is NPC or not in registry
//
// 0.0.36 03/19/2023 Lurch Added try-catch trap for duplicate ItemUID's (Does Facepunch re-use Itemuid's ?????)
//
// 0.0.35 03/19/2023 Lurch Refactored list storage - stop using Merge() function - now directly to/from persisted storage.
// Too many items being lost somehow, so we don't cache / trust anything in memory.
//
// 0.0.32 03/18/2023 Lurch Fix NRE for IsItemOwner() API call & add [HookMethod()] attribute
//
// 0.0.31 03/18/2023 Lurch Added IsItemOwner() API callable method at request of @beee
// Added if player.IsNpc check before adding item to list
//
// 0.0.30 03/16/2023 Lurch Added bypass permission and checking in both CanWear and CanEquip hooks
//
// 0.0.29 03/16/2023 Lurch Inserted Admin permission - wrapped /ioload with perms.
// Cleaned up some spammy Puts statements.
//
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#endregion Release notes
#region Namespaces
using System;
using Newtonsoft.Json;
using Oxide.Core;
using Facepunch.Extend;
using Oxide.Core.Libraries.Covalence;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Libraries;
#endregion Namespaces
namespace Oxide.Plugins
{
[Info("ItemOwnership", "Lurch", "1.0.57")]
[Description("Prevents special items from being used by other players unless items are in the shareable items list.")]
internal class ItemOwnership : RustPlugin
{
#region Vars
#region Readonly and constants
private const bool en = true; // use of English language
private const string VERSION = "1.0.57";
private const string _permissionAdmin = "itemownership.admin";
private const string _permissionBypass = "itemownership.bypass";
private const ulong _baseSteamID = 76561197960265728L;
private readonly object writeLock = new object(); // v1.0.55
#endregion Readonly and constants
private bool _Debug_ = true;
private bool _lookupInitialized = false;
private Timer _itemReconcilliationTimer;
private List<SpecialItem> _SI;
private Configuration _config;
private List<ulong> _deletedItemIDs;
private Dictionary<ulong, ExtendedItem> itemLookup;
public enum UpdateMode { Add, ChangeOwner, Delete };
#endregion Vars
#region Config
public class Configuration
{
[JsonProperty("VERSION: Plugin Version (Read only)")]
public string Version { get { return VERSION; } }
[JsonProperty("DEBUG: If true, enables debug mode messages to server console window")]
public bool DEBUG = false;
[JsonProperty("Name of the storage file of items used by this plugin")]
public string ItemStoreFilename = "ItemOwnerItemStore";
[JsonProperty("Frequency of check (in secs) to remove deleted items from storage.")]
public int ItemReconcileTimer = 15;
[JsonProperty("List of Rust Item ID's that are shareable.")]
public List<int> ShareableItemIds = new List<int>()
{
1104520648,
1488979457
}; // Chainsaw, Jackhammer
}
protected override void LoadDefaultConfig() => _config = new Configuration();
protected override void LoadConfig()
{
try
{
base.LoadConfig();
_config = Config.ReadObject<Configuration>();
if (null == _config)
throw new JsonException();
_Debug_ = _config.DEBUG;
// Check current Plugin version against stored config version
if (VERSION != _config.Version)
{
// Just re-save config to persist existing & add new fields
PrintWarning("Updating configuration to newer version.");
SaveConfig();
}
}
catch (Exception ex)
{
PrintError($"Problem loading Configuration - creating default config \r\n--> {ex}");
LoadDefaultConfig();
SaveConfig();
}
}
protected override void SaveConfig()
{
if (null == _config)
{
_config = new Configuration();
PrintWarning("No stored configuration found - creating new from defaults.");
}
try
{
Config.WriteObject(_config);
}
catch (Exception ex)
{
PrintError($"ERROR :: Unable to save configuration! \r\n-->{ex}");
}
}
#endregion Config
#region Chat Commands
[ChatCommand("io.ver")] // Users
private void CMDVersion(BasePlayer player, string command, string[] args)
{
//BasePlayer player = iplayer.Object as BasePlayer;
SendMessage(player, $"ItemOwnership version {VERSION} by Lurch.");
}
[ChatCommand("io.reload")] // Admins
private void CMDReload(BasePlayer player, string command, string[] args)
{
if (HasPerm(player.IPlayer, _permissionAdmin))
{
//BasePlayer player = iplayer.Object as BasePlayer;
SendMessage(player, $"Reloading item store");
var tmp = GetAllPersistedSpecialItems("CMDReload()");
int count = 0;
if (null != tmp)
{
_SI = null;
_SI = tmp;
count = tmp.Count;
}
SendMessage(player, $"Reloaded item store: [{count}] item(s) in memory.");
if (count > 0 && _Debug_)
{
foreach (var item in tmp)
SendMessage(player, $"ID:[{item.ItemUID}] Owned by:[{item.OwnerSteamID}]");
}
}
else
SendMessage(player, "No permission.");
}
[ChatCommand("io.changeowner")]
private void CMDChangeOwner(BasePlayer player, string command, string[] args)
{
if (HasPerm(player.IPlayer, _permissionAdmin))
{
int newID = 0;
if(null != args && args.Length > 0)
{
//check if first arg is SteamID
var id = args[0];
ulong toPlayer = _baseSteamID;
ulong fromPlayer = 0UL;
BasePlayer temp = null;
if (id == "!" || id == "0")
fromPlayer = _baseSteamID;
else
{
temp = GetPlayerByNameOrID(id);
fromPlayer = temp.userID;
}
if (fromPlayer > 0UL)
{
if (args.Length > 1)
{
temp = GetPlayerByNameOrID(args[1]);
if (null != temp)
toPlayer = temp.userID;
else
{
SendMessage(player, $"Invalid 'TO' entry - unable to match [{id}] to an existing player.");
return;
}
}
if(null != _SI)
{
int changes = 0;
foreach(var item in _SI)
{
if (item.OwnerSteamID == fromPlayer)
{
item.SetNewOwner(toPlayer);
++changes;
if(_Debug_)
SendMessage(player, $"Item:[{item.Name}] changed to owner:{toPlayer}");
}
}
if (changes > 0)
{
var ss = PersistSpecialItems("CMDChangeOwner()");
if (ss)
{
var newSI = GetAllPersistedSpecialItems("CMDChangeOwner()");
if (newSI != null && newSI.Count > 0)
{
_SI = newSI;
// Invalidate cached lookup structure
itemLookup = null;
InitLookup();
SendMessage(player, $"{(changes > 1 ? $"{changes} Items were" : $"One item was")} changed from owner:[{fromPlayer}] to: [{toPlayer}]");
}
else
SendMessage(player, $"WARNING: Failed to read new ItemOwnerStore.");
}
else
SendMessage(player, $"No changes made - failed to write new ItemOwnerStore.");
}
else
SendMessage(player, $"No items found to change! Player is a pauper!");
}
else
SendMessage(player, $"Store appears to be empty - no changes made.");
}
else
SendMessage(player, $"Invalid 'FROM' entry - unable to match [{id}] to an existing player.");
}
//SystemMessage(iplayer, $"Changing ownershiip of ALL items belonging to player: [{}] to {()}");
}
else
SendMessage(player, "No permission.");
}
[ChatCommand("io.debug")]
private void CMDDebugToggle(BasePlayer player, string command, string[] args)
{
if (HasPerm(player.IPlayer, _permissionAdmin))
{
_Debug_ = !_Debug_;
SendMessage(player, $"{(_Debug_ ? "Debug is now on":"Debug is now off")}");
}
else
SendMessage(player, $"No permission.");
}
[ChatCommand("io.co")]
private void CMDChangeOwnershort(BasePlayer player, string command, string[] args)
{
CMDChangeOwner(player, command, args);
}
[ChatCommand("io.ver")]
private void CMDVersionshort(BasePlayer player, string commnand, string[] args)
{
//CMDVersion(iplayer, commnand, args);
SendMessage(player, $"ItemOwnership version {VERSION} by Lurch.");
}
#endregion Chat Commands
#region OxideHooks
private void Loaded()
{
LoadConfig();
//
// Perms
permission.RegisterPermission(_permissionAdmin, this);
permission.RegisterPermission(_permissionBypass, this);
}
private void Unload()
{
if (null != _itemReconcilliationTimer)
{
_itemReconcilliationTimer.Destroy();
_itemReconcilliationTimer = null;
}
if(null != _SI)
_SI.Clear();
if (null != _deletedItemIDs)
_deletedItemIDs.Clear();
if(null != itemLookup)
itemLookup.Clear();
}
private void OnItemAddedToContainer(ItemContainer container, Item item)
{
if (null == item || null == container) return;
int pos = 1;
try
{
BasePlayer player = container?.playerOwner ?? null;
if (null == player || player.IsNpc) return;
pos = 4;
// If it's not personal inventory we dont care
if (container == player.inventory.containerBelt || container == player.inventory.containerMain || container == player.inventory.containerWear)
{
pos = 6;
if (!HasPerm(player.UserIDString, _permissionBypass))
{
pos = 8;
if (null != itemLookup && _lookupInitialized)
{
pos = 10;
if (itemLookup.ContainsKey(item.uid.Value))
{
pos = 12;
if (!itemLookup[item.uid.Value].IsShareable)
{
pos = 14;
if (itemLookup[item.uid.Value].OwnerSteamID == player.userID)
return;
pos = 16;
SendReply(player, "This item does not belong to you...dropping it!");
pos = 18;
item.Drop(player.GetDropPosition(), player.GetDropVelocity());
pos = 20;
container.MarkDirty();
}
}
}
}
}
}
catch(Exception ex)
{
PrintWarning($"OnItemAddedToContainer()::Error at {pos} -->\r\n{ex}");
}
}
private object OnItemPickup(Item item, BasePlayer player) // 7/31/2023 Update 1.0.50
{
if (null == item || null == player) return null;
int pos = 1;
try
{
if (!HasPerm(player.UserIDString, _permissionBypass))
{
pos = 3;
if (null != itemLookup && _lookupInitialized)
{
pos = 5;
if (itemLookup.ContainsKey(item.uid.Value))
{
pos = 7;
if (!itemLookup[item.uid.Value].IsShareable)
{
pos = 9;
if (itemLookup[item.uid.Value].OwnerSteamID == player.userID)
return null;
pos = 11;
SendReply(player, "This item does not belong to you.");
return false;
}
}
}
}
}
catch(Exception ex)
{
PrintWarning($"OnItemPickup()::Error at {pos} -->\r\n{ex}");
}
return null;
}
private bool CanEquipItem(PlayerInventory inventory, Item item, int targetPos)
{
if (null != inventory && null != item)
{
int pos = 1;
try
{
pos = 3;
BasePlayer player = inventory.containerMain.playerOwner;
if (null == player || player.IsNpc) return true;
pos = 7;
if ((bool)inventory.baseEntity?.IsNpc)
return true;
pos = 9;
if (!HasPerm(player.UserIDString, _permissionBypass))
{
if (null != itemLookup && _lookupInitialized)
{
pos = 11;
if (itemLookup.ContainsKey(item.uid.Value))
{
pos = 13;
if (!itemLookup[item.uid.Value].IsShareable)
{
pos = 15;
if (itemLookup[item.uid.Value].OwnerSteamID == player.userID)
return true;
pos = 17;
//SystemMessage(player.IPlayer, $"This item belongs to someone else.");
SendReply(player, "This item belongs to someone else.");
pos = 19;
item.Drop(player.GetDropPosition(), player.GetDropVelocity());
return false;
}
}
}
}
}
catch (Exception ex)
{
PrintWarning($"CanEquipItem()::Error at {pos} -->\r\n{ex}");
}
}
return true;
}
private object CanWearItem(PlayerInventory inventory, Item item, int targetSlot)
{
if (null != inventory && null != item)
{
if (null != inventory?.baseEntity)
{
var playerId = inventory.baseEntity.userID;
var player = inventory.containerWear.GetOwnerPlayer();
if (0UL != playerId && null != player && !player.IsNpc)
{
if (!HasPerm(player.UserIDString, _permissionBypass))
{
if (null != itemLookup && _lookupInitialized)
{
if (itemLookup.ContainsKey(item.uid.Value))
{
if (!itemLookup[item.uid.Value].IsShareable)
{
if (itemLookup[item.uid.Value].OwnerSteamID == playerId)
return null;
SendReply(player, "This item belongs to someone else.");
return false;
}
}
}
}
}
}
}
return null;
}
private object CanMoveItem(Item item, PlayerInventory playerLoot, ItemContainerId targetContainer, int targetSlot, int amount)
{
// Version 1.0.52
if (null == item || null == playerLoot) return null;
if (0 == item.uid.Value) return null;
var player = playerLoot.baseEntity as BasePlayer;
if (null != player && !player.IsNpc)
{
if (!HasPerm(player.UserIDString, _permissionBypass))
{
if (null != itemLookup && _lookupInitialized)
{
if (itemLookup.ContainsKey(item.uid.Value))
{
if (!itemLookup[item.uid.Value].IsShareable)
{
if (itemLookup[item.uid.Value].OwnerSteamID == player.userID)
return null;
SendReply(player, "This item belongs to someone else.");
return false;
}
}
}
}
}
return null;
}
private void OnServerInitialized()
{
if (_Debug_) Puts("ServerInit");
_SI = GetAllPersistedSpecialItems("ServerInit()");
_lookupInitialized = InitLookup();
//
// Init persisted items
itemLookup = new Dictionary<ulong, ExtendedItem>();
//_SI = new List<SpecialItem>();
//
// Initialize fast lookup dict
_lookupInitialized = InitLookup();
//
// Deleted Item ID watch
_deletedItemIDs = new List<ulong>();
//
// Set Debug
_Debug_ = _config.DEBUG;
// Kick off repeat checks
if(null != _itemReconcilliationTimer)
{
_itemReconcilliationTimer.Destroy();
_itemReconcilliationTimer = null;
}
_itemReconcilliationTimer = timer.Every(_config.ItemReconcileTimer, TimedEvaluations);
}
/// <summary>
/// Called when items are deleted in-world
/// </summary>
/// <param name="item"></param>
private void OnItemRemove(Item item)
{
// We just add the ID's in here and handle
// every _config.ItemReconcileTimer seconds
if (null != item)
{
if(null == _deletedItemIDs)
_deletedItemIDs= new List<ulong>();
_deletedItemIDs.Add(item.uid.Value);
}
}
#endregion OxideHooks
#region Operations
private void TimedEvaluations()
{
int count = 0;
var killSet = new List<ulong>(_deletedItemIDs);
if (null != killSet && killSet.Count > 0)
{
_deletedItemIDs.Clear();
_SI = GetAllPersistedSpecialItems("TimedEvaluations()");
foreach (var killID in killSet)
{
var kill = _SI.Where(x => x.ItemUID == killID).FirstOrDefault();
if (null != kill)
{
_SI.Remove(kill);
++count;
}
}
if (count > 0)
{
PersistSpecialItems("TimedEvaluations()");
if (_Debug_) Puts($"Deleted Item Reconcilliation: [{count}] items were removed from ItemStore.");
}
}
}
private bool PersistSpecialItems(string caller = "Self()")
{
var retval = true;
if (null != _SI && _SI.Count > 0)
{
try
{
lock (writeLock)
{
var store = new ItemStore();
store.InitializeStore(_SI);
SaveData<ItemStore>(store, _config.ItemStoreFilename);
if (_Debug_) Puts($"Successfully stored [{_SI.Count}] items to ItemStore file:[{_config.ItemStoreFilename}]");
}
}
catch (Exception ex)
{
PrintError($"Failed to save ItemStore from caller:{caller} --> {ex}");
retval = false;
}
}
else
{
PrintWarning("SpecialItems store is empty...");
//var delStore = new ItemStore();
//SaveData<ItemStore>(delStore, _config.ItemStoreFilename);
}
return retval;
}
private List<SpecialItem> GetAllPersistedSpecialItems(string caller = "Self()")
{
//if (_Debug_) Puts($"GetAllPersistedSpecialItems() called by {caller}");
try
{
if (null == _config)
LoadConfig();
var storeItems = new ItemStore();
LoadData<ItemStore>(ref storeItems, _config.ItemStoreFilename);
var storedItemData = storeItems.GetPersistedItems();
return (storedItemData != null ? storedItemData : new List<SpecialItem>());
}
catch (Exception ex)
{
PrintError($"Failed to load special items from item store ->{ex}");
}
return null;
}
public void ModifyItemList(BasePlayer player, Item item, UpdateMode mode)
{
if (null != item) // player can be null in some instances like destroy item
{
switch (mode)
{
case UpdateMode.Add:
AddItem(player, item);
break;
case UpdateMode.ChangeOwner:
ChangeOwner(player, item);
break;
case UpdateMode.Delete:
RemoveItem(player, item);
break;
}
}
}
private void AddItem(BasePlayer player, Item item)
{
if (null == player) return;
if (player.IsNpc) return;
if (null == item) return;
try
{
// Check for dupe
if (itemLookup.ContainsKey(item.uid.Value))
return;
var pid = player.userID;
//
// Create new tracking item
var myItem = new SpecialItem();
myItem.ItemUID = item.uid.Value;
myItem.OwnerSteamID = pid;
myItem.CreatorID = pid;
// Check shared items list
myItem.IsShareable = _config.ShareableItemIds.Contains(item.info.itemid);
//
// Read master list from disk & add new item to it
_SI = GetAllPersistedSpecialItems("AddItem()");
_SI.Add(myItem);
//
// Create fast lookup table entry
if (_lookupInitialized)
AddItemToLookup(player, item);
else
_lookupInitialized = InitLookup();
// Write it all back out
var ss = PersistSpecialItems("AddItem()"); // send back to disk
if(_Debug_) Puts($"{(ss ? "Sucessfully" : "WARNING!! unsuccessfully")} stored item:{item.uid.Value} belonging to player:{player.displayName} - {pid}");
}
catch(Exception ex)
{
PrintError($"Adding item error -->\r\n {ex}");
}
}
private void RemoveItem(BasePlayer player, Item item)
{
if (null == item) return;
if (itemLookup.ContainsKey(item.uid.Value))
{
//
// Get master list
_SI = GetAllPersistedSpecialItems("RemoveItem()");
var si = _SI.Where(x => x.ItemUID == item.uid.Value).FirstOrDefault();
if (null != si)
{
_SI.Remove(si);
if(!_lookupInitialized)
_lookupInitialized = InitLookup();
itemLookup.Remove(item.uid.Value);
//
// Save snapshot to disk
PersistSpecialItems("RemoveItem()");
}
}
}
public bool ChangeOwner(BasePlayer player, Item item)
{
if (null == player || null == item) return false;
_SI = GetAllPersistedSpecialItems("ChangeOwner()");
var si = _SI.Where(x => x.ItemUID == item.uid.Value).FirstOrDefault();
if (null != si)
{
si.SetNewOwner(player.userID);
if (!_lookupInitialized)
_lookupInitialized = InitLookup();
if (null != itemLookup && itemLookup.ContainsKey(item.uid.Value))
{
var it = itemLookup[item.uid.Value];
it.OwnerSteamID = player.userID;
}
else
{
PrintWarning($"Missing item:{item.uid.Value} Name:{item.name} from fast lookup - added.");
AddItemToLookup(player, item);
}
//
// Save snapshot to disk
PersistSpecialItems("ChangeOwner()");
return true;
}
else
{
AddItem(player, item);
return true;
}
}
private bool InitLookup()
{
int dupes = 0;
if (null == itemLookup)
itemLookup = new Dictionary<ulong, ExtendedItem>();
if (null == _SI) return false;
foreach (var item in _SI)
{
var extItem = new ExtendedItem();
extItem.IsShareable = item.IsShareable;
extItem.OwnerSteamID = item.OwnerSteamID;
try
{
// Does FP re-use item.uid's between sessions???? (Not yet anyway)
itemLookup.Add(item.ItemUID, extItem);
}
catch
{
++dupes;
//var myItem = itemLookup[item.ItemUID];
//PrintError($"Caught duplicate item key! New:{item.ItemUID}:{item.Name} Owner:{item.OwnerSteamID} Existing item:{item.ItemUID} Owner:{myItem.OwnerSteamID}");
}
}
if (dupes > 0)
PrintWarning($"Caught [{dupes}] duplicate item entries in ItemStore upon init() - these items were ignored.");
return true;
}
private bool AddItemToLookup(BasePlayer player, Item item)
{
if(null == player || null == item) return false;
//
// Create fast lookup table entry
var extItem = new ExtendedItem();
if (null != _config)
{
//
// Check to see if new item is listed as a shareable item
if (_config.ShareableItemIds.Contains(item.info.itemid))
extItem.IsShareable = true;
extItem.OwnerSteamID = player.userID;
//
// Add new item to fast lookup
if (!itemLookup.ContainsKey(item.uid.Value))
{
itemLookup.Add(item.uid.Value, extItem);
return true;
}
else
PrintError($"ItemID:{item.uid.Value} was already in fast lookup table");
}
return false;
}
#endregion Operations
#region PluginAPI
/// <summary>
/// Callable API to add/remove item from list
/// </summary>
/// <param name="player"></param>
/// <param name="item"></param>
/// <param name="action"></param>
[HookMethod("UpdateItemList")]
public void UpdateItemList(BasePlayer player, Item item, int action)
{
var mode = (UpdateMode)action;
ModifyItemList(player, item, mode);
}
/// <summary>
/// Callable API to determine if item is owned by player
/// </summary>
/// <param name="player"></param>
/// <param name="item"></param>
/// <returns>True if item is owned by player, null if no owner/npc owner/item doesnt exist in registry, false otherwise</returns>
[HookMethod("IsItemOwner")]
public bool? IsItemOwner(BasePlayer player, Item item)
{
if(null == itemLookup)
{
GetAllPersistedSpecialItems("API Call to IsItemOwner()");
InitLookup();
}
if (itemLookup.ContainsKey(item.uid.Value))
{
// NPC or no owner registered
if (itemLookup[item.uid.Value].OwnerSteamID < _baseSteamID)
return null;
return itemLookup[item.uid.Value].OwnerSteamID == player.userID;
}
return null;
}
#endregion PluginAPI
#region Utils
private bool HasPerm(string playerID, string perm) => permission.UserHasPermission(playerID, perm);
private bool HasPerm(IPlayer player, string perm) => player.HasPermission(perm);
private static void LoadData<T>(ref T data, string filename = null) =>
data = Interface.Oxide.DataFileSystem.ReadObject<T>(filename ?? nameof(ItemOwnership));
private static void SaveData<T>(T data, string filename = null) =>
Interface.Oxide.DataFileSystem.WriteObject(filename ?? nameof(ItemOwnership), data);
private void SystemMessage(IPlayer player, string textOrLang, params object[] args)
{
if (null != player)
{
if (player.IsConnected)
{
//string message = GetLang(textOrLang, player.Id, args);
//player.Reply(message != textOrLang ? message : textOrLang);
player.Reply(textOrLang);
}
else
PrintWarning($"Player response (offline): {textOrLang}");
}
}
private static BasePlayer GetPlayerBySteamID(ulong steamID)
{
var plyr = BasePlayer.activePlayerList.Where(x => x != null && x.userID == steamID).FirstOrDefault();
return plyr ?? null;
}
private static BasePlayer GetPlayerByName(string name)
{
BasePlayer foundPlayer = null;
foreach (var player in BasePlayer.activePlayerList)
{
if (player.displayName.Equals(name, StringComparison.OrdinalIgnoreCase)
|| player.UserIDString.Contains(name, StringComparison.OrdinalIgnoreCase)
|| player.displayName.Contains(name, StringComparison.OrdinalIgnoreCase))
{
foundPlayer = player;
}
}
return foundPlayer;
}
private static BasePlayer GetPlayerByNameOrID(string nameOrID)
{
BasePlayer myPlayer = null;
if (nameOrID.IsSteamId())
myPlayer = GetPlayerBySteamID(ToSteamID(nameOrID));
else
myPlayer = GetPlayerByName(nameOrID);
return myPlayer;
}
private void SendMessage(BasePlayer player, string msg)
{
SendReply(player, $"{msg}");
}
private static ulong ToSteamID(string id)
{
ulong val = 0UL;
if(ulong.TryParse(id, out val))
{
return val;
}
return 0;
}
#endregion Utils
#region Classes
[Serializable]
public class ItemStore
{
[JsonProperty]
public string Version
{ get; set; } = VERSION;
[JsonProperty]
public bool IsValid
{ get; private set; }
[JsonProperty]
public bool EmptyData
{ get; private set; } = true;
[JsonProperty]
public int RawRecordCount
{ get; private set; }
[JsonProperty]
public int RecCountOnDeserialize
{ get; private set; }
[JsonProperty]
public HashSet<SpecialItem> SpecialItems
{ get; private set; }
public void InitializeStore(List<SpecialItem> specialItems)
{
if (null != specialItems && specialItems.Count > 0)
{
this.RawRecordCount = specialItems.Count;
this.SpecialItems = new HashSet<SpecialItem>(specialItems);
this.EmptyData = (null == this.SpecialItems ? true : false);
this.IsValid = true;
}
}
public List<SpecialItem> GetPersistedItems()
{
if (null != this.SpecialItems)
{
var si = new List<SpecialItem>(SpecialItems);
if(null == si || si.Count < 1)
{
// Try a different deserialization approach.
si = new List<SpecialItem>();
foreach(var record in SpecialItems)
{
var rec = new SpecialItem();
rec.IsShareable = record.IsShareable;
rec.IsValid = record.IsValid;
rec.CreatorID = record.CreatorID;
rec.ItemUID = record.ItemUID;
rec.SkinID = record.SkinID;
rec.RustShortName = record.RustShortName;
rec.Name = record.Name;
rec.OwnerSteamID = record.OwnerSteamID;
rec.AltDeserialize = true;
si.Add(rec);
}
}
this.EmptyData = (null == si ? false : true);
return si;
}
return null;
}
public ItemStore()
{
}
}
[Serializable]
public class SpecialItem
{
private ulong _ownerId = 0UL;
[JsonProperty]
public bool IsValid
{ get; set; }
[JsonProperty]
public ulong OwnerSteamID
{
get { return _ownerId; }
set
{
_ownerId = value;
this.IsValid = true;
}
}
[JsonProperty]
public ulong CreatorID
{ get; set; }
[JsonProperty]
public string Name
{ get; set; }
[JsonProperty]
public string RustShortName
{ get; set; }
[JsonProperty]
public ulong SkinID
{ get; set; } = 0UL;
[JsonProperty]
public ulong ItemUID
{ get; set; } = 0UL;
[JsonProperty]
public bool IsShareable
{ get; set; }
[JsonProperty]
public bool AltDeserialize
{ get; set; } = false;
public SpecialItem()
{
}
public void SetNewOwner(ulong steamID)
{
OwnerSteamID = steamID;
}
}
[Serializable]
public class ExtendedItem
{
public ulong OwnerSteamID
{ get; set; }
public bool IsShareable
{ get; set; }
public ExtendedItem() { }
}
#endregion Classes
}
}