C#:
using System;
using System.Collections.Generic;
using Oxide.Core;
using ProtoBuf;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("MarkerTeleport", "Choanie", "1.0.0")]
[Description("Teleports players to their own map marker, with a teleport effect and auto-destruction of the marker used")]
public class MarkerTeleport : RustPlugin
{
#region Fields
private Configuration _config;
private HashSet<ulong> _enabled;
private const string DataFile = "MarkerTeleport_enabled";
#endregion
#region Config
protected override void LoadDefaultConfig() => _config = Configuration.Default();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null) throw new Exception();
}
catch
{
PrintWarning("Config file is corrupt or missing, creating a new one.");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config);
private class Configuration
{
public bool DestroyMarkerOnTeleport = true;
public float DestroyMarkerDelaySeconds = 0.3f;
public bool PlayVfx = true;
public bool PlaySfx = true;
public string TeleportDepartEffectPrefab = "assets/prefabs/missions/portal/proceduraldungeon/effects/disappear.prefab";
public string TeleportArriveEffectPrefab = "assets/prefabs/missions/portal/proceduraldungeon/effects/appear.prefab";
public float FlyhackPauseSeconds = 10f;
public static Configuration Default() => new();
}
#endregion
#region Data persistence
private void LoadData()
{
try
{
_enabled = Interface.Oxide.DataFileSystem.ReadObject<HashSet<ulong>>(DataFile);
if (_enabled == null) throw new Exception();
}
catch
{
_enabled = new();
}
}
private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(DataFile, _enabled);
#endregion
#region Lifecycle
private void Init()
{
LoadData();
}
private void OnServerInitialized()
{
foreach (var player in BasePlayer.activePlayerList)
EnsureAdminDefault(player);
}
private void OnPlayerConnected(BasePlayer player) => EnsureAdminDefault(player);
private void EnsureAdminDefault(BasePlayer player)
{
if (player == null || !player.IsAdmin) return;
if (_enabled.Add(player.userID)) SaveData();
}
#endregion
#region Commands
[ChatCommand("tpm")]
private void TpmCommand(BasePlayer player, string command, string[] args)
{
if (!player.IsAdmin)
{
player.ChatMessage("You don't have permission to use this command.");
return;
}
if (_enabled.Add(player.userID))
{
SaveData();
player.ChatMessage("MarkerTeleport enabled — placing a map marker will teleport you to it.");
return;
}
_enabled.Remove(player.userID);
SaveData();
player.ChatMessage("MarkerTeleport disabled.");
}
#endregion
#region Marker teleport
private void OnMapMarkerAdded(BasePlayer player, MapNote note)
{
if (player == null || note == null) return;
if (player.isMounted || !player.IsAlive()) return;
if (!player.IsAdmin || !_enabled.Contains(player.userID)) return;
TeleportToMarker(player, note);
}
private void TeleportToMarker(BasePlayer player, MapNote note)
{
var origin = player.transform.position;
var destination = ResolveGroundPosition(note.worldPosition);
player.PauseFlyHackDetection(_config.FlyhackPauseSeconds);
player.Teleport(destination);
player.UpdateNetworkGroup();
player.SendCompleteSnapshot();
if (_config.PlayVfx || _config.PlaySfx) PlayTeleportEffect(origin, destination);
if (_config.DestroyMarkerOnTeleport)
timer.Once(_config.DestroyMarkerDelaySeconds, () => DestroyMarker(player, note));
}
private static readonly int GroundLayerMask = LayerMask.GetMask("Terrain", "World", "Construction", "Deployed", "Debris");
private static Vector3 ResolveGroundPosition(Vector3 target)
{
var castOrigin = target + Vector3.up * 200f;
if (Physics.Raycast(castOrigin, Vector3.down, out var hit, 400f, GroundLayerMask, QueryTriggerInteraction.Ignore))
return hit.point + Vector3.up * 0.5f;
target.y = TerrainMeta.HeightMap.GetHeight(target);
return target;
}
#endregion
#region Effects
private void PlayTeleportEffect(Vector3 origin, Vector3 destination)
{
if (!string.IsNullOrEmpty(_config.TeleportDepartEffectPrefab)) Effect.server.Run(_config.TeleportDepartEffectPrefab, origin);
if (!string.IsNullOrEmpty(_config.TeleportArriveEffectPrefab)) Effect.server.Run(_config.TeleportArriveEffectPrefab, destination);
}
#endregion
#region Marker destruction
private void DestroyMarker(BasePlayer player, MapNote note)
{
try
{
if (player == null || !player.IsConnected) return;
var notes = player.State?.pointsOfInterest;
if (notes == null || !notes.Remove(note)) return;
player.DirtyPlayerState();
player.TeamUpdate();
var mapNoteList = new MapNoteList { notes = new List<MapNote>() };
if (player.ServerCurrentDeathNote != null) mapNoteList.notes.Add(player.ServerCurrentDeathNote);
mapNoteList.notes.AddRange(notes);
player.ClientRPC(RpcTarget.Player("Client_ReceiveMarkers", player), mapNoteList);
}
catch (Exception ex)
{
PrintWarning($"Failed to destroy marker after teleport: {ex}");
}
}
#endregion
}
}