C#:
using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins;
[Info("FBradleyRoute", "fufeldev", "1.0.0")]
public class FBradleyRoute : RustPlugin
{
private const string BradleyPrefab = "assets/prefabs/npc/m2bradley/bradleyapc.prefab";
private const string DataPath = "FBradleyRoute";
private static readonly int Mask = LayerMask.GetMask("Default", "Terrain", "Construction", "Water", "Deployed", "Tree", "Debris", "World", "Player (Server)", "AI", "Ragdoll");
private readonly Dictionary<ulong, RouteState> editRoutes = new Dictionary<ulong, RouteState>();
private readonly Dictionary<ulong, RouteState> controlledBradleys = new Dictionary<ulong, RouteState>();
private readonly List<ulong> removeControlled = new List<ulong>();
private readonly HashSet<ulong> hiddenPlayers = new HashSet<ulong>();
private Timer drawTimer;
private Timer routeTimer;
private class RouteState
{
public readonly List<Vector3> Points = new List<Vector3>();
public bool Loop;
public ulong OwnerId;
}
private class StoredData
{
public Dictionary<ulong, StoredRoute> Routes = new Dictionary<ulong, StoredRoute>();
}
private class StoredRoute
{
public List<StoredPoint> Points = new List<StoredPoint>();
public bool Loop;
}
private class StoredPoint
{
public float X;
public float Y;
public float Z;
}
private void OnServerInitialized()
{
LoadData();
drawTimer = timer.Every(2f, DrawAllRoutes);
routeTimer = timer.Every(1f, TickControlledRoutes);
}
private void Unload()
{
SaveData();
drawTimer?.Destroy();
routeTimer?.Destroy();
editRoutes.Clear();
controlledBradleys.Clear();
removeControlled.Clear();
hiddenPlayers.Clear();
}
[ChatCommand("br")]
private void CmdBr(BasePlayer player, string command, string[] args)
{
HandleCommand(player, args);
}
[ChatCommand("bradleyroute")]
private void CmdBradleyRoute(BasePlayer player, string command, string[] args)
{
HandleCommand(player, args);
}
[ChatCommand("spawnbradley")]
private void CmdSpawnBradley(BasePlayer player, string command, string[] args)
{
if (player == null || !player.IsAdmin) return;
var result = SpawnRouteBradley();
if (result == null) SendReply(player, "Не удалось создать Bradley. Нет сохранённого маршрута (минимум 2 точки) или танк уже жив.");
else SendReply(player, $"Bradley создан на первой точке маршрута: {result}");
}
private object OnBradleyApcPatrol(BradleyAPC bradley)
{
return IsControlled(bradley) ? true : null;
}
private object OnBradleyApcHunt(BradleyAPC bradley)
{
return IsControlled(bradley) ? true : null;
}
private object CanBradleyApcTarget(BradleyAPC bradley, BaseEntity target)
{
return null;
}
private void OnEntityKill(BaseNetworkable entity)
{
BradleyAPC bradley = entity as BradleyAPC;
if (bradley?.net == null) return;
controlledBradleys.Remove(bradley.net.ID.Value);
}
private void HandleCommand(BasePlayer player, string[] args)
{
if (player == null) return;
if (!player.IsAdmin)
{
SendReply(player, "Нет доступа.");
return;
}
string action = args != null && args.Length > 0 ? args[0].ToLower() : "help";
if (action == "start" || action == "new")
{
RouteState route = GetEditRoute(player);
route.Points.Clear();
route.Loop = HasArg(args, "loop");
SaveData();
SendReply(player, route.Loop ? "Новый маршрут: кольцо." : "Новый маршрут: от старта к финишу.");
return;
}
if (action == "loop")
{
RouteState route = GetEditRoute(player);
route.Loop = true;
SaveData();
SendReply(player, "Маршрут переключён в кольцо.");
return;
}
if (action == "once")
{
RouteState route = GetEditRoute(player);
route.Loop = false;
SaveData();
SendReply(player, "Маршрут переключён в режим старт-финиш.");
return;
}
if (action == "add")
{
if (!TryGetPoint(player, out Vector3 point))
{
SendReply(player, "Не вижу точку для маршрута.");
return;
}
RouteState route = GetEditRoute(player);
route.Points.Add(point);
SaveData();
SendReply(player, $"Точка {route.Points.Count}: {Format(point)}");
DrawRoute(player, route, 8f);
return;
}
if (action == "undo")
{
RouteState route = GetEditRoute(player);
if (route.Points.Count > 0) route.Points.RemoveAt(route.Points.Count - 1);
SaveData();
SendReply(player, $"Точек в маршруте: {route.Points.Count}.");
DrawRoute(player, route, 8f);
return;
}
if (action == "clear")
{
editRoutes.Remove(player.userID);
SaveData();
SendReply(player, "Редактируемый маршрут очищен.");
return;
}
if (action == "show")
{
RouteState route = GetEditRoute(player);
DrawRoute(player, route, 20f);
SendReply(player, $"Показан маршрут. Точек: {route.Points.Count}.");
return;
}
if (action == "hide")
{
hiddenPlayers.Add(player.userID);
SendReply(player, "Разметка маршрутов скрыта. /br on чтобы включить обратно.");
return;
}
if (action == "on" || action == "unhide")
{
hiddenPlayers.Remove(player.userID);
SendReply(player, "Разметка маршрутов включена.");
return;
}
if (action == "apply")
{
RouteState route = GetEditRoute(player);
if (route.Points.Count < 2)
{
SendReply(player, "Нужно минимум 2 точки.");
return;
}
BradleyAPC bradley = FindLookOrNearestBradley(player);
if (bradley == null)
{
SendReply(player, "Bradley не найден рядом или по взгляду.");
return;
}
ApplyRoute(bradley, route, player.userID);
SendReply(player, $"Маршрут назначен Bradley {bradley.net.ID.Value}. После финиша поедет заново.");
DrawRoute(player, route, 20f);
return;
}
if (action == "spawn")
{
RouteState route = GetEditRoute(player);
if (route.Points.Count < 2)
{
SendReply(player, "Нужно минимум 2 точки.");
return;
}
BradleyAPC bradley = SpawnBradley(route);
if (bradley == null)
{
SendReply(player, "Не удалось создать Bradley.");
return;
}
ApplyRoute(bradley, route, player.userID);
SendReply(player, $"Создан Bradley {bradley.net.ID.Value} и назначен маршрут. После финиша поедет заново.");
DrawRoute(player, route, 20f);
return;
}
if (action == "stop")
{
BradleyAPC bradley = FindLookOrNearestBradley(player);
if (bradley == null)
{
SendReply(player, "Bradley не найден рядом или по взгляду.");
return;
}
StopBradleyRoute(bradley);
SendReply(player, $"Маршрут Bradley {bradley.net.ID.Value} остановлен.");
return;
}
SendHelp(player);
}
private void LoadData()
{
editRoutes.Clear();
StoredData data;
try
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(DataPath);
}
catch
{
data = new StoredData();
}
if (data?.Routes == null) return;
foreach (KeyValuePair<ulong, StoredRoute> pair in data.Routes)
{
if (pair.Value == null || pair.Value.Points == null) continue;
RouteState route = new RouteState { Loop = pair.Value.Loop, OwnerId = pair.Key };
for (int i = 0; i < pair.Value.Points.Count; i++)
{
StoredPoint point = pair.Value.Points[i];
if (point == null) continue;
route.Points.Add(new Vector3(point.X, point.Y, point.Z));
}
editRoutes[pair.Key] = route;
}
}
private void SaveData()
{
StoredData data = new StoredData();
foreach (KeyValuePair<ulong, RouteState> pair in editRoutes)
{
if (pair.Value == null) continue;
StoredRoute route = new StoredRoute { Loop = pair.Value.Loop };
for (int i = 0; i < pair.Value.Points.Count; i++)
{
Vector3 point = pair.Value.Points[i];
route.Points.Add(new StoredPoint { X = point.x, Y = point.y, Z = point.z });
}
data.Routes[pair.Key] = route;
}
Interface.Oxide.DataFileSystem.WriteObject(DataPath, data);
}
private RouteState GetEditRoute(BasePlayer player)
{
RouteState route;
if (editRoutes.TryGetValue(player.userID, out route)) return route;
route = new RouteState { OwnerId = player.userID };
editRoutes[player.userID] = route;
return route;
}
private bool HasArg(string[] args, string value)
{
if (args == null) return false;
for (int i = 0; i < args.Length; i++)
{
if (args[i].ToLower() == value) return true;
}
return false;
}
private bool TryGetPoint(BasePlayer player, out Vector3 point)
{
if (Physics.Raycast(player.eyes.HeadRay(), out RaycastHit hit, 500f, Mask, QueryTriggerInteraction.Ignore))
{
point = hit.point;
return true;
}
point = Vector3.zero;
return false;
}
private BradleyAPC FindLookOrNearestBradley(BasePlayer player)
{
if (Physics.Raycast(player.eyes.HeadRay(), out RaycastHit hit, 200f, Mask, QueryTriggerInteraction.Ignore))
{
BradleyAPC looked = hit.collider != null ? hit.collider.GetComponentInParent<BradleyAPC>() : null;
if (looked != null && looked.IsAlive()) return looked;
}
BradleyAPC nearest = null;
float nearestDistance = 80f * 80f;
foreach (BaseNetworkable networkable in BaseNetworkable.serverEntities)
{
BradleyAPC bradley = networkable as BradleyAPC;
if (bradley == null || !bradley.IsAlive()) continue;
float distance = (bradley.transform.position - player.transform.position).sqrMagnitude;
if (distance >= nearestDistance) continue;
nearestDistance = distance;
nearest = bradley;
}
return nearest;
}
private BradleyAPC SpawnBradley(RouteState route)
{
Vector3 first = route.Points[0] + Vector3.up * 1f;
Vector3 second = route.Points[1] - route.Points[0];
second.y = 0f;
Quaternion rotation = second.sqrMagnitude > 0.1f ? Quaternion.LookRotation(second.normalized, Vector3.up) : Quaternion.identity;
BaseEntity entity = GameManager.server.CreateEntity(BradleyPrefab, first, rotation);
if (entity == null) return null;
BradleyAPC bradley = entity.GetComponent<BradleyAPC>();
if (bradley == null)
{
entity.Kill();
return null;
}
bradley.RoadSpawned = true;
entity.Spawn();
return bradley;
}
private void ApplyRoute(BradleyAPC bradley, RouteState route, ulong ownerId)
{
SetBradleyPath(bradley, route);
RouteState active = new RouteState { Loop = route.Loop, OwnerId = ownerId };
for (int i = 0; i < route.Points.Count; i++)
{
active.Points.Add(route.Points[i]);
}
controlledBradleys[bradley.net.ID.Value] = active;
}
private void SetBradleyPath(BradleyAPC bradley, RouteState route)
{
if (bradley.currentPath == null) bradley.currentPath = new List<Vector3>();
bradley.currentPath.Clear();
for (int i = 0; i < route.Points.Count; i++)
{
bradley.currentPath.Add(route.Points[i] + Vector3.up * 1f);
}
bradley.currentPathIndex = -1;
bradley.pathLooping = route.Loop;
bradley.finalDestination = bradley.currentPath[bradley.currentPath.Count - 1];
bradley.SetDestination(bradley.currentPath[0]);
bradley.DoAI = true;
}
private void StopBradleyRoute(BradleyAPC bradley)
{
if (bradley.currentPath != null) bradley.currentPath.Clear();
bradley.currentPathIndex = -1;
bradley.pathLooping = false;
bradley.finalDestination = bradley.transform.position;
bradley.SetDestination(bradley.transform.position);
if (bradley.net != null) controlledBradleys.Remove(bradley.net.ID.Value);
}
private bool IsControlled(BradleyAPC bradley)
{
return bradley?.net != null && controlledBradleys.ContainsKey(bradley.net.ID.Value);
}
private void TickControlledRoutes()
{
removeControlled.Clear();
foreach (KeyValuePair<ulong, RouteState> pair in controlledBradleys)
{
BradleyAPC bradley = BaseNetworkable.serverEntities.Find(new NetworkableId(pair.Key)) as BradleyAPC;
if (bradley == null || !bradley.IsAlive() || pair.Value == null || pair.Value.Points.Count < 2)
{
removeControlled.Add(pair.Key);
continue;
}
if (!bradley.HasPath() || bradley.PathComplete())
{
SetBradleyPath(bradley, pair.Value);
}
}
for (int i = 0; i < removeControlled.Count; i++)
{
controlledBradleys.Remove(removeControlled[i]);
}
}
private void DrawAllRoutes()
{
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsAdmin) continue;
if (hiddenPlayers.Contains(player.userID)) continue;
RouteState editRoute;
if (editRoutes.TryGetValue(player.userID, out editRoute)) DrawRoute(player, editRoute, 2.2f);
}
}
private void DrawRoute(BasePlayer player, RouteState route, float duration)
{
if (route == null || route.Points.Count == 0) return;
for (int i = 0; i < route.Points.Count; i++)
{
Vector3 point = route.Points[i] + Vector3.up * 0.35f;
Color color = i == 0 ? Color.green : i == route.Points.Count - 1 && !route.Loop ? Color.red : Color.yellow;
UnityEngine.DDraw.Sphere(player, point, 1.2f, color, duration, false, false);
UnityEngine.DDraw.Text(player, point + Vector3.up * 1.4f, (i + 1).ToString(), Color.white, duration, false, false, 1.2f);
if (i + 1 < route.Points.Count)
{
UnityEngine.DDraw.Line(player, point, route.Points[i + 1] + Vector3.up * 0.35f, Color.cyan, duration, false, false);
}
}
if (route.Loop && route.Points.Count > 2)
{
UnityEngine.DDraw.Line(player, route.Points[route.Points.Count - 1] + Vector3.up * 0.35f, route.Points[0] + Vector3.up * 0.35f, Color.cyan, duration, false, false);
}
}
private string Format(Vector3 point)
{
return $"{point.x:0.0} {point.y:0.0} {point.z:0.0}";
}
private void SendHelp(BasePlayer player)
{
SendReply(player, "Команды FBradleyRoute:");
SendReply(player, "/br start - очистить текущие точки и начать новый маршрут.");
SendReply(player, "/br start loop - начать новый кольцевой маршрут.");
SendReply(player, "/br add - добавить точку туда, куда смотришь.");
SendReply(player, "/br undo - удалить последнюю добавленную точку.");
SendReply(player, "/br show - показать сохранённые точки, сферы, номера и линии.");
SendReply(player, "/br hide - скрыть постоянную разметку маршрутов.");
SendReply(player, "/br on - включить постоянную разметку обратно.");
SendReply(player, "/br apply - назначить маршрут ближайшему Bradley или Bradley по взгляду.");
SendReply(player, "/br spawn - создать Bradley на первой точке, отправить по маршруту и повторять после финиша.");
SendReply(player, "/br stop - остановить маршрут ближайшего Bradley или Bradley по взгляду.");
SendReply(player, "/br clear - удалить твой сохранённый маршрут из data-файла.");
SendReply(player, "/br loop - сделать текущий маршрут кольцевым.");
SendReply(player, "/br once - сделать текущий маршрут обычным старт-финиш.");
SendReply(player, "/bradleyroute - полный алиас команды /br.");
}
[HookMethod("SpawnRouteBradley")]
public object SpawnRouteBradley()
{
RouteState route = null;
foreach (var pair in editRoutes)
{
if (pair.Value != null && pair.Value.Points.Count >= 2)
{
route = pair.Value;
break;
}
}
if (route == null)
{
foreach (var pair in controlledBradleys)
{
if (pair.Value != null && pair.Value.Points.Count >= 2)
{
route = pair.Value;
break;
}
}
}
if (route == null) return null;
foreach (BaseNetworkable networkable in BaseNetworkable.serverEntities)
{
BradleyAPC existing = networkable as BradleyAPC;
if (existing != null && existing.IsAlive()) return null;
}
BradleyAPC bradley = SpawnBradley(route);
if (bradley == null) return null;
ApplyRoute(bradley, route, 0);
return bradley.net.ID.Value;
}
}