OnPhoneCallStarted
OnPhoneCallStarted is called from PhoneController.BeginCall after the calling phone switches to the InProcess state and the call timeout has been scheduled, but before the method returns.
void OnPhoneCallStarted(PhoneController phoneController, PhoneController activeCallTo, BasePlayer currentPlayer)Parameters
| Parameter | Type | Description |
|---|---|---|
phoneController | PhoneController | The PhoneController of the phone that placed the call. Its net.ID identifies the calling device and it carries the current call state. |
activeCallTo | PhoneController | The PhoneController on the receiving end of the call, or null if the target did not resolve to a phone entity. |
currentPlayer | BasePlayer | The BasePlayer who is operating the calling phone. Can be null in edge cases, so null-check before use. |
Return value
Any value returned from OnPhoneCallStarted is ignored; the original BeginCall continues unchanged. To prevent a call you must hook earlier in the dialing path, not here.
How it works
The hook fires inside PhoneController.BeginCall, which runs when a player successfully starts an outgoing call from a telephone. By the time it is invoked the calling phone has already been set to Telephone.CallState.InProcess and TimeOutCall has been scheduled at TelephoneManager.MaxCallLength, so the call is live regardless of what your plugin does in the hook. phoneController is the calling device, activeCallTo is the receiving device (or null when the dial target was not a phone), and currentPlayer is the player who dialed. Both phone arguments are useful for matching the two ends of the call, recording which numbers pair up, or gating features on the caller. Since a call has already started, the hook is best used for reaction rather than prevention: log calls, notify staff, play an effect at the receiving phone, or enforce a per-player call limit by acting on the player. The return value is ignored, so you cannot cancel the call from here; use CanReceiveCall or a dial hook if you need to block it. Declaring BasePlayer for the third parameter is already the most specific type; Oxide only calls your method when the argument matches the declared type.
Example plugin: Announce phone calls to admins and play a ring effect
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Phone Call Announcer", "RustPluginGenerator", "1.0.0")]
[Description("Notifies online admins when a phone call starts and plays a ring effect at the receiver.")]
public class PhoneCallAnnouncer : RustPlugin
{
private const string PermBypass = "phonecallannouncer.bypass";
private PluginConfig config;
private readonly Dictionary<ulong, float> lastCall = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Notify admins")]
public bool NotifyAdmins { get; set; } = true;
[JsonProperty("Cooldown seconds per player")]
public float CooldownSeconds { get; set; } = 30f;
[JsonProperty("Play ring effect")]
public bool PlayRingEffect { get; set; } = true;
}
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>
{
["CallNotice"] = "{0} started a phone call (from #{1} to #{2})"
}, 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(PermBypass, this);
}
private ulong GetEntityId(PhoneController controller)
{
if (controller == null) return 0UL;
var entity = controller.GetComponent<BaseEntity>();
if (entity == null || entity.net == null) return 0UL;
return entity.net.ID.Value;
}
private void OnPhoneCallStarted(PhoneController phoneController, PhoneController activeCallTo, BasePlayer currentPlayer)
{
if (currentPlayer == null || phoneController == null) return;
ulong callerId = currentPlayer.userID.Get();
if (!permission.UserHasPermission(currentPlayer.UserIDString, PermBypass))
{
float now = UnityEngine.Time.realtimeSinceStartup;
float last;
if (lastCall.TryGetValue(callerId, out last) && now - last < config.CooldownSeconds) return;
lastCall[callerId] = now;
}
if (config.PlayRingEffect && activeCallTo != null)
{
Effect.server.Run("assets/bundled/prefabs/fx/telephone/ring_2d.prefab", activeCallTo.transform.position);
}
if (!config.NotifyAdmins || activeCallTo == null) return;
string message = Msg("CallNotice", null, currentPlayer.displayName, GetEntityId(phoneController), GetEntityId(activeCallTo));
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected || !player.IsAdmin) continue;
player.ChatMessage(message);
}
}
private void Unload()
{
lastCall.Clear();
}
}
}The plugin listens for OnPhoneCallStarted and, when the optional Notify Admins config is on, sends a chat message to every connected admin that includes the caller's display name and the two phone entity IDs. It also runs a short ring sound effect at the receiving phone's position so the event is noticeable in-world. A per-player cooldown dictionary (lastCall) prevents the same caller from spamming the admin channel within the configured interval. The hook null-checks currentPlayer and activeCallTo before using them, and all player-facing text goes through the language file so admins can localize the output. On Unload the cooldown dictionary is cleared.
Common use cases
Admin call logging
Post a chat or console notice to online admins whenever a call starts, including the caller and the two phone IDs so reports can be cross-referenced.
Per-player call limits
Track call timestamps per player and warn or act on players who exceed a configured number of calls per minute, since the hook fires at the start of every live call.
In-world feedback for the receiver
Spawn an effect or sound at the receiving phone's position so nearby players know a call is arriving even before they answer.
Pitfalls
You cannot cancel the call here
The phone is already in the InProcess state and the timeout is scheduled before the hook fires, and the return value is ignored, so blocking requires an earlier dial or receive hook.
activeCallTo can be null
The receiving controller is not guaranteed to resolve; always null-check before reading its position, net.ID, or state.
Firing frequency
This fires once per call, not per tick, but busy servers with voice/phone plugins can still produce steady traffic, so avoid heavy work like entity scans in the handler.
Build a plugin with OnPhoneCallStarted
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 OnPhoneCallStarted
Generate