Skip to content
RustPluginGenerator

How to make a Rust daily rewards plugin (Oxide C#)

Adds a /daily command that lets each player claim one reward from a configurable weighted list every 24 hours, with a streak bonus that scales while claims stay within 48 hours of each other. Claims and streaks are persisted to oxide/data with the Oxide data file API.

Servers that want players to log in every day usually script it by hand: a timer that broadcasts a reminder, an admin who pays out in chat, a notepad of who already got their scrap. It does not scale and it is easy to cheat. This plugin gives every player a personal /daily command. Each claim is checked against a 24 hour cooldown stored in oxide/data/DailyRewards.json, so it survives restarts and does not depend on the player being online. Rewards come from the config as a weighted list of item shortnames, amounts and optional skins. Consecutive claims build a streak: the second claim within 48 hours of the last one increases the streak and pays a scaled item bonus, and any gap longer than 48 hours drops the streak back to 1. Staff can inspect and reset a player with a console command.

The plugin code

DailyRewards.cs
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Daily Rewards", "RustPluginGenerator", "1.0.0")]
    [Description("Adds a /daily command with a 24 hour cooldown, weighted rewards and a streak bonus")]
    public class DailyRewards : RustPlugin
    {
        private const string PermAdmin = "dailyrewards.admin";
        private const string DataFileName = "DailyRewards";

        private PluginConfig config;
        private Dictionary<string, PlayerData> data = new Dictionary<string, PlayerData>();

        private class PlayerData
        {
            [JsonProperty("Last claim (UTC)")]
            public DateTime LastClaim { get; set; }

            [JsonProperty("Streak")]
            public int Streak { get; set; }
        }

        private class PluginConfig
        {
            [JsonProperty("Cooldown Hours")]
            public double CooldownHours = 24.0;

            [JsonProperty("Streak Reset Hours")]
            public double StreakResetHours = 48.0;

            [JsonProperty("Streak Bonus Shortname")]
            public string StreakBonusShortname = "scrap";

            [JsonProperty("Streak Bonus Base Amount")]
            public int StreakBonusBaseAmount = 10;

            [JsonProperty("Streak Bonus Per Day")]
            public int StreakBonusPerDay = 5;

            [JsonProperty("Rewards")]
            public List<RewardEntry> Rewards = new List<RewardEntry>
            {
                new RewardEntry { Shortname = "scrap", Amount = 150, Weight = 4 },
                new RewardEntry { Shortname = "metal.fragments", Amount = 500, Weight = 3 },
                new RewardEntry { Shortname = "sulfur.ore", Amount = 200, Weight = 2 },
                new RewardEntry { Shortname = "rifle.ak", Amount = 1, Weight = 1, Skin = 0 }
            };
        }

        private class RewardEntry
        {
            [JsonProperty("Shortname")]
            public string Shortname = "scrap";

            [JsonProperty("Amount")]
            public int Amount = 1;

            [JsonProperty("Skin")]
            public ulong Skin = 0;

            [JsonProperty("Weight")]
            public int Weight = 1;
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["Claimed"] = "You claimed {0} x{1}. Streak: {2} day(s).{3}",
                ["Bonus"] = " Streak bonus: {0} x{1}.",
                ["Cooldown"] = "You already claimed today. Next reward in {0}.",
                ["NoRewards"] = "The reward list is empty. Tell an admin.",
                ["InvalidTarget"] = "No player found for that name or id.",
                ["ResetDone"] = "Rewards reset for {0}.",
                ["FormatHours"] = "{0}h {1}m"
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) =>
            string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, PlayerData>>(DataFileName)
                   ?? new Dictionary<string, PlayerData>();

            var bad = config.Rewards
                .Where(r => r == null || r.Weight <= 0 || ItemManager.FindItemDefinition(r.Shortname) == null)
                .ToList();
            if (config.Rewards.Count == 0)
                PrintWarning("Reward list is empty; /daily will not pay anything.");
            else if (bad.Count > 0)
                PrintWarning($"{bad.Count} reward entries have an unknown shortname or non-positive weight and will be skipped.");
            if (!string.IsNullOrEmpty(config.StreakBonusShortname) &&
                ItemManager.FindItemDefinition(config.StreakBonusShortname) == null)
                PrintWarning($"Streak bonus shortname '{config.StreakBonusShortname}' is unknown; the bonus will not be granted.");
        }

        private void OnServerSave()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
        }

        private void Unload()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
        }

        [ChatCommand("daily")]
        private void CmdDaily(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsConnected) return;

            var id = player.UserIDString;
            if (!data.TryGetValue(id, out var record) || record == null)
            {
                record = new PlayerData();
                data[id] = record;
            }

            var now = DateTime.UtcNow;
            var cooldown = TimeSpan.FromHours(Math.Max(0.0, config.CooldownHours));
            var elapsed = now - record.LastClaim;

            if (record.LastClaim != default(DateTime) && elapsed < cooldown)
            {
                var left = cooldown - elapsed;
                player.ChatMessage(Msg("Cooldown", id, Msg("FormatHours", id, (int)left.TotalHours, left.Minutes)));
                return;
            }

            var streakWindow = TimeSpan.FromHours(Math.Max(0.0, config.StreakResetHours));
            record.Streak = (record.LastClaim != default(DateTime) && elapsed <= streakWindow)
                ? record.Streak + 1
                : 1;

            var reward = PickReward();
            string bonusText = string.Empty;

            if (reward != null)
            {
                var item = ItemManager.CreateByName(reward.Shortname, Math.Max(1, reward.Amount), reward.Skin);
                if (item != null)
                {
                    player.GiveItem(item);
                    player.ChatMessage(Msg("Claimed", id, item.info.displayName.english, item.amount, record.Streak,
                        BuildBonusText(player, record.Streak, id)));
                }
                else
                {
                    player.ChatMessage(Msg("NoRewards", id));
                }
            }
            else
            {
                player.ChatMessage(Msg("NoRewards", id));
            }

            record.LastClaim = now;
        }

        private string BuildBonusText(BasePlayer player, int streak, string id)
        {
            if (string.IsNullOrEmpty(config.StreakBonusShortname)) return string.Empty;

            var definition = ItemManager.FindItemDefinition(config.StreakBonusShortname);
            if (definition == null) return string.Empty;

            var amount = config.StreakBonusBaseAmount + config.StreakBonusPerDay * (streak - 1);
            if (amount <= 0) return string.Empty;

            var bonus = ItemManager.CreateByName(config.StreakBonusShortname, amount);
            if (bonus == null) return string.Empty;

            player.GiveItem(bonus);
            return Msg("Bonus", id, definition.displayName.english, amount);
        }

        private RewardEntry PickReward()
        {
            var entries = config.Rewards
                .Where(r => r != null && r.Weight > 0 && ItemManager.FindItemDefinition(r.Shortname) != null)
                .ToList();
            if (entries.Count == 0) return null;

            var total = entries.Sum(r => r.Weight);
            var roll = UnityEngine.Random.Range(0f, (float)total);
            float running = 0f;
            foreach (var entry in entries)
            {
                running += entry.Weight;
                if (roll < running) return entry;
            }
            return entries[entries.Count - 1];
        }

        [ConsoleCommand("daily.reset")]
        private void CcmdReset(ConsoleSystem.Arg arg)
        {
            var caller = arg.Player();
            if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermAdmin))
            {
                arg.ReplyWith("You do not have permission to use this command.");
                return;
            }

            var target = arg.GetString(0, string.Empty);
            if (string.IsNullOrEmpty(target))
            {
                arg.ReplyWith("Usage: daily.reset <steamid|name>");
                return;
            }

            var player = BasePlayer.FindAwakeOrSleeping(target);
            if (player == null)
            {
                arg.ReplyWith(Msg("InvalidTarget"));
                return;
            }

            data.Remove(player.UserIDString);
            Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
            arg.ReplyWith(Msg("ResetDone", null, player.displayName));
        }
    }
}

