OnPlayerInput
OnPlayerInput is called on every input tick from BasePlayer.OnReceiveTick, after the player's client input has been decoded into an InputState. Returning a non-null value aborts the rest of the tick, skipping the game's normal processing for that input.
object OnPlayerInput(BasePlayer player, InputState serverInput)Parameters
| Parameter | Type | Description |
|---|---|---|
player | BasePlayer | The BasePlayer whose input packet was just processed. Check IsConnected and IsNpc before acting on it. |
serverInput | InputState | The InputState built from the client's input packet for this tick. Read serverInput.current.buttons and serverInput.previous.buttons to detect newly pressed buttons, and use WasJustPressed/IsDown on it. |
Return value
Returning null lets the tick continue normally. Returning any non-null value makes the original code return immediately, before spectating, movement and other post-input processing run, so the player's packet is effectively swallowed for that tick.
How it works
The hook fires inside BasePlayer.OnReceiveTick, right after the server compares the current and previous button masks and after the snapshot/transfer-protection check. Every input packet that reaches the server goes through this path, so the hook runs many times per second for each connected player.
serverInput is the decoded InputState: current.buttons, previous.buttons, IsDown(BUTTON.FIRE_PRIMARY), WasJustPressed(BUTTON.USE) and the movement/aim fields are all readable here. Because you get both button masks you can distinguish a held key from a fresh press without keeping your own state.
The hook is not per-player filterable by type narrowing; declaring BasePlayer is the only sensible signature, and the game does not call it for NPCs. Treat it strictly as a read-only inspection point: the tick is hot, so keep the callback short, avoid PrintToChat inside it in normal operation, and cheaply increment counters or timestamp activity.
Returning something non-null stops the tick early, which suppresses the rest of the game's input handling for that frame.
Example plugin: Track time since each player's last input
using System;
using System.Collections.Generic;
using Facepunch.Math;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Input Idle Tracker", "RustPluginGenerator", "1.0.0")]
[Description("Records the last input time per player and reports idle time to admins")]
public class InputIdleTracker : RustPlugin
{
private const string PermAdmin = "inputidletracker.admin";
private PluginConfig config;
private readonly Dictionary<string, InputRecord> records = new Dictionary<string, InputRecord>();
private class PluginConfig
{
[JsonProperty("Idle threshold in seconds")]
public int IdleThresholdSeconds = 300;
}
private class InputRecord
{
public int LastInput;
public int Packets;
}
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>
{
["NoPermission"] = "You do not have permission to use this command.",
["NoRecord"] = "No input recorded for {0}.",
["Status"] = "{0} last sent input {1} seconds ago ({2} packets this session, idle threshold {3}s).",
["Usage"] = "Usage: /idle <name or steamid>"
}, 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 object OnPlayerInput(BasePlayer player, InputState serverInput)
{
if (player == null || player.IsNpc || !player.IsConnected)
return null;
var id = player.UserIDString;
InputRecord record;
if (!records.TryGetValue(id, out record))
{
record = new InputRecord();
records[id] = record;
}
record.LastInput = Epoch.Current;
record.Packets++;
return null;
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
records.Remove(player.UserIDString);
}
[ChatCommand("idle")]
private void CmdIdle(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
SendReply(player, Msg("Usage", player.UserIDString));
return;
}
var target = BasePlayer.FindAwakeOrSleeping(args[0]);
if (target == null)
{
SendReply(player, Msg("NoRecord", player.UserIDString, args[0]));
return;
}
InputRecord record;
if (!records.TryGetValue(target.UserIDString, out record))
{
SendReply(player, Msg("NoRecord", player.UserIDString, target.displayName));
return;
}
var idle = Epoch.Current - record.LastInput;
SendReply(player, Msg("Status", player.UserIDString, target.displayName, idle, record.Packets, config.IdleThresholdSeconds));
}
private void Unload()
{
records.Clear();
}
}
}The plugin records the last time an input tick arrived for each player in a dictionary keyed by UserIDString. OnPlayerInput refreshes that timestamp and counts packets, keeping the work to two property writes. Admins run /idle <name or id> to print how many seconds have passed since that player's last packet plus the packet count. OnPlayerDisconnected removes the entry so the dictionary does not grow. Timestamps use Facepunch.Math.Epoch.Current, so no timers or background work are needed.
Common use cases
Idle and AFK detection
Compare the stored last-input time against the current epoch to flag players who have sent no packets for several minutes, without polling.
Macro and auto-clicker heuristics
Count how often WASJUSTPRESSED(BUTTON.FIRE_PRIMARY) occurs in a window; unnaturally regular press intervals are worth investigating.
Intercept input for a scripted sequence
While a plugin-driven cinematic or forced teleport is active, return true to skip the normal tick so player movement does not fight the sequence.
Pitfalls
This hook is extremely hot
It fires for every connected player many times per second. Avoid allocations, string formatting and chat output in the body; a heavy handler here costs server CPU directly.
Returning non-null blocks the whole tick
Any non-null return stops the remainder of OnReceiveTick, not just your chosen action. Use it only for deliberate input suppression and return null otherwise.
Not a replacement for OnPlayerTick
Input ticks stop when a client stops sending packets even if the entity still simulates; use OnPlayerTick for time-based state that must keep running.
Plugin recipes that use OnPlayerInput
Build a plugin with OnPlayerInput
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 OnPlayerInput
Generate