OnPortalUsed
OnPortalUsed is called after a player finishes traveling through a BasePortal that has a target portal. It fires at the end of BasePortal.UsePortal, after the player's network state has been updated for the destination.
void OnPortalUsed(BasePlayer player, BasePortal portal)Parameters
| Parameter | Type | Description |
|---|---|---|
player | BasePlayer | The BasePlayer who traveled through the portal. Use it for permissions, chat notices, teleport effects, or per-player cooldowns. |
portal | BasePortal | The BasePortal that was used. This is the sending portal, since the hook fires inside its UsePortal method after the player has been moved to the destination. |
Return value
Returning a value has no effect. The return value is ignored, and the game method continues normally; UsePortal returns void regardless of what the hook returns.
How it works
The hook fires inside BasePortal.UsePortal when a player interacts with a portal that has a valid target portal assigned. It runs after the transition sound and effect have played, after player.UpdateNetworkGroup(), and after the ReceivingSnapshot flag has been set on the player. The player argument is the traveler; the portal argument is the sending portal instance (this in the game method), so it carries the portal's position, owner and linked target data.
The hook is purely observational: it is called after the game has already moved the player and updated networking, so you cannot use it to cancel travel or redirect it to a different destination. Use the separate OnPortalUse hook for blocking or changing portal interactions before they happen.
Declaring a more specific parameter type for portal narrows dispatch, but the hook still only fires on successful travel because the game calls it at the end of the method. Null-check player and portal anyway since the hook can be invoked from unusual code paths or plugin calls. Avoid heavy work here, since the player's connection is mid-teleport.
Example plugin: Announce portal arrivals and log travel
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Portal Travel Announcer", "RustPluginGenerator", "1.0.0")]
[Description("Announces portal arrivals and logs travel counts per portal.")]
public class PortalTravelAnnouncer : RustPlugin
{
private PluginConfig config;
private StoredData data;
private const string PermStats = "portaltravelannouncer.stats";
private readonly Dictionary<ulong, float> lastTravel = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Announce arrivals to player")]
public bool Announce { get; set; } = true;
[JsonProperty("Chat cooldown (seconds)")]
public float Cooldown { get; set; } = 5f;
}
private class StoredData
{
[JsonProperty("Travel counts by portal id")]
public Dictionary<string, int> Counts { get; set; } = new Dictionary<string, int>();
}
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>
{
["Arrived"] = "You traveled through a portal. Arrived near {0}.",
["Stats"] = "Portal {0} has been used {1} times."
}, 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(PermStats, this);
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
private void OnPortalUsed(BasePlayer player, BasePortal portal)
{
if (player == null || portal == null || !player.IsConnected) return;
string portalId = portal.net != null ? portal.net.ID.Value.ToString() : "unknown";
int count;
data.Counts.TryGetValue(portalId, out count);
data.Counts[portalId] = count + 1;
if (!config.Announce) return;
float last;
if (lastTravel.TryGetValue(player.userID, out last))
{
if (UnityEngine.Time.realtimeSinceStartup - last < config.Cooldown) return;
}
lastTravel[player.userID] = UnityEngine.Time.realtimeSinceStartup;
Vector3 dest = portal.targetPortal != null ? portal.targetPortal.transform.position : portal.transform.position;
string label = string.Format("{0}, {1}", Mathf.RoundToInt(dest.x), Mathf.RoundToInt(dest.z));
player.ChatMessage(Msg("Arrived", player.UserIDString, label));
}
[ConsoleCommand("portal.stats")]
private void CmdStats(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermStats))
{
return;
}
foreach (var kvp in data.Counts)
{
arg.ReplyWith(Msg("Stats", player?.UserIDString, kvp.Key, kvp.Value));
}
}
}
}The plugin watches every successful portal travel and produces two outputs. First, it prints a localized chat message to the traveler confirming the destination position, using a configurable cooldown to avoid chat spam when portals are chained. Second, it tracks per-portal travel counts in a data file so admins can inspect heavy portal usage with a console command. LoadDefaultConfig gives admins a toggle and cooldown. The hook returns nothing because OnPortalUsed is an observational call site, so any return value would be ignored. Chat text is routed through the language file so server owners can translate it. The portal argument is used to read the destination portal name and position for the message.
Common use cases
Notify the traveler where they arrived
Read
portal.targetPortaland send a chat message with the destination portal's position so players are not confused after long-distance travel.Rate-limit portal hops
Store a per-player last-portal timestamp and use
OnPortalUseto block repeated use within a window;OnPortalUsedupdates the timestamp after successful travel.Track portal traffic for admins
Increment a counter per portal ID in a data file and expose a console command to show the busiest portals on the map.
Pitfalls
Hook fires after travel is complete
Returning false or true does nothing; use
OnPortalUsefor cancellation or gating, not this hook.Receiver is the sending portal
The
portalparameter is the source portal, not the destination. Look atportal.targetPortalif you need the destination data.
Build a plugin with OnPortalUsed
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 OnPortalUsed
Generate