C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using Newtonsoft.Json.Linq;
namespace Oxide.Plugins
{
[Info("Clan TP", "Nexus Labs", "1.0.3")]
[Description("Lets clan leaders pull their members to them. Has cooldown, warmup, combat/damage/movement cancels, etc. Compatible with both Oxide and Carbon.")]
public class ClanTeleport : RustPlugin
{
private ConfigData config; // our config stuff
private class ConfigData
{
public float CooldownSeconds = 600f;
public bool RequireAllOnline = false;
public float MaxDistance = 150f;
public bool UseNTeleportation = true;
public string TeleportPermission = "clanteleport.use";
public string BypassCooldownPermission = "clanteleport.bypass";
public bool ShowTeleportEffects = true;
public bool EnableWarmup = true;
public float WarmupTime = 8f;
public bool CancelOnMovement = true;
public bool CancelOnDamage = true;
public bool AllowDuringCombat = false;
public float CombatTime = 45f;
public int MaxMembers = 25;
public bool NotifyAllMembers = true;
}
[PluginReference] private Plugin Clans, NTeleportation;
private readonly Dictionary<ulong, DateTime> cooldowns = new();
private readonly Dictionary<ulong, Timer> warmupTimers = new();
private readonly Dictionary<ulong, DateTime> lastCombatTime = new();
private readonly Dictionary<ulong, Vector3> warmupPositions = new();
#region Oxide Hooks
protected override void LoadDefaultConfig()
{
config = new ConfigData();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<ConfigData>();
if (config == null) throw new Exception();
}
catch
{
PrintError("Config corrupted, loading defaults...");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
void Init()
{
permission.RegisterPermission(config.TeleportPermission, this);
permission.RegisterPermission(config.BypassCooldownPermission, this);
AddCovalenceCommand("clantp", "CmdClanTeleport");
AddCovalenceCommand("clantpcancel", "CmdCancelTeleport");
}
void OnServerInitialized()
{
if (Clans == null)
{
PrintError("Clans plugin missing! Need a compatible Clans plugin (e.g., k1lly0u/Reborn/Chaos). Unloading...");
Interface.Oxide.UnloadPlugin(Name);
return;
}
if (config.UseNTeleportation && NTeleportation == null)
PrintWarning("NTeleportation not loaded - falling back to basic teleport.");
}
void Unload()
{
foreach (var timer in warmupTimers.Values)
timer?.Destroy();
warmupTimers.Clear();
warmupPositions.Clear();
}
void OnPlayerDisconnected(BasePlayer player)
{
warmupTimers.Remove(player.userID);
warmupPositions.Remove(player.userID);
}
#endregion
#region Commands
private void CmdClanTeleport(IPlayer iplayer, string command, string[] args)
{
if (!(iplayer.Object is BasePlayer player)) return; // not a player, bail
if (!permission.UserHasPermission(player.UserIDString, config.TeleportPermission))
{
Reply(player, "no_permission");
return;
}
if (!config.AllowDuringCombat && IsInCombat(player))
{
Reply(player, "in_combat");
return;
}
string clanTag = GetPlayerClanTag(player);
if (string.IsNullOrEmpty(clanTag))
{
Reply(player, "no_clan");
return;
}
ulong? leaderId = GetClanLeaderId(clanTag);
if (!leaderId.HasValue || player.userID != leaderId.Value)
{
Reply(player, "not_leader");
return;
}
if (!CanUseTeleport(player))
{
double remaining = GetCooldownRemaining(player.userID);
Reply(player, "cooldown", Math.Ceiling(remaining));
return;
}
if (config.EnableWarmup)
StartWarmup(player, clanTag);
else
ExecuteTeleport(player, clanTag);
}
private void CmdCancelTeleport(IPlayer iplayer, string command, string[] args)
{
if (!(iplayer.Object is BasePlayer player)) return;
if (warmupTimers.TryGetValue(player.userID, out Timer timer))
{
timer.Destroy();
warmupTimers.Remove(player.userID);
warmupPositions.Remove(player.userID);
Reply(player, "cancelled");
}
else
{
Reply(player, "no_warmup");
}
}
#endregion
#region Warmup & Teleport Logic
private void StartWarmup(BasePlayer leader, string clanTag)
{
if (warmupTimers.TryGetValue(leader.userID, out Timer existingTimer))
existingTimer.Destroy(); // kill old timer if any
warmupPositions[leader.userID] = leader.transform.position;
Reply(leader, "warmup_start", config.WarmupTime);
Reply(leader, "warmup_cancel_hint");
var members = GetClanMembers(clanTag);
if (config.NotifyAllMembers)
{
foreach (var member in members.Where(m => m != leader && m.IsConnected))
{
Reply(member, "notify_member", leader.displayName, config.WarmupTime);
}
}
warmupTimers[leader.userID] = timer.Once(config.WarmupTime, () =>
{
warmupTimers.Remove(leader.userID);
warmupPositions.Remove(leader.userID);
if (!leader.IsConnected || leader.IsDead() || leader.IsSleeping())
return; // leader gone, abort
ExecuteTeleport(leader, clanTag);
});
}
private void ExecuteTeleport(BasePlayer leader, string clanTag)
{
var members = GetClanMembers(clanTag);
if (config.RequireAllOnline && members.Any(m => !m.IsConnected && m != leader))
{
Reply(leader, "require_all_online");
return;
}
if (members.Count > config.MaxMembers)
{
Reply(leader, "too_many_members", config.MaxMembers);
return;
}
int teleportedCount = 0;
foreach (var member in members)
{
if (member == null || !member.IsConnected || member == leader || member.IsDead() || member.IsSleeping())
continue;
if (config.MaxDistance > 0 && Vector3.Distance(member.transform.position, leader.transform.position) > config.MaxDistance)
{
Reply(leader, "member_too_far", member.displayName);
continue;
}
if (!config.AllowDuringCombat && IsInCombat(member))
{
Reply(leader, "member_in_combat", member.displayName);
continue;
}
TeleportMember(leader, member);
teleportedCount++;
if (config.NotifyAllMembers)
Reply(member, "teleported_success", leader.displayName);
}
ApplyCooldown(leader);
Reply(leader, "teleport_complete", teleportedCount);
}
private void TeleportMember(BasePlayer leader, BasePlayer member)
{
// Slight random spread to avoid stacking issues
Vector3 offset = UnityEngine.Random.insideUnitSphere * 1.5f;
offset.y = 0;
Vector3 targetPos = leader.transform.position + leader.transform.forward * 1.5f + offset;
if (config.UseNTeleportation && NTeleportation != null)
{
// Updated to use "Teleport" based on some old code examples, hope it works
NTeleportation.Call("Teleport", member, targetPos);
}
else
{
member.Teleport(targetPos);
member.SendNetworkUpdate(); // make sure client updates
}
if (config.ShowTeleportEffects)
{
Effect.server.Run("assets/prefabs/tools/teleporter/effects/teleport_beam.prefab", member.transform.position);
Effect.server.Run("assets/prefabs/tools/teleporter/effects/teleport_beam.prefab", targetPos);
}
}
#endregion
#region Helpers
private string GetPlayerClanTag(BasePlayer player)
{
// For k1lly0u's Clans, use GetClanOf with string ID
return Clans?.Call<string>("GetClanOf", player.UserIDString);
}
private ulong? GetClanLeaderId(string clanTag)
{
// Get the clan object
object clanObj = Clans?.Call("GetClan", clanTag);
if (clanObj is JObject jClan)
{
string ownerStr = jClan["owner"]?.ToString();
if (!string.IsNullOrEmpty(ownerStr) && ulong.TryParse(ownerStr, out ulong ownerId))
return ownerId;
// Fallback if it's "leader" in some plugins
string leaderStr = jClan["leader"]?.ToString();
if (!string.IsNullOrEmpty(leaderStr) && ulong.TryParse(leaderStr, out ulong leaderId))
return leaderId;
}
return null;
}
private List<BasePlayer> GetClanMembers(string clanTag)
{
var members = new List<BasePlayer>();
// For k1lly0u, get clan and extract members
object clanObj = Clans?.Call("GetClan", clanTag);
if (clanObj is JObject jClan)
{
var membersArray = jClan["members"] as JArray;
if (membersArray != null)
{
foreach (var idToken in membersArray)
{
if (ulong.TryParse(idToken.ToString(), out ulong userId))
{
var player = BasePlayer.FindAwakeOrSleeping(userId.ToString()); // Use string for Find
if (player != null) members.Add(player);
}
}
}
}
return members;
}
private bool CanUseTeleport(BasePlayer player)
{
if (permission.UserHasPermission(player.UserIDString, config.BypassCooldownPermission))
return true;
if (!cooldowns.TryGetValue(player.userID, out DateTime lastUse))
return true;
return (DateTime.UtcNow - lastUse).TotalSeconds >= config.CooldownSeconds;
}
private double GetCooldownRemaining(ulong userId)
{
if (!cooldowns.TryGetValue(userId, out DateTime lastUse))
return 0;
double remaining = config.CooldownSeconds - (DateTime.UtcNow - lastUse).TotalSeconds;
return Math.Max(0, remaining);
}
private void ApplyCooldown(BasePlayer player)
{
if (!permission.UserHasPermission(player.UserIDString, config.BypassCooldownPermission))
{
cooldowns[player.userID] = DateTime.UtcNow;
}
}
private bool IsInCombat(BasePlayer player)
{
if (!lastCombatTime.TryGetValue(player.userID, out DateTime time))
return false;
return (DateTime.UtcNow - time).TotalSeconds < config.CombatTime;
}
private void Reply(BasePlayer player, string key, params object[] args)
{
string message = lang.GetMessage(key, this, player.UserIDString);
if (args.Length > 0) message = string.Format(message, args);
player.ChatMessage(message);
}
#endregion
#region Combat / Movement Detection
void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (entity is BasePlayer player && info?.InitiatorPlayer != null)
{
lastCombatTime[player.userID] = DateTime.UtcNow;
if (config.CancelOnDamage && warmupTimers.ContainsKey(player.userID))
CancelWarmup(player, "damage_cancel");
}
}
void OnPlayerAttack(BasePlayer attacker, HitInfo info)
{
if (attacker != null && info?.HitEntity is BaseCombatEntity)
{
lastCombatTime[attacker.userID] = DateTime.UtcNow;
}
}
void OnPlayerInput(BasePlayer player, InputState input)
{
if (!config.CancelOnMovement || !warmupTimers.ContainsKey(player.userID))
return;
if (!warmupPositions.TryGetValue(player.userID, out Vector3 startPos))
return;
float moveThreshold = 2.5f;
bool moved = Vector3.Distance(player.transform.position, startPos) > moveThreshold;
bool jumpedOrSprinted = input.WasJustPressed(BUTTON.JUMP) || input.WasJustPressed(BUTTON.SPRINT);
if (moved || jumpedOrSprinted)
{
CancelWarmup(player, "movement_cancel");
}
}
private void CancelWarmup(BasePlayer player, string reasonKey)
{
if (warmupTimers.TryGetValue(player.userID, out Timer timer))
{
timer.Destroy();
warmupTimers.Remove(player.userID);
warmupPositions.Remove(player.userID);
Reply(player, reasonKey);
}
}
#endregion
#region Lang
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["no_permission"] = "<color=#ff4444>You don't have permission to use this.</color>",
["no_clan"] = "<color=#ff4444>You're not in a clan!</color>",
["not_leader"] = "<color=#ff4444>Only the clan leader can call the squad.</color>",
["in_combat"] = "<color=#ff4444>Can't TP while in combat.</color>",
["cooldown"] = "<color=#ff4444>Wait {0} seconds before calling again.</color>",
["warmup_start"] = "<color=#ffff44>Clan pull starting in {0} seconds - hold still!</color>",
["warmup_cancel_hint"] = "<color=#aaaa44>Use /clantpcancel if you need to abort.</color>",
["cancelled"] = "<color=#ffff44>Pull cancelled.</color>",
["no_warmup"] = "<color=#ffff44>Nothing to cancel.</color>",
["notify_member"] = "<color=#ffff44>Your leader {0} is pulling the clan in {1} seconds!</color>",
["require_all_online"] = "<color=#ff4444>Everyone needs to be online for this.</color>",
["too_many_members"] = "<color=#ff4444>Too many members (limit {0}).</color>",
["member_too_far"] = "<color=#ffff44>{0} is too far away to pull.</color>",
["member_in_combat"] = "<color=#ffff44>{0} is in combat.</color>",
["teleport_complete"] = "<color=#44ff44>Pulled {0} members to you!</color>",
["teleported_success"] = "<color=#44ff44>{0} pulled you in!</color>",
["damage_cancel"] = "<color=#ff4444>Pull cancelled - you got hit!</color>",
["movement_cancel"] = "<color=#ff4444>Pull cancelled - you moved!</color>"
}, this);
}
#endregion
#region Console Command(s)
[ConsoleCommand("clanteleport.resetcooldown")]
private void CmdResetCooldown(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin) return;
cooldowns.Clear();
Puts("All clan TP cooldowns reset by admin."); // log it
arg.ReplyWith("Cooldowns cleared.");
}
#endregion
}
}