using System; using System.Collections.Generic; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries; using UnityEngine; namespace Oxide.Plugins { [Info("ServerTentRewards", "ServerTent", "2.0.0")] [Description("Lets Rust players vote, review, and claim ServerTent rewards.")] public class ServerTentRewards : RustPlugin { private const string PermissionUse = "servertentrewards.use"; private PluginConfig config; private StoredData storedData; private readonly Dictionary lastChecks = new Dictionary(); private readonly HashSet activeRequests = new HashSet(); #region Configuration private class RewardConfig { [JsonProperty("Enabled")] public bool Enabled = true; [JsonProperty("Item shortname")] public string ItemShortname = "scrap"; [JsonProperty("Item amount")] public int ItemAmount = 100; } private class PluginConfig { [JsonProperty("ServerTent rewards API URL")] public string ApiUrl = "https://servertent.net/api/v1/rewards"; [JsonProperty("ServerTent listing ID")] public string ListingId = "YOUR_LISTING_ID"; [JsonProperty("ServerTent listing API key")] public string ApiKey = "YOUR_LISTING_API_KEY"; [JsonProperty("Public listing URL")] public string ListingUrl = "https://servertent.net/listings/view/YOUR_LISTING_SLUG"; [JsonProperty("Require use permission")] public bool RequirePermission = false; [JsonProperty("Check cooldown in seconds")] public float CheckCooldownSeconds = 5f; [JsonProperty("Vote reward")] public RewardConfig VoteReward = new RewardConfig(); [JsonProperty("Review reward (requires a Premium listing)")] public RewardConfig ReviewReward = new RewardConfig { ItemShortname = "scrap", ItemAmount = 200 }; } protected override void LoadDefaultConfig() { config = new PluginConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) throw new JsonException(); } catch { PrintWarning("Invalid configuration detected; creating a fresh configuration."); LoadDefaultConfig(); } config.VoteReward = config.VoteReward ?? new RewardConfig(); config.ReviewReward = config.ReviewReward ?? new RewardConfig(); config.CheckCooldownSeconds = Mathf.Max(1f, config.CheckCooldownSeconds); SaveConfig(); } protected override void SaveConfig() { Config.WriteObject(config, true); } #endregion #region Data private class StoredData { [JsonProperty("Pending claim confirmations")] public HashSet PendingClaims = new HashSet(); } private void LoadData() { try { storedData = Interface.Oxide.DataFileSystem.ReadObject(Name); } catch { PrintWarning("Could not read stored data; starting with an empty claim queue."); storedData = new StoredData(); } if (storedData == null) storedData = new StoredData(); if (storedData.PendingClaims == null) storedData.PendingClaims = new HashSet(); } private void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(Name, storedData); } #endregion #region Hooks and commands private void Init() { permission.RegisterPermission(PermissionUse, this); LoadData(); } private void Unload() { SaveData(); } [ChatCommand("vote")] private void VoteCommand(BasePlayer player, string command, string[] args) { if (!CanUse(player)) return; SendReply(player, $"Vote for this server: {BuildListingUrl(player.UserIDString, false)}"); SendReply(player, "After voting, type /claimvote or /claim."); } [ChatCommand("review")] private void ReviewCommand(BasePlayer player, string command, string[] args) { if (!CanUse(player)) return; SendReply(player, $"Review this server: {BuildListingUrl(player.UserIDString, true)}"); SendReply(player, "After reviewing, type /claimreview or /claim. Review rewards require ServerTent Premium."); } [ChatCommand("claim")] private void ClaimCommand(BasePlayer player, string command, string[] args) { if (!CanUse(player) || !ValidateSteamId(player)) return; CheckReward(player, "vote", config.VoteReward); CheckReward(player, "review", config.ReviewReward); } [ChatCommand("claimvote")] private void ClaimVoteCommand(BasePlayer player, string command, string[] args) { if (!CanUse(player) || !ValidateSteamId(player)) return; CheckReward(player, "vote", config.VoteReward); } [ChatCommand("claimreview")] private void ClaimReviewCommand(BasePlayer player, string command, string[] args) { if (!CanUse(player) || !ValidateSteamId(player)) return; CheckReward(player, "review", config.ReviewReward); } private bool CanUse(BasePlayer player) { if (player == null) return false; if (!config.RequirePermission || permission.UserHasPermission(player.UserIDString, PermissionUse)) return true; SendReply(player, "You do not have permission to use ServerTent rewards."); return false; } private bool ValidateSteamId(BasePlayer player) { ulong steamId; if (ulong.TryParse(player.UserIDString, out steamId) && player.UserIDString.Length == 17) return true; SendReply(player, "Your SteamID64 could not be read."); return false; } #endregion #region ServerTent API private void CheckReward(BasePlayer player, string resource, RewardConfig reward) { if (!reward.Enabled) { SendReply(player, $"{FormatResource(resource)} rewards are disabled on this server."); return; } if (!IsConfigured()) { SendReply(player, "ServerTent rewards have not been configured by an administrator."); return; } var requestKey = GetRequestKey(player.UserIDString, resource); if (activeRequests.Contains(requestKey)) { SendReply(player, $"Your {resource} reward is already being checked."); return; } if (storedData.PendingClaims.Contains(requestKey)) { activeRequests.Add(requestKey); ConfirmClaim(player, resource, reward, requestKey, true, 1); return; } var now = Time.realtimeSinceStartup; float lastCheck; if (lastChecks.TryGetValue(requestKey, out lastCheck) && now - lastCheck < config.CheckCooldownSeconds) { SendReply(player, "Please wait a moment before checking again."); return; } lastChecks[requestKey] = now; activeRequests.Add(requestKey); SendApiRequest(player.UserIDString, resource, "check", string.Empty, (code, body) => { activeRequests.Remove(requestKey); if (!IsPlayerAvailable(player)) return; RewardResponse response; if (!TryReadResponse(code, body, out response)) { ReplyWithApiError(player, response, resource); return; } if (response.Result == 0) { SendReply(player, $"No eligible {resource} was found. Use /{resource}, then try again."); return; } if (response.Result == 2) { SendReply(player, $"Your {resource} reward has already been claimed."); return; } if (response.Result != 1) { SendReply(player, $"ServerTent returned an unknown {resource} reward status."); return; } if (!GrantReward(player, reward)) return; storedData.PendingClaims.Add(requestKey); SaveData(); activeRequests.Add(requestKey); ConfirmClaim(player, resource, reward, requestKey, false, 1); }); } private void ConfirmClaim(BasePlayer player, string resource, RewardConfig reward, string requestKey, bool recovery, int attempt) { var rewardReference = $"rust-{resource}:{reward.ItemShortname}:{reward.ItemAmount}"; SendApiRequest(player.UserIDString, resource, "claim", rewardReference, (code, body) => { RewardResponse response; if (TryReadResponse(code, body, out response) && response.Result == 2) { activeRequests.Remove(requestKey); storedData.PendingClaims.Remove(requestKey); SaveData(); if (IsPlayerAvailable(player)) { SendReply(player, recovery ? $"Your previous {resource} reward claim is now confirmed." : $"{FormatResource(resource)} reward claimed: {reward.ItemAmount} {reward.ItemShortname}."); } return; } if (attempt < 3) { timer.Once(2f * attempt, () => ConfirmClaim(player, resource, reward, requestKey, recovery, attempt + 1)); return; } activeRequests.Remove(requestKey); PrintWarning($"Could not confirm the {resource} claim for SteamID {player.UserIDString}; it remains queued."); if (IsPlayerAvailable(player)) SendReply(player, $"Your {resource} reward was handled, but claim confirmation is still queued. Try again shortly."); }); } private bool GrantReward(BasePlayer player, RewardConfig reward) { if (string.IsNullOrWhiteSpace(reward.ItemShortname) || reward.ItemAmount < 1) { SendReply(player, "The configured reward is invalid. Tell an administrator."); return false; } var item = ItemManager.CreateByName(reward.ItemShortname, reward.ItemAmount); if (item == null) { SendReply(player, "The configured reward item does not exist. Tell an administrator."); return false; } if (!player.GiveItem(item)) item.Drop(player.transform.position + Vector3.up, Vector3.zero); return true; } private void SendApiRequest(string steamId, string resource, string action, string rewardReference, Action callback) { var url = $"{config.ApiUrl.TrimEnd('/')}?server={Uri.EscapeDataString(config.ListingId)}&voter={Uri.EscapeDataString(steamId)}&type={action}&resource={resource}"; if (!string.IsNullOrEmpty(rewardReference)) url += $"&reward={Uri.EscapeDataString(rewardReference)}"; var headers = new Dictionary { ["Authorization"] = $"Bearer {config.ApiKey}", ["Accept"] = "application/json" }; webrequest.Enqueue(url, null, callback, this, RequestMethod.GET, headers, 10f); } private bool TryReadResponse(int code, string body, out RewardResponse response) { response = null; if (string.IsNullOrEmpty(body)) return false; try { response = JsonConvert.DeserializeObject(body); } catch { return false; } return code == 200 && response != null && response.Ok; } private void ReplyWithApiError(BasePlayer player, RewardResponse response, string resource) { if (response != null && !string.IsNullOrWhiteSpace(response.Message)) { SendReply(player, $"ServerTent: {response.Message}"); return; } SendReply(player, $"ServerTent could not check your {resource} reward right now."); } private string BuildListingUrl(string steamId, bool reviews) { var baseUrl = config.ListingUrl.Split('#')[0]; var separator = baseUrl.Contains("?") ? "&" : "?"; return $"{baseUrl}{separator}voter={Uri.EscapeDataString(steamId)}{(reviews ? "#reviews" : string.Empty)}"; } private bool IsConfigured() { return !string.IsNullOrWhiteSpace(config.ApiUrl) && !string.IsNullOrWhiteSpace(config.ListingId) && !string.IsNullOrWhiteSpace(config.ApiKey) && !config.ListingId.StartsWith("YOUR_") && !config.ApiKey.StartsWith("YOUR_"); } private static bool IsPlayerAvailable(BasePlayer player) { return player != null && player.IsConnected; } private static string GetRequestKey(string steamId, string resource) { return $"{steamId}:{resource}"; } private static string FormatResource(string resource) { return char.ToUpperInvariant(resource[0]) + resource.Substring(1); } private class RewardResponse { [JsonProperty("ok")] public bool Ok; [JsonProperty("result")] public int Result; [JsonProperty("message")] public string Message; } #endregion } }