OnPlayerTick
OnPlayerTick is called in BasePlayer.OnReceiveTick after the server stores the incoming input state and before it compares current and previous buttons. Returning a non-null value aborts the rest of that tick processing.
object OnPlayerTick(BasePlayer player, PlayerTick msg, bool wasPlayerStalled)Parameters
| Parameter | Type | Description |
|---|---|---|
player | BasePlayer | The BasePlayer whose input tick was received. Use it for per-player state (position, mounted status, held item) when deciding whether to block the tick. |
msg | PlayerTick | The PlayerTick network message being processed. It carries the client inputState; the game flips current and previous input state from it before this hook runs. |
wasPlayerStalled | bool | True when the server considered the player stalled (no input received recently) before processing this tick. Useful to distinguish recovered input from normal ticks. |
Return value
Return null (or let the method fall through) to let the tick continue normally. Returning any non-null value makes OnReceiveTick return immediately, so the rest of the tick, including input-button change detection, is skipped for that message.
How it works
The hook fires on the server every time a PlayerTick message arrives from a client, inside BasePlayer.OnReceiveTick. By the time it runs, the game has already stored the message input state as the player's current input (the flip step happens just before the call). The remaining tick work, including the comparison of current versus previous button state that drives button-press events, happens only if your hook returns null.
player lets you inspect the client that sent the tick, msg gives access to the raw tick payload (for example msg.inputState), and wasPlayerStalled tells you whether the player was flagged as stalled before this message. There is no safer narrowing option here: BasePlayer is the real type passed, and declaring anything else causes Oxide to skip the call rather than filter it.
Because this fires for every awake player many times per second, keep the body extremely cheap. Avoid allocations, string formatting, entity lookups and any chat or log output. Persist a small flag or timestamp on the player and do heavy work elsewhere.
Example plugin: Freeze input of players who failed a check
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Tick Input Freeze", "RustPluginGenerator", "1.0.0")]
[Description("Lets admins freeze a player's incoming input ticks and persist the frozen list")]
public class TickInputFreeze : RustPlugin
{
private const string PermUse = "tickinputfreeze.use";
private const string DataFile = "TickInputFreeze";
private PluginConfig config;
private HashSet<ulong> frozen = new HashSet<ulong>();
private Timer saveTimer;
private class PluginConfig
{
[JsonProperty("Ignore admins")]
public bool IgnoreAdmins { get; set; } = true;
[JsonProperty("Autosave interval (seconds)")]
public float AutosaveInterval { get; set; } = 120f;
}
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>
{
["Frozen"] = "{0} is now frozen.",
["Unfrozen"] = "{0} is no longer frozen.",
["NotFound"] = "No matching player found.",
["Usage"] = "Usage: /tickfreeze <name or id>",
["NoPermission"] = "You do not have permission to use this command."
}, 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(PermUse, this);
LoadData();
saveTimer = timer.Every(config.AutosaveInterval, SaveData);
}
private void Unload()
{
SaveData();
saveTimer?.Destroy();
frozen = null;
}
private void LoadData()
{
try
{
frozen = Interface.Oxide.DataFileSystem.ReadObject<HashSet<ulong>>(DataFile) ?? new HashSet<ulong>();
}
catch
{
frozen = new HashSet<ulong>();
}
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(DataFile, frozen);
}
private object OnPlayerTick(BasePlayer player, PlayerTick msg, bool wasPlayerStalled)
{
if (player == null || frozen == null || frozen.Count == 0) return null;
if (config.IgnoreAdmins && player.IsAdmin) return null;
if (!frozen.Contains(player.userID.Get())) return null;
return false;
}
[ChatCommand("tickfreeze")]
private void CmdFreeze(BasePlayer player, string command, string[] args)
{
if (player != null && !permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
BasePlayer target = ResolveTarget(player, args);
if (target == null)
{
Reply(player, Msg("NotFound", player?.UserIDString));
return;
}
frozen.Add(target.userID.Get());
Reply(player, Msg("Frozen", player?.UserIDString, target.displayName));
}
[ChatCommand("tickunfreeze")]
private void CmdUnfreeze(BasePlayer player, string command, string[] args)
{
if (player != null && !permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
BasePlayer target = ResolveTarget(player, args);
if (target == null || !frozen.Remove(target.userID.Get()))
{
Reply(player, Msg("NotFound", player?.UserIDString));
return;
}
Reply(player, Msg("Unfrozen", player?.UserIDString, target.displayName));
}
private BasePlayer ResolveTarget(BasePlayer caller, string[] args)
{
if (args == null || args.Length == 0)
{
Reply(caller, Msg("Usage", caller?.UserIDString));
return null;
}
return BasePlayer.FindAwakeOrSleeping(string.Join(" ", args));
}
private void Reply(BasePlayer player, string message)
{
if (player != null) SendReply(player, message);
else Puts(message);
}
}
}The plugin lets admins freeze a player with /tickfreeze <nameOrId> and unfreeze with /tickunfreeze <nameOrId>. A frozen player's ID is stored in a HashSet<ulong>, and OnPlayerTick returns a non-null marker when the sending player is in that set, which stops the tick from being processed further. The frozen set is saved to oxide/data so it survives restarts and is reloaded on startup. Chat feedback goes through the language file, permission tickinputfreeze.use gates both commands, and Unload clears the set reference and writes data one last time.
Common use cases
Freeze a cheater or AFK player
Drop ticks for a specific player so their input stops producing movement and button events while an admin investigates.
Tick rate telemetry
Count ticks per player on a cheap counter and report players whose rate is implausibly high from a periodic timer, not from the hook itself.
Stall recovery tracking
Record when
wasPlayerStalledis true to spot clients that repeatedly stall, then act from your own timer.
Pitfalls
Very high call frequency
The hook runs for every tick of every connected player. Anything beyond a set lookup and a boolean return will show up in server frame time.
Blocking does not disconnect
Returning non-null only skips the rest of this tick. The player stays connected and ticks keep arriving, so your condition must be cleared deliberately.
No parameter narrowing
Declaring
playeras a subclass makes Oxide skip the call for normal BasePlayer arguments, so the hook silently never fires.
Plugin recipes that use OnPlayerTick
- How to make a Rust playtime tracker plugin (total and wipe time)Records session start time on connect and adds elapsed connected time on disconnect, persisted per player ID in oxide/data, with a /playtime command that reports hours and minutes. Uses OnPlayerConnected, OnPlayerDisconnected and OnNewSave.
- How to make a Rust teleport requests plugin (TPR/TPA)A complete Oxide plugin that implements /tpr player-to-player teleport requests with /tpa, /tpc, request timeouts, a warmup before the move, a per-player cooldown and blocks for wounded, dead, mounted or swimming players. It uses OnPlayerTick and OnEntityTakeDamage to guard the warmup.
Build a plugin with OnPlayerTick
Describe what should happen and get a compiled plugin that uses this hook.
Edit the prompt on the next screen. 2 free plugins a month, no card.
Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.
Build a plugin with OnPlayerTick
Generate