OnPlayerHealthChange
OnPlayerHealthChange is called from BasePlayer.OnHealthChanged every time a player's health value is updated, before the base health-change logic and network update run.
object OnPlayerHealthChange(BasePlayer player, float oldvalue, float newvalue)Parameters
| Parameter | Type | Description |
|---|---|---|
player | BasePlayer | The BasePlayer whose health value is changing. Already spawned and valid when the hook fires. |
oldvalue | float | The health value the player had before this change, as a float. |
newvalue | float | The health value the player is about to have. Compare it with oldvalue to tell healing from damage. |
Return value
Returning null (or nothing) lets the original BasePlayer.OnHealthChanged run, so the base health logic and network update proceed normally. Returning any non-null object makes the patched code return before calling the base implementation, so the change is not propagated through the normal health-change path. Treat it as a way to stop the update, not to set a custom value: the new number has already been assigned when the hook runs.
How it works
The hook fires inside BasePlayer.OnHealthChanged(float oldvalue, float newvalue), which the game calls whenever a player's health number is set to a new value: taking damage, healing, bleeding ticks, radiation, metabolism effects, revive and respawn flows. It is a low-level notification of the value change, not a damage event, so it does not tell you what caused the change.
player is the affected BasePlayer. oldvalue is the previous health and newvalue is the incoming value, both raw floats. Subtracting oldvalue from newvalue gives the signed delta: negative means health dropped, positive means it went up. The hook fires often for wounded, bleeding or irradiated players, so keep the body cheap and avoid Puts/PrintWarning inside it.
Declaring player as BasePlayer matches every call because the game only ever passes a BasePlayer here. There is no narrower subtype to filter on; if you only care about player-controlled characters, check player.IsNpc and player.IsConnected yourself. For the cause of the change use OnEntityTakeDamage instead, since this hook has no HitInfo.
Example plugin: Announce health thresholds and heavy hits to a team
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Health Change Notifier", "RustPluginGenerator", "1.0.0")]
[Description("Warns a player's team when health crosses configured thresholds or drops sharply")]
public class HealthChangeNotifier : RustPlugin
{
private const string PermNotify = "healthchangenotifier.notify";
private PluginConfig config;
private readonly Dictionary<ulong, float> nextNotify = new Dictionary<ulong, float>();
private class Threshold
{
[JsonProperty("Health percent")]
public int Percent { get; set; }
[JsonProperty("Message key")]
public string Key { get; set; }
}
private class PluginConfig
{
[JsonProperty("Warning thresholds (highest first)")]
public List<Threshold> Thresholds { get; set; } = new List<Threshold>
{
new Threshold { Percent = 50, Key = "Half" },
new Threshold { Percent = 25, Key = "Quarter" },
new Threshold { Percent = 10, Key = "Critical" }
};
[JsonProperty("Minimum single drop to report (health points)")]
public float BigHitDelta { get; set; } = 25f;
[JsonProperty("Seconds between warnings per player")]
public float Cooldown { get; set; } = 8f;
}
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>
{
["Half"] = "{0} dropped below 50% health ({1:0})",
["Quarter"] = "{0} dropped below 25% health ({1:0})",
["Critical"] = "{0} is critical at {1:0} health",
["BigHit"] = "{0} took a large hit, now at {1:0} health",
["SelfWarn"] = "You are at {0:0} health"
}, 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(PermNotify, this);
}
private void Unload()
{
nextNotify.Clear();
}
private object OnPlayerHealthChange(BasePlayer player, float oldvalue, float newvalue)
{
if (player == null || player.IsNpc || !player.IsConnected) return null;
if (newvalue <= 0f || newvalue >= oldvalue) return null;
if (oldvalue <= 0f) return null;
float now = Time.realtimeSinceStartup;
float next;
if (nextNotify.TryGetValue(player.userID, out next) && now < next) return null;
float max = player.MaxHealth();
if (max <= 0f) return null;
float oldPct = oldvalue / max * 100f;
float newPct = newvalue / max * 100f;
string warning = null;
if (config.Thresholds != null)
{
foreach (Threshold threshold in config.Thresholds)
{
if (threshold == null) continue;
if (oldPct > threshold.Percent && newPct <= threshold.Percent)
{
warning = Msg(threshold.Key, player.UserIDString, player.displayName, newvalue);
break;
}
}
}
if (warning == null && oldvalue - newvalue >= config.BigHitDelta)
warning = Msg("BigHit", player.UserIDString, player.displayName, newvalue);
if (warning == null) return null;
nextNotify[player.userID] = now + config.Cooldown;
NotifyTeam(player, warning);
return null;
}
private void NotifyTeam(BasePlayer player, string message)
{
if (player.currentTeam == 0) return;
RelationshipManager.PlayerTeam team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
if (team == null) return;
foreach (ulong memberId in team.members)
{
if (memberId == player.userID) continue;
BasePlayer member = BasePlayer.FindByID(memberId);
if (member == null || !member.IsConnected) continue;
if (!permission.UserHasPermission(member.UserIDString, PermNotify)) continue;
member.ChatMessage(message);
}
}
}
}The plugin stores a list of percentage thresholds in its config, each with a language key and color. In OnPlayerHealthChange it computes the health percentage before and after the change and checks whether the player crossed any configured threshold on the way down, sending a team chat warning when that happens. It also reports large single-step drops (configurable minimum delta) so teammates see big hits. Npcs, disconnected players and players without a team are skipped. All text goes through the language file, and each player has a short cooldown so bleed ticks cannot spam the team.
Common use cases
Low health warnings
Notify a player or their team when health crosses 50%, 25% or 10% so they can react before going down.
Damage audit trail
Record large health drops with timestamps into oxide/data for admin review of suspicious damage patterns.
Healing feedback
Detect when health rises and show a small confirmation, useful for custom medical items that call Heal() directly.
Pitfalls
Not a damage hook
There is no HitInfo, attacker or damage type here. Use OnEntityTakeDamage when you need to know who caused the change or how much of each damage type was applied.
Fires very often
Bleeding, radiation and metabolism update health repeatedly. Keep the callback light, cache config lookups, and never write to console from it.
Fires on server and client objects
The method exists on BasePlayer generally; guard with player.IsNpc, player.IsConnected and IsAlive() before touching team or chat APIs.
Build a plugin with OnPlayerHealthChange
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 OnPlayerHealthChange
Generate