Hooks used: OnServerInitialized, OnServerSave

Config file

Created automatically on first load at oxide/config/DailyRewards.json.

DailyRewards.json
{
  "Cooldown Hours": 24,
  "Streak Reset Hours": 48,
  "Streak Bonus Shortname": "scrap",
  "Streak Bonus Base Amount": 10,
  "Streak Bonus Per Day": 5,
  "Rewards": [
    {
      "Shortname": "scrap",
      "Amount": 150,
      "Skin": 0,
      "Weight": 4
    },
    {
      "Shortname": "metal.fragments",
      "Amount": 500,
      "Skin": 0,
      "Weight": 3
    },
    {
      "Shortname": "sulfur.ore",
      "Amount": 200,
      "Skin": 0,
      "Weight": 2
    },
    {
      "Shortname": "rifle.ak",
      "Amount": 1,
      "Skin": 0,
      "Weight": 1
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/dailyClaim today's reward. Refuses with the remaining cooldown if the player already claimed within 24 hours.Everyone
daily.reset <steamid|name>Wipe a player's stored claim time and streak so they can claim again immediately.dailyrewards.admin
Permissions
PermissionGrants
dailyrewards.adminGrants the daily.reset console command.

How the code works

  1. Config shape

    The config class holds the cooldown (Cooldown Hours, default 24), the streak window (Streak Reset Hours, default 48), a Streak Bonus block with an item shortname, a starting amount and an amount added per streak day, plus Rewards, a list of entries with Shortname, Amount, Skin and Weight. LoadDefaultConfig builds the defaults and LoadConfig/SaveConfig handle reading and writing oxide/config/DailyRewards.json. Weights are relative: an entry with weight 3 is three times as likely as one with weight 1.

  2. Persistence

    Claim data lives in a dictionary keyed by UserIDString with the last claim timestamp and the current streak. It is loaded in OnServerInitialized through Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, PlayerData>>(DataFileName) and written back in Unload and in OnServerSave so a crash costs at most one save cycle. Timestamps use DateTime.UtcNow so the 24 hour and 48 hour windows do not depend on the machine timezone.

  3. The /daily command

    [ChatCommand("daily")] runs the whole claim. It resolves the player's record, compares DateTime.UtcNow against LastClaim and the cooldown, and if the player is waiting replies with the formatted remaining time. On a successful claim the plugin picks a reward with a weighted roll, creates the item with ItemManager.CreateByName, gives it via player.GiveItem and bumps the streak. The reply is sent through the lang file so names and numbers are formatted server-side.

  4. Weighted reward selection

    The picker sums the Weight of every entry that has a valid ItemManager.FindItemDefinition result, rolls UnityEngine.Random.Range(0f, total), then walks the list subtracting each weight until the running total passes the roll. Entries with an unknown shortname are skipped and reported once with PrintWarning at startup rather than silently breaking the roll at claim time.

  5. Streaks and the bonus

    If the new claim falls inside Streak Reset Hours of the previous one, the streak increases; otherwise it resets to 1. When Streak Bonus Shortname is set, the plugin grants Base Amount + Per Day * (streak - 1) of that item on top of the rolled reward. The reply reports the streak and the bonus so the player sees why logging in tomorrow matters.

  6. Admin reset command

    [ConsoleCommand("daily.reset")] takes a steam id or a partial name, resolves it with BasePlayer.FindAwakeOrSleeping, and removes the entry from the data dictionary. It works from the server console and RCON, uses arg.ReplyWith for output, and checks permission.UserHasPermission only when the argument comes from a player so console staff are not blocked.

  7. Cleanup and unload

    Unload writes the data file so a reload does not lose claims made since the last save. The plugin registers no timers and draws no CUI, so there is nothing else to tear down; keeping it that way means the reward check only costs a dictionary lookup at command time.

Install it on your server

  1. Save DailyRewards.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/DailyRewards.json (Carbon: carbon/configs/DailyRewards.json). After editing, reload it:oxide.reload DailyRewards c.reload DailyRewards # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default dailyrewards.admin c.grant group default dailyrewards.admin # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Chat reminder for unclaimed rewards

    Nags players who are online but have not claimed yet.

    Generate this version
  • Economics payout instead of items

    Pays the reward as server currency when the plugin is loaded.

    Generate this version
  • Team-wide reward scaling

    Rewards players for claiming with teammates online.

    Generate this version

FAQ

How do I install this on Oxide or Carbon?

Put DailyRewards.cs in oxide/plugins, reload the plugin or restart the server, then edit oxide/config/DailyRewards.json and reload. Carbon loads the same file from the same folder. No extra permissions are needed for /daily; only dailyrewards.admin is registered, for the reset command.

Does the 24 hour cooldown count while the player is offline?

Yes. The cooldown is a timestamp in the data file, not a running timer, so time passes whether the player is on the server or not. A player who claims before logging off can claim again 24 hours later without any admin action.

What happens if a streak bonus item shortname is wrong?

The plugin validates every reward shortname and the bonus shortname at startup and prints a single warning listing the bad entries. Invalid reward entries are skipped in the roll, and a bonus that cannot be created is simply not granted; the base reward still goes through.

Can I give different rewards to VIPs?

The stock plugin uses one list for everyone. Give VIPs a separate permission and gate an extra roll or a multiplier on permission.UserHasPermission(player.UserIDString, "dailyrewards.vip") to layer it on; the data format does not need to change.

Where is claim data stored and can I edit it?

Everything is in oxide/data/DailyRewards.json as a map from steam id to a last-claim UTC timestamp and a streak number. Stop the server before editing it by hand, and prefer daily.reset for single players.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Write a Rust daily rewards Oxide plugin with a /daily command, 24 hour cooldown, a weighted reward list, a streak bonus item scaling per consecutive claim day, 48 hour streak reset, JSON data storage and a daily.reset console command.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize daily rewards

Customize