Skip to content
RustPluginGenerator
Compiled · Rust 2635.289.1 · Oxide 2.0.7839Oxide & Carbonquality of life

Playtime Rewards: Item Payouts for Time Online

PlaytimeRewards tracks how long each player stays connected and pays an item reward at every set interval of online time. Players can check their total playtime and minutes until the next reward with /playtime.

The plugin

PlaytimeRewards.cs
// Generated with RustPluginGenerator.com
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Playtime Rewards", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks total online time, pays a reward for every interval spent online and shows playtime with /playtime.")]
    public class PlaytimeRewards : RustPlugin
    {
        private const string PermUse = "playtimerewards.use";
        private const float TickSeconds = 60f;

        private Configuration config;
        private StoredData data;
        private Timer tickTimer;
        private bool rewardItemValid;

        // Last realtime (seconds since startup) at which each online player's playtime was accumulated.
        private readonly Dictionary<ulong, double> lastUpdate = new Dictionary<ulong, double>();

        #region Configuration

        private class Configuration
        {
            [JsonProperty("Reward item shortname")]
            public string RewardItemShortname = "scrap";

            [JsonProperty("Reward amount per interval")]
            public int RewardAmountPerHour = 100;

            [JsonProperty("Reward interval (minutes online)")]
            public int RewardIntervalMinutes = 60;

            [JsonProperty("Require playtimerewards.use permission to receive rewards")]
            public bool RequirePermission = false;

            [JsonProperty("Notify player when rewarded")]
            public bool NotifyOnReward = true;
        }

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

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

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

        #endregion

        #region Data

        private class PlayerRecord
        {
            [JsonProperty("Total seconds online")]
            public double TotalSeconds;

            [JsonProperty("Seconds toward next reward")]
            public double RewardProgressSeconds;
        }

        private class StoredData
        {
            [JsonProperty("Players")]
            public Dictionary<string, PlayerRecord> Players = new Dictionary<string, PlayerRecord>();
        }

        private void LoadData()
        {
            try
            {
                data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
            }
            catch
            {
                data = null;
            }

            if (data == null) data = new StoredData();
            if (data.Players == null) data.Players = new Dictionary<string, PlayerRecord>();
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, data);

        private PlayerRecord GetRecord(string userId)
        {
            PlayerRecord record;
            if (!data.Players.TryGetValue(userId, out record))
            {
                record = new PlayerRecord();
                data.Players[userId] = record;
            }
            return record;
        }

        #endregion

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["NoPermission"] = "You don't have permission to use this command.",
                ["Playtime"] = "You have played <color=#ffb000>{0}</color> hour(s) and <color=#ffb000>{1}</color> minute(s) in total.",
                ["NextReward"] = "Next reward in <color=#ffb000>{0}</color> minute(s).",
                ["Rewarded"] = "You received <color=#ffb000>{0}x {1}</color> for playing on the server. Thanks for staying!"
            }, this);
        }

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

        #endregion

        #region Lifecycle

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

        private void OnServerInitialized()
        {
            if (config.RewardIntervalMinutes < 1)
            {
                PrintWarning("Reward interval must be at least 1 minute, using 60");
                config.RewardIntervalMinutes = 60;
            }

            rewardItemValid = ItemManager.FindItemDefinition(config.RewardItemShortname) != null;
            if (!rewardItemValid)
                PrintWarning($"Reward item '{config.RewardItemShortname}' does not exist, rewards are disabled");

            foreach (BasePlayer player in BasePlayer.activePlayerList)
                StartSession(player);

            tickTimer = timer.Every(TickSeconds, Tick);
        }

        private void Unload()
        {
            tickTimer?.Destroy();
            tickTimer = null;

            foreach (BasePlayer player in BasePlayer.activePlayerList)
                Accumulate(player);

            lastUpdate.Clear();
            SaveData();
        }

        #endregion

        #region Hooks

        private void OnPlayerConnected(BasePlayer player) => StartSession(player);

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null || player.IsNpc) return;

            Accumulate(player);
            lastUpdate.Remove(player.userID.Get());
        }

        private void OnServerSave()
        {
            foreach (BasePlayer player in BasePlayer.activePlayerList)
                Accumulate(player);

            SaveData();
        }

        #endregion

        #region Tracking

        private void StartSession(BasePlayer player)
        {
            if (player == null || player.IsNpc || !player.userID.Get().IsSteamId()) return;

            GetRecord(player.UserIDString);
            lastUpdate[player.userID.Get()] = Time.realtimeSinceStartup;
        }

        private bool CanReceiveReward(BasePlayer player) =>
            rewardItemValid && (!config.RequirePermission || permission.UserHasPermission(player.UserIDString, PermUse));

        // Adds time elapsed since the last update to the player's totals and reward progress.
        private void Accumulate(BasePlayer player)
        {
            if (player == null || player.IsNpc) return;

            ulong id = player.userID.Get();
            double last;
            if (!lastUpdate.TryGetValue(id, out last)) return;

            double now = Time.realtimeSinceStartup;
            double elapsed = now - last;
            lastUpdate[id] = now;
            if (elapsed <= 0) return;

            PlayerRecord record = GetRecord(player.UserIDString);
            record.TotalSeconds += elapsed;

            // Progress only builds while eligible, so granting permission later does not pay out a backlog.
            if (CanReceiveReward(player))
                record.RewardProgressSeconds += elapsed;
        }

        private void Tick()
        {
            double intervalSeconds = config.RewardIntervalMinutes * 60.0;

            foreach (BasePlayer player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected || player.IsNpc) continue;

                Accumulate(player);

                PlayerRecord record;
                if (!data.Players.TryGetValue(player.UserIDString, out record)) continue;

                while (record.RewardProgressSeconds >= intervalSeconds)
                {
                    record.RewardProgressSeconds -= intervalSeconds;
                    GiveReward(player);
                }
            }
        }

        private void GiveReward(BasePlayer player)
        {
            if (config.RewardAmountPerHour <= 0) return;

            Item item = ItemManager.CreateByName(config.RewardItemShortname, config.RewardAmountPerHour);
            if (item == null) return;

            string itemName = item.info.displayName.english;

            if (!player.inventory.GiveItem(item))
                item.Drop(player.GetDropPosition(), player.GetDropVelocity());

            if (config.NotifyOnReward)
                player.ChatMessage(Lang("Rewarded", player.UserIDString, config.RewardAmountPerHour, itemName));
        }

        #endregion

        #region Commands

        [ChatCommand("playtime")]
        private void CmdPlaytime(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;

            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Lang("NoPermission", player.UserIDString));
                return;
            }

            Accumulate(player);
            PlayerRecord record = GetRecord(player.UserIDString);

            long totalMinutes = (long)(record.TotalSeconds / 60.0);
            player.ChatMessage(Lang("Playtime", player.UserIDString, totalMinutes / 60, totalMinutes % 60));

            if (CanReceiveReward(player))
            {
                double remaining = config.RewardIntervalMinutes * 60.0 - record.RewardProgressSeconds;
                int minutesLeft = Mathf.Max(0, Mathf.CeilToInt((float)(remaining / 60.0)));
                player.ChatMessage(Lang("NextReward", player.UserIDString, minutesLeft));
            }
        }

        #endregion
    }
}

