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
This commit is contained in:
saulteafarmer 2025-04-03 15:30:09 +00:00
parent 29ed11d6c2
commit 8fc360d688

View File

@ -1,289 +1,414 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; // For CultureInfo using System.Globalization;
using System.Linq; using System.IO;
using Newtonsoft.Json; using System.Linq;
using Oxide.Core; using Newtonsoft.Json;
using Oxide.Core.Libraries.Covalence; using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
namespace Oxide.Plugins using UnityEngine;
{
[Info("DepositBox", "rustysats", "0.2.0")] namespace Oxide.Plugins
[Description("Drop box that registers drops for admin while removing items from the game.")] {
internal class DepositBox : RustPlugin [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).")]
private static DepositBox instance; internal class DepositBox : RustPlugin
// Configuration variables {
private int DepositItemID; private static DepositBox instance;
private ulong DepositBoxSkinID;
// Permission constants private int DepositItemID;
private const string permPlace = "depositbox.place"; private ulong DepositBoxSkinID;
private const string permCheck = "depositbox.check"; private string DiscordWebhookUrl;
private const string permAdminCheck = "depositbox.admincheck"; private int ClaimRewardBudget;
private DepositLog depositLog; private bool UseServerInfo;
private Dictionary<Item, BasePlayer> depositTrack = new Dictionary<Item, BasePlayer>(); // Track deposits private int ServerInfoUpdateFreq;
#region Oxide Hooks private const string permPlace = "depositbox.place";
void Init() private const string permCreateClaim = "depositbox.createclaim";
{
instance = this; private Dictionary<string, int> depositLog = new Dictionary<string, int>();
LoadConfiguration();
LoadDepositLog(); private readonly Dictionary<Item, BasePlayer> depositTrack = new Dictionary<Item, BasePlayer>();
permission.RegisterPermission(permPlace, this);
permission.RegisterPermission(permCheck, this); private Timer serverInfoTimer;
permission.RegisterPermission(permAdminCheck, this);
} #region Oxide Hooks
protected override void LoadDefaultConfig() private void Init()
{ {
PrintWarning("Creating a new configuration file."); instance = this;
Config["DepositItemID"] = -1779183908; // Default Item ID for deposits (paper) LoadConfiguration();
Config["DepositBoxSkinID"] = 1641384897; // Default skin ID for the deposit box LoadDepositLog();
SaveConfig(); permission.RegisterPermission(permPlace, this);
} permission.RegisterPermission(permCreateClaim, this);
}
void OnServerInitialized(bool initial)
{ protected override void LoadDefaultConfig()
foreach (var entity in BaseNetworkable.serverEntities) {
{ PrintWarning("Creating a new configuration file.");
if (entity is StorageContainer storageContainer) Config["ClaimRewardBudget"] = "100000";
{ Config["DepositBoxSkinID"] = 3406403145;
OnEntitySpawned(storageContainer); Config["DepositItemID"] = -1779183908;
} Config["DiscordWebhookUrl"] = "";
} Config["UseServerInfo"] = false;
} Config["ServerInfoUpdateFreq"] = 30;
SaveConfig();
void Unload() }
{
foreach (var entity in BaseNetworkable.serverEntities) private void OnServerInitialized(bool initial)
{ {
if (entity is StorageContainer storageContainer && storageContainer.TryGetComponent(out DepositBoxRestriction restriction)) ReinitializeDepositBoxes();
{
restriction.Destroy(); if (UseServerInfo)
} {
} serverInfoTimer = timer.Every(ServerInfoUpdateFreq * 60, UpdateServerInfoLeaderboard);
instance = null; }
} }
void OnEntitySpawned(StorageContainer container) private void Unload()
{ {
if (container == null || container.skinID != DepositBoxSkinID) return; // Early return for non-matching containers if (serverInfoTimer != null)
if (!container.TryGetComponent(out DepositBoxRestriction mono)) {
{ serverInfoTimer.Destroy();
mono = container.gameObject.AddComponent<DepositBoxRestriction>(); serverInfoTimer = null;
mono.container = container.inventory; // Assign inventory upon component addition }
mono.InitDepositBox();
} foreach (var entity in BaseNetworkable.serverEntities)
} {
#endregion if (entity is StorageContainer container)
{
#region Commands var restrictions = container.GetComponents<DepositBoxRestriction>();
[ChatCommand("depositbox")] foreach (var r in restrictions)
private void GiveDepositBox(BasePlayer player, string command, string[] args) {
{ UnityEngine.Object.Destroy(r);
if (!permission.UserHasPermission(player.UserIDString, permPlace)) }
{ }
player.ChatMessage(lang.GetMessage("NoPermission", this, player.UserIDString)); }
return; depositTrack.Clear();
}
player.inventory.containerMain.GiveItem(ItemManager.CreateByItemID(833533164, 1, DepositBoxSkinID)); instance = null;
player.ChatMessage(lang.GetMessage("BoxGiven", this, player.UserIDString)); }
}
private void ReinitializeDepositBoxes()
[ChatCommand("checkdeposits")] {
private void CheckDepositsCommand(BasePlayer player, string command, string[] args) foreach (var entity in BaseNetworkable.serverEntities)
{ {
if (!permission.UserHasPermission(player.UserIDString, permCheck)) if (entity is StorageContainer container && container.skinID == DepositBoxSkinID)
{ {
player.ChatMessage(lang.GetMessage("NoCheckPermission", this, player.UserIDString)); var restrictions = container.GetComponents<DepositBoxRestriction>();
return; foreach (var restriction in restrictions)
} {
UnityEngine.Object.Destroy(restriction);
if (depositLog == null || depositLog.Deposits.Count == 0) }
{ var newRestriction = container.gameObject.AddComponent<DepositBoxRestriction>();
player.ChatMessage(lang.GetMessage("NoDepositData", this, player.UserIDString)); newRestriction.container = container.inventory;
return; newRestriction.InitDepositBox();
} }
}
// Group the deposits by SteamID and calculate the total amount deposited for each player }
var depositSummary = depositLog.Deposits
.GroupBy(entry => entry.SteamId) private void OnEntitySpawned(StorageContainer container)
.Select(group => new {
{ if (container == null || container.skinID != DepositBoxSkinID) return;
SteamId = group.Key,
TotalAmount = group.Sum(entry => entry.AmountDeposited) if (!container.TryGetComponent(out DepositBoxRestriction mono))
}) {
.ToList(); mono = container.gameObject.AddComponent<DepositBoxRestriction>();
mono.container = container.inventory;
// Calculate the total amount deposited by all players mono.InitDepositBox();
int totalDeposited = depositSummary.Sum(summary => summary.TotalAmount); }
}
// Find the current player's total deposits
var playerSummary = depositSummary.FirstOrDefault(summary => summary.SteamId == player.UserIDString); #endregion
if (playerSummary != null) #region Commands
{
// Calculate the percentage of total deposits for the current player [ChatCommand("depositbox")]
double percentageOfTotal = ((double)playerSummary.TotalAmount / totalDeposited) * 100; private void GiveDepositBox(BasePlayer player, string command, string[] args)
player.ChatMessage(lang.GetMessage("PlayerDepositSummary", this, player.UserIDString) {
.Replace("{amount}", playerSummary.TotalAmount.ToString(CultureInfo.InvariantCulture)) if (!permission.UserHasPermission(player.UserIDString, permPlace))
.Replace("{percentage}", percentageOfTotal.ToString("F2", CultureInfo.InvariantCulture))); {
} player.ChatMessage(lang.GetMessage("NoPermission", this, player.UserIDString));
else return;
{ }
player.ChatMessage(lang.GetMessage("NoPlayerDeposits", this, player.UserIDString)); player.inventory.containerMain.GiveItem(ItemManager.CreateByItemID(833533164, 1, DepositBoxSkinID));
} player.ChatMessage(lang.GetMessage("BoxGiven", this, player.UserIDString));
}
// Admin view if player has both permissions
if (permission.UserHasPermission(player.UserIDString, permAdminCheck)) [ChatCommand("createclaim")]
{ private void CreateClaimCommand(BasePlayer player, string command, string[] args)
player.ChatMessage(lang.GetMessage("DepositTotals", this, player.UserIDString)); {
foreach (var summary in depositSummary) if (!permission.UserHasPermission(player.UserIDString, permCreateClaim))
{ {
double percentage = ((double)summary.TotalAmount / totalDeposited) * 100; player.ChatMessage(lang.GetMessage("NoCreateClaimPermission", this, player.UserIDString));
player.ChatMessage(lang.GetMessage("DepositEntrySummary", this, player.UserIDString) return;
.Replace("{steamid}", summary.SteamId) }
.Replace("{amount}", summary.TotalAmount.ToString(CultureInfo.InvariantCulture)) if (depositLog == null || depositLog.Count == 0)
.Replace("{percentage}", percentage.ToString("F2", CultureInfo.InvariantCulture))); {
} player.ChatMessage(lang.GetMessage("NoDepositData", this, player.UserIDString));
} return;
} }
#endregion int totalDeposits = depositLog.Values.Sum();
if (totalDeposits == 0)
#region DepositBoxRestriction Class {
public class DepositBoxRestriction : FacepunchBehaviour player.ChatMessage(lang.GetMessage("ZeroDeposits", this, player.UserIDString));
{ return;
public ItemContainer container; }
public void InitDepositBox() var claimData = new Dictionary<string, int>();
{ foreach (var entry in depositLog)
container.canAcceptItem += CanAcceptItem; {
container.onItemAddedRemoved += OnItemAddedRemoved; double percentage = (double)entry.Value / totalDeposits;
} int reward = (int)Math.Floor(percentage * ClaimRewardBudget);
private bool CanAcceptItem(Item item, int targetPos) claimData[entry.Key] = reward;
{ }
// Only allow the configured deposit item to be deposited Interface.Oxide.DataFileSystem.WriteObject("DepositBox_Claim", claimData);
if (item == null || item.info == null || item.info.itemid != DepositBox.instance.DepositItemID) player.ChatMessage(lang.GetMessage("ClaimFileUpdated", this, player.UserIDString));
{ }
return false;
} #endregion
if (item.GetOwnerPlayer() is BasePlayer player)
{ #region DepositBoxRestriction Class
DepositBox.instance.TrackDeposit(item, player); // Track the item with player reference
} public class DepositBoxRestriction : FacepunchBehaviour
return true; {
} public ItemContainer container;
private void OnItemAddedRemoved(Item item, bool added)
{ public void InitDepositBox()
// Early exit if item isn't added or isn't the correct deposit item {
if (!added || item.info.itemid != DepositBox.instance.DepositItemID) return; container.canAcceptItem += CanAcceptItem;
// Try to get the player who deposited the item container.onItemAddedRemoved += OnItemAddedRemoved;
if (DepositBox.instance.depositTrack.TryGetValue(item, out BasePlayer player)) }
{
DepositBox.instance.LogDeposit(player, item.amount); // Log the deposit first private bool CanAcceptItem(Item item, int targetPos)
DepositBox.instance.depositTrack.Remove(item); // Remove from tracking {
// Now remove the deposited item from the box, after logging is complete if (item == null || item.info == null || item.info.itemid != DepositBox.instance.DepositItemID)
item.Remove(); {
} return false;
} }
public void Destroy() if (item.GetOwnerPlayer() is BasePlayer player)
{ {
container.canAcceptItem -= CanAcceptItem; DepositBox.instance.TrackDeposit(item, player);
container.onItemAddedRemoved -= OnItemAddedRemoved; }
Destroy(this); return true;
} }
}
#endregion private void OnItemAddedRemoved(Item item, bool added)
{
#region Logging if (!added || item.info.itemid != DepositBox.instance.DepositItemID) return;
private class DepositLog if (DepositBox.instance.depositTrack.TryGetValue(item, out BasePlayer player))
{ {
[JsonProperty("deposits")] DepositBox.instance.LogDeposit(player, item.amount);
public List<DepositEntry> Deposits { get; set; } = new List<DepositEntry>(); DepositBox.instance.depositTrack.Remove(item);
} item.Remove();
}
private class DepositEntry }
{
[JsonProperty("steamid")] public void Destroy()
public string SteamId { get; set; } {
[JsonProperty("timestamp")] container.canAcceptItem -= CanAcceptItem;
public string Timestamp { get; set; } container.onItemAddedRemoved -= OnItemAddedRemoved;
[JsonProperty("amount_deposited")] Destroy(this);
public int AmountDeposited { get; set; } }
} }
public void LogDeposit(BasePlayer player, int amount) #endregion
{
// Record this deposit #region Logging and Discord Integration
depositLog.Deposits.Add(new DepositEntry
{ private void LogDeposit(BasePlayer player, int depositAmount)
SteamId = player.UserIDString, {
Timestamp = DateTime.UtcNow.ToString("o"), string steamId = player.UserIDString;
AmountDeposited = amount if (!depositLog.ContainsKey(steamId))
}); {
SaveDepositLog(); // Save the log after recording the deposit depositLog[steamId] = 0;
}
// Calculate the player's total deposits depositLog[steamId] += depositAmount;
int playerTotalDeposits = depositLog.Deposits SaveDepositLog();
.Where(entry => entry.SteamId == player.UserIDString) int playerTotalDeposits = depositLog[steamId];
.Sum(entry => entry.AmountDeposited); int totalDepositedByAllPlayers = depositLog.Values.Sum();
double playerPercentageOfTotal = totalDepositedByAllPlayers > 0 ? (double)playerTotalDeposits / totalDepositedByAllPlayers * 100.0 : 0.0;
// Calculate the total deposits of all players player.ChatMessage(lang.GetMessage("DepositRecorded", this, steamId)
int totalDepositedByAllPlayers = depositLog.Deposits.Sum(entry => entry.AmountDeposited); .Replace("{amount}", depositAmount.ToString(CultureInfo.InvariantCulture))
.Replace("{total_amount}", playerTotalDeposits.ToString(CultureInfo.InvariantCulture))
// Calculate the player's percentage of all deposits .Replace("{percentage}", playerPercentageOfTotal.ToString("F2", CultureInfo.InvariantCulture)));
double playerPercentageOfTotal = ((double)playerTotalDeposits / totalDepositedByAllPlayers) * 100; SendDiscordNotification(player, depositAmount, playerTotalDeposits);
}
// Send the updated deposit message to the player
player.ChatMessage(lang.GetMessage("DepositRecorded", this, player.UserIDString) public void TrackDeposit(Item item, BasePlayer player)
.Replace("{amount}", amount.ToString(CultureInfo.InvariantCulture)) {
.Replace("{total_amount}", playerTotalDeposits.ToString(CultureInfo.InvariantCulture)) if (item != null && player != null)
.Replace("{percentage}", playerPercentageOfTotal.ToString("F2", CultureInfo.InvariantCulture))); {
} depositTrack[item] = player;
}
public void TrackDeposit(Item item, BasePlayer player) }
{
if (item != null && player != null) private void LoadDepositLog()
{ {
depositTrack[item] = player; // Track the item with its owner depositLog = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>("DepositBoxLog")
} ?? new Dictionary<string, int>();
} }
private void LoadDepositLog() private void SaveDepositLog()
{ {
depositLog = Interface.Oxide.DataFileSystem.ReadObject<DepositLog>("DepositBoxLog") ?? new DepositLog(); Interface.Oxide.DataFileSystem.WriteObject("DepositBoxLog", depositLog);
} }
private void SaveDepositLog() private void SendDiscordNotification(BasePlayer player, int depositAmount, int newTotal)
{ {
Interface.Oxide.DataFileSystem.WriteObject("DepositBoxLog", depositLog); if (string.IsNullOrEmpty(DiscordWebhookUrl))
} {
#endregion return;
}
#region Configuration string playerName = !string.IsNullOrEmpty(player.displayName) ? player.displayName : player.UserIDString;
private void LoadConfiguration() string message = $"{playerName} ({player.UserIDString}) deposited {depositAmount} items. Their total is now {newTotal}.";
{ if (string.IsNullOrWhiteSpace(message))
DepositItemID = Convert.ToInt32(Config["DepositItemID"], CultureInfo.InvariantCulture); // Specified CultureInfo {
DepositBoxSkinID = Convert.ToUInt64(Config["DepositBoxSkinID"], CultureInfo.InvariantCulture); // Specified CultureInfo return;
} }
#endregion var payload = new Dictionary<string, string> { ["content"] = message };
string postData = JsonConvert.SerializeObject(payload);
#region Localization var headers = new Dictionary<string, string> { ["Content-Type"] = "application/json" };
protected override void LoadDefaultMessages() webrequest.EnqueuePost(DiscordWebhookUrl, postData, (code, response) =>
{ {
lang.RegisterMessages(new Dictionary<string, string> if (code != 200 && code != 204)
{ {
["NoPermission"] = "You do not have permission to place this box.", PrintWarning($"Discord notification failed: {code} {response}");
["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.", }, this, headers);
["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.", #endregion
["PlayerDepositSummary"] = "You have deposited a total of {amount} items, which is {percentage}% of all deposits.",
["NoPlayerDeposits"] = "You have not made any deposits.", #region ServerInfo Leaderboard Update
["DepositTotals"] = "Deposit Totals:",
["DepositEntrySummary"] = "SteamID: {steamid}, Total Deposited: {amount}, {percentage}% of total." private void UpdateServerInfoLeaderboard()
}, this); {
} string serverInfoPath = GetServerInfoPath();
#endregion 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<Dictionary<string, object>>(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<string> { $"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<object>();
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<string, string>
{
["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
}
}