From 8fc360d688f8888ec4c6fe8ad4c9fd4d57bb11dd Mon Sep 17 00:00:00 2001 From: saulteafarmer Date: Thu, 3 Apr 2025 15:30:09 +0000 Subject: [PATCH] Version 3.0.0 Rewrite of DepositBox to restructure the data storage. - Instead of writing each deposit to storage; plugin now updates each SteamID's total balance. Introduced Discord Webhooks on Deposit - Allows each deposit to be written to a Discord Webhook to store individual deposits (optional config) Removed /checkdeposits in favor of option ServerInfo support - Adds new config to support ServerInfo; creates new Leaderboard tab that tracks player deposits (optional config) Added /createclaim - Creates a json file to be used with Claim-Player-Rewards based on the config amount of rewards for the wipe --- DepositBox.cs | 703 +++++++++++++++++++++++++++++--------------------- 1 file changed, 414 insertions(+), 289 deletions(-) diff --git a/DepositBox.cs b/DepositBox.cs index 0b94659..f7c6d89 100644 --- a/DepositBox.cs +++ b/DepositBox.cs @@ -1,289 +1,414 @@ -using System; -using System.Collections.Generic; -using System.Globalization; // For CultureInfo -using System.Linq; -using Newtonsoft.Json; -using Oxide.Core; -using Oxide.Core.Libraries.Covalence; - -namespace Oxide.Plugins -{ - [Info("DepositBox", "rustysats", "0.2.0")] - [Description("Drop box that registers drops for admin while removing items from the game.")] - internal class DepositBox : RustPlugin - { - private static DepositBox instance; - // Configuration variables - private int DepositItemID; - private ulong DepositBoxSkinID; - // Permission constants - private const string permPlace = "depositbox.place"; - private const string permCheck = "depositbox.check"; - private const string permAdminCheck = "depositbox.admincheck"; - private DepositLog depositLog; - private Dictionary depositTrack = new Dictionary(); // Track deposits - - #region Oxide Hooks - void Init() - { - instance = this; - LoadConfiguration(); - LoadDepositLog(); - permission.RegisterPermission(permPlace, this); - permission.RegisterPermission(permCheck, this); - permission.RegisterPermission(permAdminCheck, this); - } - - protected override void LoadDefaultConfig() - { - PrintWarning("Creating a new configuration file."); - Config["DepositItemID"] = -1779183908; // Default Item ID for deposits (paper) - Config["DepositBoxSkinID"] = 1641384897; // Default skin ID for the deposit box - SaveConfig(); - } - - void OnServerInitialized(bool initial) - { - foreach (var entity in BaseNetworkable.serverEntities) - { - if (entity is StorageContainer storageContainer) - { - OnEntitySpawned(storageContainer); - } - } - } - - void Unload() - { - foreach (var entity in BaseNetworkable.serverEntities) - { - if (entity is StorageContainer storageContainer && storageContainer.TryGetComponent(out DepositBoxRestriction restriction)) - { - restriction.Destroy(); - } - } - instance = null; - } - - void OnEntitySpawned(StorageContainer container) - { - if (container == null || container.skinID != DepositBoxSkinID) return; // Early return for non-matching containers - if (!container.TryGetComponent(out DepositBoxRestriction mono)) - { - mono = container.gameObject.AddComponent(); - mono.container = container.inventory; // Assign inventory upon component addition - mono.InitDepositBox(); - } - } - #endregion - - #region Commands - [ChatCommand("depositbox")] - private void GiveDepositBox(BasePlayer player, string command, string[] args) - { - if (!permission.UserHasPermission(player.UserIDString, permPlace)) - { - player.ChatMessage(lang.GetMessage("NoPermission", this, player.UserIDString)); - return; - } - player.inventory.containerMain.GiveItem(ItemManager.CreateByItemID(833533164, 1, DepositBoxSkinID)); - player.ChatMessage(lang.GetMessage("BoxGiven", this, player.UserIDString)); - } - - [ChatCommand("checkdeposits")] - private void CheckDepositsCommand(BasePlayer player, string command, string[] args) - { - if (!permission.UserHasPermission(player.UserIDString, permCheck)) - { - player.ChatMessage(lang.GetMessage("NoCheckPermission", this, player.UserIDString)); - return; - } - - if (depositLog == null || depositLog.Deposits.Count == 0) - { - player.ChatMessage(lang.GetMessage("NoDepositData", this, player.UserIDString)); - return; - } - - // Group the deposits by SteamID and calculate the total amount deposited for each player - var depositSummary = depositLog.Deposits - .GroupBy(entry => entry.SteamId) - .Select(group => new - { - SteamId = group.Key, - TotalAmount = group.Sum(entry => entry.AmountDeposited) - }) - .ToList(); - - // Calculate the total amount deposited by all players - int totalDeposited = depositSummary.Sum(summary => summary.TotalAmount); - - // Find the current player's total deposits - var playerSummary = depositSummary.FirstOrDefault(summary => summary.SteamId == player.UserIDString); - - if (playerSummary != null) - { - // Calculate the percentage of total deposits for the current player - double percentageOfTotal = ((double)playerSummary.TotalAmount / totalDeposited) * 100; - player.ChatMessage(lang.GetMessage("PlayerDepositSummary", this, player.UserIDString) - .Replace("{amount}", playerSummary.TotalAmount.ToString(CultureInfo.InvariantCulture)) - .Replace("{percentage}", percentageOfTotal.ToString("F2", CultureInfo.InvariantCulture))); - } - else - { - player.ChatMessage(lang.GetMessage("NoPlayerDeposits", this, player.UserIDString)); - } - - // Admin view if player has both permissions - if (permission.UserHasPermission(player.UserIDString, permAdminCheck)) - { - player.ChatMessage(lang.GetMessage("DepositTotals", this, player.UserIDString)); - foreach (var summary in depositSummary) - { - double percentage = ((double)summary.TotalAmount / totalDeposited) * 100; - player.ChatMessage(lang.GetMessage("DepositEntrySummary", this, player.UserIDString) - .Replace("{steamid}", summary.SteamId) - .Replace("{amount}", summary.TotalAmount.ToString(CultureInfo.InvariantCulture)) - .Replace("{percentage}", percentage.ToString("F2", CultureInfo.InvariantCulture))); - } - } - } - #endregion - - #region DepositBoxRestriction Class - public class DepositBoxRestriction : FacepunchBehaviour - { - public ItemContainer container; - public void InitDepositBox() - { - container.canAcceptItem += CanAcceptItem; - container.onItemAddedRemoved += OnItemAddedRemoved; - } - private bool CanAcceptItem(Item item, int targetPos) - { - // Only allow the configured deposit item to be deposited - if (item == null || item.info == null || item.info.itemid != DepositBox.instance.DepositItemID) - { - return false; - } - if (item.GetOwnerPlayer() is BasePlayer player) - { - DepositBox.instance.TrackDeposit(item, player); // Track the item with player reference - } - return true; - } - private void OnItemAddedRemoved(Item item, bool added) - { - // Early exit if item isn't added or isn't the correct deposit item - if (!added || item.info.itemid != DepositBox.instance.DepositItemID) return; - // Try to get the player who deposited the item - if (DepositBox.instance.depositTrack.TryGetValue(item, out BasePlayer player)) - { - DepositBox.instance.LogDeposit(player, item.amount); // Log the deposit first - DepositBox.instance.depositTrack.Remove(item); // Remove from tracking - // Now remove the deposited item from the box, after logging is complete - item.Remove(); - } - } - public void Destroy() - { - container.canAcceptItem -= CanAcceptItem; - container.onItemAddedRemoved -= OnItemAddedRemoved; - Destroy(this); - } - } - #endregion - - #region Logging - private class DepositLog - { - [JsonProperty("deposits")] - public List Deposits { get; set; } = new List(); - } - - private class DepositEntry - { - [JsonProperty("steamid")] - public string SteamId { get; set; } - [JsonProperty("timestamp")] - public string Timestamp { get; set; } - [JsonProperty("amount_deposited")] - public int AmountDeposited { get; set; } - } - - public void LogDeposit(BasePlayer player, int amount) - { - // Record this deposit - depositLog.Deposits.Add(new DepositEntry - { - SteamId = player.UserIDString, - Timestamp = DateTime.UtcNow.ToString("o"), - AmountDeposited = amount - }); - SaveDepositLog(); // Save the log after recording the deposit - - // Calculate the player's total deposits - int playerTotalDeposits = depositLog.Deposits - .Where(entry => entry.SteamId == player.UserIDString) - .Sum(entry => entry.AmountDeposited); - - // Calculate the total deposits of all players - int totalDepositedByAllPlayers = depositLog.Deposits.Sum(entry => entry.AmountDeposited); - - // Calculate the player's percentage of all deposits - double playerPercentageOfTotal = ((double)playerTotalDeposits / totalDepositedByAllPlayers) * 100; - - // Send the updated deposit message to the player - player.ChatMessage(lang.GetMessage("DepositRecorded", this, player.UserIDString) - .Replace("{amount}", amount.ToString(CultureInfo.InvariantCulture)) - .Replace("{total_amount}", playerTotalDeposits.ToString(CultureInfo.InvariantCulture)) - .Replace("{percentage}", playerPercentageOfTotal.ToString("F2", CultureInfo.InvariantCulture))); - } - - public void TrackDeposit(Item item, BasePlayer player) - { - if (item != null && player != null) - { - depositTrack[item] = player; // Track the item with its owner - } - } - - private void LoadDepositLog() - { - depositLog = Interface.Oxide.DataFileSystem.ReadObject("DepositBoxLog") ?? new DepositLog(); - } - - private void SaveDepositLog() - { - Interface.Oxide.DataFileSystem.WriteObject("DepositBoxLog", depositLog); - } - #endregion - - #region Configuration - private void LoadConfiguration() - { - DepositItemID = Convert.ToInt32(Config["DepositItemID"], CultureInfo.InvariantCulture); // Specified CultureInfo - DepositBoxSkinID = Convert.ToUInt64(Config["DepositBoxSkinID"], CultureInfo.InvariantCulture); // Specified CultureInfo - } - #endregion - - #region Localization - protected override void LoadDefaultMessages() - { - lang.RegisterMessages(new Dictionary - { - ["NoPermission"] = "You do not have permission to place this box.", - ["BoxGiven"] = "You have received a Deposit Box.", - ["DepositRecorded"] = "Your deposit of {amount} has been recorded. You have deposited a total of {total_amount} items, which is {percentage}% of all deposits.", - ["PlacedNoPerm"] = "You have placed a deposit box but lack permission to place it.", - ["NoCheckPermission"] = "You do not have permission to check deposits.", - ["NoDepositData"] = "No deposit data found.", - ["PlayerDepositSummary"] = "You have deposited a total of {amount} items, which is {percentage}% of all deposits.", - ["NoPlayerDeposits"] = "You have not made any deposits.", - ["DepositTotals"] = "Deposit Totals:", - ["DepositEntrySummary"] = "SteamID: {steamid}, Total Deposited: {amount}, {percentage}% of total." - }, this); - } - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Oxide.Core; +using Oxide.Core.Libraries.Covalence; +using UnityEngine; + +namespace Oxide.Plugins +{ + [Info("DepositBox", "rustysats", "0.3.0")] + [Description("Drop box that registers deposits for admin while removing items from the game. Now with Discord, claim functionality, and optional ServerInfo leaderboard updates (supports Oxide and Carbon).")] + internal class DepositBox : RustPlugin + { + private static DepositBox instance; + + private int DepositItemID; + private ulong DepositBoxSkinID; + private string DiscordWebhookUrl; + private int ClaimRewardBudget; + private bool UseServerInfo; + private int ServerInfoUpdateFreq; + + private const string permPlace = "depositbox.place"; + private const string permCreateClaim = "depositbox.createclaim"; + + private Dictionary depositLog = new Dictionary(); + + private readonly Dictionary depositTrack = new Dictionary(); + + private Timer serverInfoTimer; + + #region Oxide Hooks + + private void Init() + { + instance = this; + LoadConfiguration(); + LoadDepositLog(); + permission.RegisterPermission(permPlace, this); + permission.RegisterPermission(permCreateClaim, this); + } + + protected override void LoadDefaultConfig() + { + PrintWarning("Creating a new configuration file."); + Config["ClaimRewardBudget"] = "100000"; + Config["DepositBoxSkinID"] = 3406403145; + Config["DepositItemID"] = -1779183908; + Config["DiscordWebhookUrl"] = ""; + Config["UseServerInfo"] = false; + Config["ServerInfoUpdateFreq"] = 30; + SaveConfig(); + } + + private void OnServerInitialized(bool initial) + { + ReinitializeDepositBoxes(); + + if (UseServerInfo) + { + serverInfoTimer = timer.Every(ServerInfoUpdateFreq * 60, UpdateServerInfoLeaderboard); + } + } + + private void Unload() + { + if (serverInfoTimer != null) + { + serverInfoTimer.Destroy(); + serverInfoTimer = null; + } + + foreach (var entity in BaseNetworkable.serverEntities) + { + if (entity is StorageContainer container) + { + var restrictions = container.GetComponents(); + foreach (var r in restrictions) + { + UnityEngine.Object.Destroy(r); + } + } + } + depositTrack.Clear(); + + instance = null; + } + + private void ReinitializeDepositBoxes() + { + foreach (var entity in BaseNetworkable.serverEntities) + { + if (entity is StorageContainer container && container.skinID == DepositBoxSkinID) + { + var restrictions = container.GetComponents(); + foreach (var restriction in restrictions) + { + UnityEngine.Object.Destroy(restriction); + } + var newRestriction = container.gameObject.AddComponent(); + newRestriction.container = container.inventory; + newRestriction.InitDepositBox(); + } + } + } + + private void OnEntitySpawned(StorageContainer container) + { + if (container == null || container.skinID != DepositBoxSkinID) return; + + if (!container.TryGetComponent(out DepositBoxRestriction mono)) + { + mono = container.gameObject.AddComponent(); + mono.container = container.inventory; + mono.InitDepositBox(); + } + } + + #endregion + + #region Commands + + [ChatCommand("depositbox")] + private void GiveDepositBox(BasePlayer player, string command, string[] args) + { + if (!permission.UserHasPermission(player.UserIDString, permPlace)) + { + player.ChatMessage(lang.GetMessage("NoPermission", this, player.UserIDString)); + return; + } + player.inventory.containerMain.GiveItem(ItemManager.CreateByItemID(833533164, 1, DepositBoxSkinID)); + player.ChatMessage(lang.GetMessage("BoxGiven", this, player.UserIDString)); + } + + [ChatCommand("createclaim")] + private void CreateClaimCommand(BasePlayer player, string command, string[] args) + { + if (!permission.UserHasPermission(player.UserIDString, permCreateClaim)) + { + player.ChatMessage(lang.GetMessage("NoCreateClaimPermission", this, player.UserIDString)); + return; + } + if (depositLog == null || depositLog.Count == 0) + { + player.ChatMessage(lang.GetMessage("NoDepositData", this, player.UserIDString)); + return; + } + int totalDeposits = depositLog.Values.Sum(); + if (totalDeposits == 0) + { + player.ChatMessage(lang.GetMessage("ZeroDeposits", this, player.UserIDString)); + return; + } + var claimData = new Dictionary(); + foreach (var entry in depositLog) + { + double percentage = (double)entry.Value / totalDeposits; + int reward = (int)Math.Floor(percentage * ClaimRewardBudget); + claimData[entry.Key] = reward; + } + Interface.Oxide.DataFileSystem.WriteObject("DepositBox_Claim", claimData); + player.ChatMessage(lang.GetMessage("ClaimFileUpdated", this, player.UserIDString)); + } + + #endregion + + #region DepositBoxRestriction Class + + public class DepositBoxRestriction : FacepunchBehaviour + { + public ItemContainer container; + + public void InitDepositBox() + { + container.canAcceptItem += CanAcceptItem; + container.onItemAddedRemoved += OnItemAddedRemoved; + } + + private bool CanAcceptItem(Item item, int targetPos) + { + if (item == null || item.info == null || item.info.itemid != DepositBox.instance.DepositItemID) + { + return false; + } + if (item.GetOwnerPlayer() is BasePlayer player) + { + DepositBox.instance.TrackDeposit(item, player); + } + return true; + } + + private void OnItemAddedRemoved(Item item, bool added) + { + if (!added || item.info.itemid != DepositBox.instance.DepositItemID) return; + if (DepositBox.instance.depositTrack.TryGetValue(item, out BasePlayer player)) + { + DepositBox.instance.LogDeposit(player, item.amount); + DepositBox.instance.depositTrack.Remove(item); + item.Remove(); + } + } + + public void Destroy() + { + container.canAcceptItem -= CanAcceptItem; + container.onItemAddedRemoved -= OnItemAddedRemoved; + Destroy(this); + } + } + + #endregion + + #region Logging and Discord Integration + + private void LogDeposit(BasePlayer player, int depositAmount) + { + string steamId = player.UserIDString; + if (!depositLog.ContainsKey(steamId)) + { + depositLog[steamId] = 0; + } + depositLog[steamId] += depositAmount; + SaveDepositLog(); + int playerTotalDeposits = depositLog[steamId]; + int totalDepositedByAllPlayers = depositLog.Values.Sum(); + double playerPercentageOfTotal = totalDepositedByAllPlayers > 0 ? (double)playerTotalDeposits / totalDepositedByAllPlayers * 100.0 : 0.0; + player.ChatMessage(lang.GetMessage("DepositRecorded", this, steamId) + .Replace("{amount}", depositAmount.ToString(CultureInfo.InvariantCulture)) + .Replace("{total_amount}", playerTotalDeposits.ToString(CultureInfo.InvariantCulture)) + .Replace("{percentage}", playerPercentageOfTotal.ToString("F2", CultureInfo.InvariantCulture))); + SendDiscordNotification(player, depositAmount, playerTotalDeposits); + } + + public void TrackDeposit(Item item, BasePlayer player) + { + if (item != null && player != null) + { + depositTrack[item] = player; + } + } + + private void LoadDepositLog() + { + depositLog = Interface.Oxide.DataFileSystem.ReadObject>("DepositBoxLog") + ?? new Dictionary(); + } + + private void SaveDepositLog() + { + Interface.Oxide.DataFileSystem.WriteObject("DepositBoxLog", depositLog); + } + + private void SendDiscordNotification(BasePlayer player, int depositAmount, int newTotal) + { + if (string.IsNullOrEmpty(DiscordWebhookUrl)) + { + return; + } + string playerName = !string.IsNullOrEmpty(player.displayName) ? player.displayName : player.UserIDString; + string message = $"{playerName} ({player.UserIDString}) deposited {depositAmount} items. Their total is now {newTotal}."; + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + var payload = new Dictionary { ["content"] = message }; + string postData = JsonConvert.SerializeObject(payload); + var headers = new Dictionary { ["Content-Type"] = "application/json" }; + webrequest.EnqueuePost(DiscordWebhookUrl, postData, (code, response) => + { + if (code != 200 && code != 204) + { + PrintWarning($"Discord notification failed: {code} {response}"); + } + }, this, headers); + } + + #endregion + + #region ServerInfo Leaderboard Update + + private void UpdateServerInfoLeaderboard() + { + string serverInfoPath = GetServerInfoPath(); + if (string.IsNullOrEmpty(serverInfoPath)) + { + Puts("ServerInfo.json not found in oxide/config or carbon/configs, skipping update."); + return; + } + try + { + string fileContent = File.ReadAllText(serverInfoPath); + var serverInfo = JsonConvert.DeserializeObject>(fileContent); + if (serverInfo == null || !serverInfo.ContainsKey("settings")) + { + Puts("ServerInfo.json format unexpected, skipping update."); + return; + } + var settings = serverInfo["settings"] as Newtonsoft.Json.Linq.JObject; + if (settings == null) return; + var tabs = settings["Tabs"] as Newtonsoft.Json.Linq.JArray; + if (tabs == null) + { + tabs = new Newtonsoft.Json.Linq.JArray(); + settings["Tabs"] = tabs; + } + var leaderboardLines = new List { $"Leaderboard is updated every {ServerInfoUpdateFreq} minutes.", "" }; + var sorted = depositLog.OrderByDescending(x => x.Value).Take(100).ToList(); + int rank = 1; + foreach (var kv in sorted) + { + string steamId = kv.Key; + int totalDeposited = kv.Value; + string name = covalence.Players.FindPlayerById(steamId)?.Name ?? steamId; + leaderboardLines.Add($"{rank}. {name} - {totalDeposited} items"); + rank++; + } + int linesPerPage = 20; + var pages = new List(); + for (int i = 0; i < leaderboardLines.Count; i += linesPerPage) + { + var chunk = leaderboardLines.Skip(i).Take(linesPerPage).ToList(); + pages.Add(new { TextLines = chunk, ImageSettings = new object[] { } }); + } + var leaderboardTab = new + { + ButtonText = "Leaderboard", + HeaderText = "Top Depositors", + Pages = pages.ToArray(), + TabButtonAnchor = 4, + TabButtonFontSize = 16, + HeaderAnchor = 0, + HeaderFontSize = 32, + TextFontSize = 16, + TextAnchor = 3, + OxideGroup = "" + }; + for (int i = tabs.Count - 1; i >= 0; i--) + { + if (tabs[i]["ButtonText"]?.ToString() == "Leaderboard") + { + tabs.RemoveAt(i); + } + } + tabs.Add(Newtonsoft.Json.Linq.JObject.FromObject(leaderboardTab)); + File.WriteAllText(serverInfoPath, JsonConvert.SerializeObject(serverInfo, Formatting.Indented)); + Puts("ServerInfo.json updated with leaderboard data."); + string reloadCmd = GetServerInfoReloadCommand(serverInfoPath); + if (!string.IsNullOrEmpty(reloadCmd)) + { + ConsoleSystem.Run(ConsoleSystem.Option.Server, reloadCmd); + } + } + catch (Exception ex) + { + PrintWarning($"Error updating ServerInfo: {ex.Message}"); + } + } + + private string GetServerInfoPath() + { + if (File.Exists("oxide/config/ServerInfo.json")) + return "oxide/config/ServerInfo.json"; + if (File.Exists("carbon/configs/ServerInfo.json")) + return "carbon/configs/ServerInfo.json"; + return null; + } + + private string GetServerInfoReloadCommand(string serverInfoPath) + { + if (serverInfoPath.Contains("oxide")) + return "oxide.reload ServerInfo"; + if (serverInfoPath.Contains("carbon")) + return "carbon.reload ServerInfo"; + return ""; + } + + #endregion + + #region Configuration + + private void LoadConfiguration() + { + DepositItemID = Convert.ToInt32(Config["DepositItemID"], CultureInfo.InvariantCulture); + DepositBoxSkinID = Convert.ToUInt64(Config["DepositBoxSkinID"], CultureInfo.InvariantCulture); + DiscordWebhookUrl = Convert.ToString(Config["DiscordWebhookUrl"]); + ClaimRewardBudget = Convert.ToInt32(Config["ClaimRewardBudget"], CultureInfo.InvariantCulture); + UseServerInfo = Convert.ToBoolean(Config["UseServerInfo"]); + ServerInfoUpdateFreq = Convert.ToInt32(Config["ServerInfoUpdateFreq"], CultureInfo.InvariantCulture); + } + + #endregion + + #region Localization + + protected override void LoadDefaultMessages() + { + lang.RegisterMessages(new Dictionary + { + ["NoPermission"] = "You do not have permission to place this box.", + ["BoxGiven"] = "You have received a Deposit Box.", + ["DepositRecorded"] = "Your deposit of {amount} has been recorded. You have deposited a total of {total_amount} items, which is {percentage}% of all deposits.", + ["NoCreateClaimPermission"] = "You do not have permission to create a claim file.", + ["NoDepositData"] = "No deposit data found to create a claim.", + ["ZeroDeposits"] = "Total deposits are zero, cannot create claim.", + ["ClaimFileUpdated"] = "Claim file has been created/updated." + }, this); + } + + #endregion + } +}