What it does

PlaytimeRewards counts how long each player stays connected and gives them an item each time they reach a set amount of online time. Players can type /playtime to see their total hours and minutes played and, when rewards apply to them, how many minutes remain until the next payout.

How it works

A timer runs every 60 seconds and adds the elapsed time to each online player's record. Records are written to the plugin data file on server save, on plugin unload, and when a player disconnects. Reward progress is tracked separately from total playtime. When progress reaches the configured interval, the plugin creates the reward item and gives it to the player's inventory, dropping it at their feet if the inventory is full.

Progress only builds while the player is eligible. If the permission is granted later, no backlog of time is paid out. If the reward item shortname does not exist, rewards are disabled and a warning is printed at startup. Players whose SteamID is not valid are not tracked.

Use cases

Servers that want to reward regular players for staying online, such as a steady trickle of scrap or other resources. Admins can also gate payouts behind the playtimerewards.use permission, for example to limit rewards to VIP or donor groups. Note that /playtime itself requires the same permission, so grant it to every group that should see its playtime and reward timer.

Customizing it

The config file controls the reward item shortname, the amount paid per interval, the interval length in minutes (minimum 1; lower values are reset to 60 at startup), whether the permission is required to receive rewards, and whether players get a chat message when rewarded. The amount setting is named RewardAmountPerHour in the code, but it is paid once per interval, so the name is misleading if you change the interval length.

Commands

Chat and console commands
CommandWhat it doesPermission
/playtimeShows the player's total playtime and minutes remaining until the next reward. Requires the playtimerewards.use permission.playtimerewards.use

Permissions

Permissions
PermissionGrants
playtimerewards.useAllows use of the /playtime command and, when the RequirePermission option is enabled, is required to receive rewards.

Default config

oxide/config/PlaytimeRewards.json
{
  "Reward item shortname": "scrap",
  "Reward amount per interval": 100,
  "Reward interval (minutes online)": 60,
  "Require playtimerewards.use permission to receive rewards": false,
  "Notify player when rewarded": true
}

Install

  1. Save PlaytimeRewards.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/PlaytimeRewards.json (Carbon: carbon/configs/PlaytimeRewards.json). After editing, reload it:oxide.reload PlaytimeRewards c.reload PlaytimeRewards # Carbon
  4. Grant the permission

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

Hooks used

Report this plugin

Tell us what is wrong (malicious code, offensive content, copyright).

Want your own version?

Customize