C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using static BaseEntity;
using static TrainCar;
using static TrainEngine;
using static TrainTrackSpline;
namespace Oxide.Plugins {
[Info("Automated Workcarts", "WhiteThunder", "0.35.0")]
[Description("Automates workcarts with NPC conductors.")]
internal class AutomatedWorkcarts : CovalencePlugin {
#region Fields
[PluginReference]
private Plugin CargoTrainEvent;
private const string PermissionToggle = "automatedworkcarts.toggle";
private const string PermissionManageTriggers = "automatedworkcarts.managetriggers";
private const string ShopkeeperPrefab = "assets/prefabs/npc/bandit/shopkeepers/bandit_shopkeeper.prefab";
private const string GenericMapMarkerPrefab = "assets/prefabs/tools/map/genericradiusmarker.prefab";
private const string VendingMapMarkerPrefab = "assets/prefabs/deployable/vendingmachine/vending_mapmarker.prefab";
private const string ExplosionMapMakerPrefab = "assets/prefabs/tools/map/explosionmarker.prefab";
private const string CrateMarkerPrefab = "assets/prefabs/tools/map/cratemarker.prefab";
private const string BradleyExplosionEffectPrefab = "assets/prefabs/npc/m2bradley/effects/bradley_explosion.prefab";
private static readonly FieldInfo TrainCouplingIsValidField = typeof(TrainCoupling).GetField("isValid", BindingFlags.NonPublic | BindingFlags.Instance) ?? typeof(TrainCoupling).GetField("isValid", BindingFlags.Public | BindingFlags.Instance);
private readonly object False = false;
private static readonly Regex IdRegex = new("\\$id", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly BasePlayer[] _playerQueryResults = new BasePlayer[64];
private Configuration _config;
private StoredPluginData _data;
private StoredTunnelData _tunnelData;
private StoredMapData _mapData;
private readonly SpawnedTrainCarTracker _spawnedTrainCarTracker = new();
private readonly DisableSpawnPointManager _disableSpawnPointManager = new();
private readonly TriggerManager _triggerManager;
private readonly TrainManager _trainManager;
private readonly RouteManager _routeManager;
private readonly ColorMarkerUpdateManager _colorMarkerUpdateManager;
private Coroutine _startupCoroutine;
private Timer _showStatesTimer;
public AutomatedWorkcarts() {
_trainManager = new TrainManager(this, _spawnedTrainCarTracker);
_triggerManager = new TriggerManager(this, _trainManager);
_routeManager = new RouteManager(this);
_colorMarkerUpdateManager = new ColorMarkerUpdateManager(this);
}
#endregion
#region Hooks
private void Init() {
_config.Init();
_data = StoredPluginData.Load();
_tunnelData = StoredTunnelData.Load();
permission.RegisterPermission(PermissionToggle, this);
permission.RegisterPermission(PermissionManageTriggers, this);
if (!_config.MapMarkers.AnyColorsEnabled) {
Unsubscribe(nameof(OnPlayerConnected));
}
}
private void OnServerInitialized() {
if (!_config.UsingDefaults) {
var addedPrefabs = _config.PopulateConfig();
if (addedPrefabs.Count > 0) {
LogWarning($"Discovered and added {addedPrefabs.Count} train engine prefabs to Configuration.\n - {string.Join("\n - ", addedPrefabs)}");
SaveConfig();
}
}
_config.OnServerInitialized();
_mapData = StoredMapData.Load();
_startupCoroutine = ServerMgr.Instance.StartCoroutine(new TrackedCoroutine(this).WithEnumerator(DoStartupRoutine()));
}
private void Unload() {
if (_startupCoroutine != null) {
ServerMgr.Instance.StopCoroutine(_startupCoroutine);
}
OnServerSave();
_triggerManager.DestroyAll();
_trainManager.Unload();
_disableSpawnPointManager.Unload();
_routeManager.Unload();
_colorMarkerUpdateManager.Unload();
}
private void OnServerSave() {
if (_trainManager.UpdateTrainEngineData()) {
_data.Save();
}
else {
_data.SaveIfDirty();
}
}
private void OnNewSave() {
_data = StoredPluginData.Clear();
}
private void OnPlayerConnected(BasePlayer player) {
if (player.IsReceivingSnapshot) {
timer.Once(1f, () => OnPlayerConnected(player));
return;
}
_colorMarkerUpdateManager.Restart();
}
private object OnTrainCarUncouple(TrainCar trainCar, BasePlayer player) {
// Disallow uncoupling train cars from automated trains.return _trainManager.HasTrainController(trainCar)
? False : null;
}
#endregion
#region Commands
[Command("aw.toggle")]
private void CommandAutomateTrain(IPlayer player, string cmd, string[] args) {
if (player.IsServer || !VerifyPermission(player, PermissionToggle)) return;
var basePlayer = player.Object as BasePlayer;
var trainCar = GetTrainCarWhereAiming(basePlayer);
if (trainCar == null) {
ReplyToPlayer(player, Lang.ErrorNoWorkcartFound);
return;
}
var trainController = _trainManager.GetTrainController(trainCar);
if (trainController == null) {
var leadTrainEngine = GetLeadTrainEngine(trainCar);
if (leadTrainEngine == null) {
ReplyToPlayer(player, Lang.ErrorNoWorkcart);
return;
}
if (IsTrainOwned(trainCar)) {
ReplyToPlayer(player, Lang.ErrorWorkcartOwned);
return;
}
if (!_trainManager.CanHaveMoreConductors()) {
ReplyToPlayer(player, Lang.ErrorMaxConductors, _trainManager.CountedConductors, _config.MaxConductors);
return;
}
TrainEngineData trainEngineData = null;
if (args.Length > 0) {
var routeName = GetRouteNameFromArg(args[0], requirePrefix : false);
if (!string.IsNullOrWhiteSpace(routeName)) {
trainEngineData = new TrainEngineData {
Route = routeName
}
;
}
}
if (_trainManager.TryCreateTrainController(leadTrainEngine, trainEngineData : trainEngineData)) {
var baseMessage = trainEngineData != null ? GetMessage(player, Lang.ToggleOnWithRouteSuccess, trainEngineData.Route) : GetMessage(player, Lang.ToggleOnSuccess);
player.Reply(baseMessage + " " + GetConductorCountMessage(player));
if (player.HasPermission(PermissionManageTriggers)) {
if (trainEngineData?.Route != null) {
_triggerManager.SetPlayerDisplayedRoute(basePlayer, trainEngineData.Route);
}
_triggerManager.ShowAllRepeatedly(basePlayer);
}
}
else {
ReplyToPlayer(player, Lang.ErrorAutomateBlocked);
}
}
else {
_trainManager.KillTrainController(trainCar);
player.Reply(GetMessage(player, Lang.ToggleOffSuccess) + " " + GetConductorCountMessage(player));
}
}
[Command("aw.resetall")]
private void CommandResetTrains(IPlayer player, string cmd, string[] args) {
if (!player.IsServer && !VerifyPermission(player, PermissionToggle)) return;
var trainCount = _trainManager.ResetAll();
ReplyToPlayer(player, Lang.ResetAllSuccess, trainCount);
}
[Command("aw.addtrigger", "awt.add")]
private void CommandAddTrigger(IPlayer player, string cmd, string[] args) {
if (player.IsServer || !VerifyPermission(player, PermissionManageTriggers)) return;
if (!_config.EnableMapTriggers) {
ReplyToPlayer(player, Lang.ErrorMapTriggersDisabled);
return;
}
if (!VerifyAimingAtTrackPosition(player, out var trackPosition)) return;
var triggerData = new TriggerData {
Position = trackPosition
}
;
AddTriggerShared(player, cmd, args, triggerData);
}
[Command("aw.addtunneltrigger", "awt.addt")]
private void CommandAddTunnelTrigger(IPlayer player, string cmd, string[] args) {
if (player.IsServer || !VerifyPermission(player, PermissionManageTriggers)) return;
if (!VerifyAimingAtTrackPosition(player, out var trackPosition) || !VerifySupportedNearbyTrainTunnel(player, trackPosition, out var dungeonCellWrapper)) return;
if (!_config.IsTunnelTypeEnabled(dungeonCellWrapper.TunnelType)) {
ReplyToPlayer(player, Lang.ErrorTunnelTypeDisabled, dungeonCellWrapper.TunnelType);
return;
}
var triggerData = new TriggerData {
TunnelType = dungeonCellWrapper.TunnelType.ToString(), Position = dungeonCellWrapper.InverseTransformPoint(trackPosition),
}
;
AddTriggerShared(player, cmd, args, triggerData, dungeonCellWrapper);
}
private void AddTriggerShared(IPlayer player, string cmd, string[] args, TriggerData triggerData, DungeonCellWrapper dungeonCellWrapper = null) {
foreach (var arg in args) {
if (!VerifyValidArgAndModifyTrigger(player, cmd, arg, triggerData, Lang.AddTriggerSyntax)) return;
}
if (!triggerData.IsSpawner && !triggerData.AddConductor && !triggerData.Destroy && triggerData.GetTrackSelectionInstruction() == null && triggerData.GetSpeedInstruction() == null && triggerData.GetDirectionInstruction() == null) {
triggerData.Speed = EngineSpeeds.Zero.ToString();
}
var basePlayer = player.Object as BasePlayer;
if (triggerData.IsSpawner) {
var rotation = Quaternion.Euler(basePlayer.viewAngles);
if (dungeonCellWrapper != null) {
rotation *= Quaternion.Inverse(dungeonCellWrapper.Rotation);
}
triggerData.RotationAngle = rotation.eulerAngles.y % 360;
}
_triggerManager.AddTrigger(triggerData);
if (triggerData.Route != null) {
_triggerManager.SetPlayerDisplayedRoute(basePlayer, triggerData.Route);
}
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.AddTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.updatetrigger", "awt.update")]
private void CommandUpdateTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.UpdateTriggerSyntax, out var triggerData, out var optionArgs)) return;
if (optionArgs.Length == 0) {
ReplyToPlayer(player, Lang.UpdateTriggerSyntax, cmd, GetTriggerOptions(player));
return;
}
var newTriggerData = triggerData.Clone();
foreach (var arg in optionArgs) {
if (!VerifyValidArgAndModifyTrigger(player, cmd, arg, newTriggerData, Lang.UpdateTriggerSyntax)) return;
}
_triggerManager.UpdateTrigger(triggerData, newTriggerData);
var basePlayer = player.Object as BasePlayer;
if (triggerData.Route != null) {
_triggerManager.SetPlayerDisplayedRoute(basePlayer, triggerData.Route);
}
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.replacetrigger", "awt.replace")]
private void CommandReplaceTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.UpdateTriggerSyntax, out var triggerData, out var optionArgs)) return;
if (optionArgs.Length == 0) {
ReplyToPlayer(player, Lang.UpdateTriggerSyntax, cmd, GetTriggerOptions(player));
return;
}
var newTriggerData = new TriggerData();
foreach (var arg in optionArgs) {
if (!VerifyValidArgAndModifyTrigger(player, cmd, arg, newTriggerData, Lang.UpdateTriggerSyntax)) return;
}
_triggerManager.UpdateTrigger(triggerData, newTriggerData);
var basePlayer = player.Object as BasePlayer;
if (triggerData.Route != null) {
_triggerManager.SetPlayerDisplayedRoute(basePlayer, triggerData.Route);
}
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.enabletrigger", "awt.enable")]
private void CommandEnableTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.SimpleTriggerSyntax, out var triggerData, out _)) return;
var newTriggerData = triggerData.Clone();
newTriggerData.Enabled = true;
_triggerManager.UpdateTrigger(triggerData, newTriggerData);
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.disabletrigger", "awt.disable")]
private void CommandDisableTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.SimpleTriggerSyntax, out var triggerData, out _)) return;
var newTriggerData = triggerData.Clone();
newTriggerData.Enabled = false;
_triggerManager.UpdateTrigger(triggerData, newTriggerData);
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.movetrigger", "awt.move")]
private void CommandMoveTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.SimpleTriggerSyntax, out var triggerData, out _)) return;
if (!VerifyAimingAtTrackPosition(player, out var trackPosition)) return;
if (triggerData.TriggerType == TrainTriggerType.Tunnel) {
if (!VerifySupportedNearbyTrainTunnel(player, trackPosition, out var dungeonCellWrapper)) return;
if (dungeonCellWrapper.TunnelType != triggerData.GetTunnelType()) {
ReplyToPlayer(player, Lang.ErrorUnsupportedTunnel);
return;
}
trackPosition = dungeonCellWrapper.InverseTransformPoint(trackPosition);
}
_triggerManager.MoveTrigger(triggerData, trackPosition);
var basePlayer = player.Object as BasePlayer;
if (triggerData.Route != null) {
_triggerManager.SetPlayerDisplayedRoute(basePlayer, triggerData.Route);
}
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.MoveTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.removetrigger", "awt.remove")]
private void CommandRemoveTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.SimpleTriggerSyntax, out var triggerData, out _)) return;
_triggerManager.RemoveTrigger(triggerData);
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.RemoveTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.rotatetrigger", "awt.rotate")]
private void CommandSetTriggerRotation(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.SimpleTriggerSyntax, out var triggerData, out _)) return;
var basePlayer = player.Object as BasePlayer;
var playerPosition = basePlayer.transform.position;
var triggerInstance = _triggerManager.FindNearestTrigger(playerPosition, triggerData);
var rotation = Quaternion.Euler(basePlayer.viewAngles);
var needsRespawn = false;
if (triggerInstance.Spline != null) {
rotation = GetSplineTangentRotation(triggerInstance.Spline, triggerInstance.DistanceOnSpline, rotation);
if (Vector3.Dot(triggerInstance.SpawnRotation * Vector3.forward, rotation * Vector3.forward) < 0) {
needsRespawn = true;
}
}
if (triggerInstance is TunnelTriggerInstance tunnelTriggerInstance) {
rotation *= Quaternion.Inverse(tunnelTriggerInstance.DungeonCellWrapper.Rotation);
}
_triggerManager.RotateTrigger(triggerData, rotation.eulerAngles.y % 360);
if (needsRespawn) {
_triggerManager.RespawnTrigger(triggerData);
}
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.RotateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.respawntrigger", "awt.respawn")]
private void CommandRespawnTrigger(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.SimpleTriggerSyntax, out var triggerData, out _)) return;
if (!triggerData.IsSpawner) {
ReplyToPlayer(player, Lang.ErrorRequiresSpawnTrigger);
return;
}
if (!triggerData.Enabled) {
ReplyToPlayer(player, Lang.ErrorTriggerDisabled);
return;
}
_triggerManager.RespawnTrigger(triggerData);
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
}
[Command("aw.addtriggercommand", "awt.addcommand", "awt.addcmd")]
private void CommandAddCommand(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.AddCommandSyntax, out var triggerData, out var optionArgs)) return;
if (optionArgs.Length < 1) {
ReplyToPlayer(player, Lang.AddCommandSyntax, cmd);
return;
}
var quotedCommands = optionArgs.Select(command => command.Contains(" ") ? $"\"{command}\"" : command).ToArray();
_triggerManager.AddTriggerCommand(triggerData, string.Join(" ", quotedCommands));
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.removetriggercommand", "awt.removecommand", "awt.removecmd")]
private void CommandRemoveCommand(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.RemoveCommandSyntax, out var triggerData, out var optionArgs)) return;
if (optionArgs.Length < 1 || !int.TryParse(optionArgs[0], out var commandIndex)) {
ReplyToPlayer(player, Lang.RemoveCommandSyntax, cmd);
return;
}
if (commandIndex < 1 || commandIndex > triggerData.Commands.Count) {
ReplyToPlayer(player, Lang.RemoveCommandErrorIndex, commandIndex);
return;
}
_triggerManager.RemoveTriggerCommand(triggerData, commandIndex - 1);
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.settriggertrain", "awt.train")]
private void CommandTriggerTrain(IPlayer player, string cmd, string[] args) {
if (!VerifyCanModifyTrigger(player, cmd, args, Lang.RemoveCommandSyntax, out var triggerData, out var optionArgs)) return;
var trainCarAliases = new List<string>();
foreach (var arg in optionArgs) {
var trainCarPrefab = TrainCarPrefab.FindPrefab(arg);
if (trainCarPrefab == null) {
ReplyToPlayer(player, Lang.ErrorUnrecognizedTrainCar, arg);
return;
}
trainCarAliases.Add(trainCarPrefab.TrainCarAlias);
}
var newTriggerData = triggerData.Clone();
newTriggerData.TrainCars = trainCarAliases.ToArray();
_triggerManager.UpdateTrigger(triggerData, newTriggerData);
var basePlayer = player.Object as BasePlayer;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, Lang.UpdateTriggerSuccess, GetTriggerPrefix(player, triggerData), triggerData.Id);
}
[Command("aw.showtriggers", "awt.show")]
private void CommandShowTriggers(IPlayer player, string cmd, string[] args) {
if (player.IsServer || !VerifyPermission(player, PermissionManageTriggers) || !VerifyAnyTriggers(player)) return;
var duration = 60;
string routeName = null;
foreach (var arg in args) {
if (duration == 60) {
if (int.TryParse(arg, out var argIntValue)) {
duration = argIntValue;
continue;
}
}
if (routeName == null) {
var routeNameArg = GetRouteNameFromArg(arg, requirePrefix : false);
if (!string.IsNullOrWhiteSpace(routeNameArg)) routeName = routeNameArg;
}
}
var basePlayer = player.Object as BasePlayer;
_triggerManager.SetPlayerDisplayedRoute(basePlayer, routeName);
_triggerManager.ShowAllRepeatedly(basePlayer, duration);
if (routeName != null) {
ReplyToPlayer(player, Lang.ShowTriggersWithRouteSuccess, routeName, FormatTime(duration));
}
else {
ReplyToPlayer(player, Lang.ShowTriggersSuccess, FormatTime(duration));
}
}
// Debugging command, usable by one player at a time.[Command("aw.showstates")]
private void CommandShowStates(IPlayer player, string cmd, string[] args) {
if (player.IsServer || !VerifyPermission(player, PermissionManageTriggers) || !VerifyAnyTriggers(player)) return;
if (args.Length < 1 || !int.TryParse(args[0], out var duration)) {
duration = 60;
}
var drawDuration = 0.5f;
var maxDistanceSquared = Mathf.Pow(_config.TriggerDisplayDistance, 2);
var basePlayer = player.Object as BasePlayer;
_showStatesTimer?.Destroy();
_showStatesTimer = timer.Repeat(drawDuration, Mathf.RoundToInt(duration / drawDuration), () => {
if (basePlayer == null || basePlayer.IsDestroyed || !basePlayer.IsConnected) {
_showStatesTimer.Destroy();
_showStatesTimer = null;
return;
}
_trainManager.ShowNearbyTrainStates(basePlayer, maxDistanceSquared, drawDuration);
}
);
}
#endregion
#region API
[HookMethod(nameof(API_AutomateWorkcart))]
public bool API_AutomateWorkcart(TrainEngine trainEngine) {
return _trainManager.HasTrainController(trainEngine) || _trainManager.TryCreateTrainController(trainEngine);
}
[HookMethod(nameof(API_StopAutomatingWorkcart))]
public void API_StopAutomatingWorkcart(TrainEngine trainEngine) {
_trainManager.KillTrainController(trainEngine);
}
[HookMethod(nameof(API_IsWorkcartAutomated))]
public bool API_IsWorkcartAutomated(TrainEngine trainEngine) {
return _trainManager.HasTrainController(trainEngine);
}
[HookMethod(nameof(API_GetAutomatedWorkcarts))]
public TrainEngine[] API_GetAutomatedWorkcarts() {
return _trainManager.GetAutomatedTrainEngines();
}
#endregion
#region Exposed Hooks
private static class ExposedHooks {
public static object OnWorkcartAutomationStart(TrainEngine trainEngine) {
return Interface.CallHook("OnWorkcartAutomationStart", trainEngine);
}
public static void OnWorkcartAutomationStarted(TrainEngine trainEngine) {
Interface.CallHook("OnWorkcartAutomationStarted", trainEngine);
}
public static void OnWorkcartAutomationStopped(TrainEngine trainEngine) {
Interface.CallHook("OnWorkcartAutomationStopped", trainEngine);
}
}
#endregion
#region Dependencies
private bool IsCargoTrain(TrainEngine trainEngine) {
return CargoTrainEvent?.Call("IsTrainSpecial", trainEngine.net.ID) is true;
}
#endregion
#region Helper Methods - Static
private static List<T> FindPrefabsOfType<T>() where T : BaseEntity {
var prefabList = new List<T>();
foreach (var assetPath in GameManifest.Current.entities) {
var entity = GameManager.server.FindPrefab(assetPath)?.GetComponent<T>();
if (entity == null) continue;
prefabList.Add(entity);
}
return prefabList;
}
#endregion
#region Helper Methods - Command Checks
private bool VerifyPermission(IPlayer player, string permissionName) {
if (player.HasPermission(permissionName)) return true;
ReplyToPlayer(player, Lang.ErrorNoPermission);
return false;
}
private bool VerifyAnyTriggers(IPlayer player) {
if (_mapData.MapTriggers.Count > 0 || _tunnelData.TunnelTriggers.Count > 0) return true;
ReplyToPlayer(player, Lang.ErrorNoTriggers);
return false;
}
private bool VerifyTriggerExists(IPlayer player, int triggerId, TrainTriggerType triggerType, out TriggerData triggerData) {
triggerData = _triggerManager.FindTrigger(triggerId, triggerType);
if (triggerData != null) return true;
_triggerManager.ShowAllRepeatedly(player.Object as BasePlayer);
ReplyToPlayer(player, Lang.ErrorTriggerNotFound, GetTriggerPrefix(player, triggerType), triggerId);
return false;
}
private bool VerifyAimingAtTrackPosition(IPlayer player, out Vector3 trackPosition) {
if (TryGetTrackPosition(player.Object as BasePlayer, out trackPosition)) return true;
ReplyToPlayer(player, Lang.ErrorNoTrackFound);
return false;
}
private bool IsTriggerArg(IPlayer player, string arg, out int triggerId, out TrainTriggerType triggerType) {
triggerType = TrainTriggerType.Map;
triggerId = 0;
if (arg.StartsWith("#")) {
arg = arg.Substring(1);
}
if (arg.Length <= 1) return false;
var triggerPrefix = arg.Substring(0, 1).ToLower();
var triggerIdString = arg.Substring(1).ToLower();
if (!int.TryParse(triggerIdString, out triggerId)) return false;
if (triggerPrefix == GetTriggerPrefix(player, TrainTriggerType.Tunnel).ToLower()) {
triggerType = TrainTriggerType.Tunnel;
return true;
}
if (triggerPrefix == GetTriggerPrefix(player, TrainTriggerType.Map).ToLower()) {
triggerType = TrainTriggerType.Map;
return true;
}
return false;
}
private bool VerifyTriggerWhereAiming(IPlayer player, string cmd, string[] args, string errorMessageName, out TriggerData triggerData, out string[] optionArgs) {
var basePlayer = player.Object as BasePlayer;
optionArgs = args;
triggerData = null;
triggerData = _triggerManager.FindNearestTriggerWhereAiming(basePlayer)?.TriggerData;
if (triggerData != null) return true;
_triggerManager.ShowAllRepeatedly(basePlayer);
ReplyToPlayer(player, errorMessageName, cmd, GetTriggerOptions(player));
return false;
}
private bool VerifyCanModifyTrigger(IPlayer player, string cmd, string[] args, string errorMessageName, out TriggerData triggerData, out string[] optionArgs) {
triggerData = null;
optionArgs = null;
if (!VerifyPermission(player, PermissionManageTriggers) || !VerifyAnyTriggers(player)) return false;
if (args.Length > 0 && IsTriggerArg(player, args[0], out var triggerId, out var triggerType)) {
optionArgs = args.Skip(1).ToArray();
return VerifyTriggerExists(player, triggerId, triggerType, out triggerData);
}
if (player.IsServer) {
// Server commands must specify a trigger id.ReplyToPlayer(player, errorMessageName, cmd, GetTriggerOptions(player));
return false;
}
return VerifyTriggerWhereAiming(player, cmd, args, errorMessageName, out triggerData, out optionArgs);
}
private bool VerifySupportedNearbyTrainTunnel(IPlayer player, Vector3 trackPosition, out DungeonCellWrapper dungeonCellWrapper) {
dungeonCellWrapper = FindNearestDungeonCell(trackPosition);
if (dungeonCellWrapper == null || dungeonCellWrapper.TunnelType == TunnelType.Unsupported) {
ReplyToPlayer(player, Lang.ErrorUnsupportedTunnel);
return false;
}
return true;
}
private bool VerifyValidArgAndModifyTrigger(IPlayer player, string cmd, string arg, TriggerData triggerData, string errorMessageName) {
var argLower = arg.ToLower();
if (argLower is "start" or "conductor") {
triggerData.AddConductor = true;
return true;
}
if (argLower.StartsWith("brake")) {
triggerData.Brake = true;
return true;
}
if (argLower.StartsWith("destroy")) {
triggerData.Destroy = true;
return true;
}
if (argLower.StartsWith("enable")) {
triggerData.Enabled = true;
return true;
}
if (argLower.StartsWith("disable")) {
triggerData.Enabled = false;
return true;
}
var prefab = TrainCarPrefab.FindPrefab(argLower);
if (prefab != null) {
if (triggerData.TrainCars == null) {
triggerData.TrainCars = new[] {
prefab.TrainCarAlias
}
;
}
else {
var length = triggerData.TrainCars.Length;
Array.Resize(ref triggerData.TrainCars, length + 1);
triggerData.TrainCars[length] = prefab.TrainCarAlias;
}
return true;
}
if (arg[^1] == '%' && float.TryParse(arg[..^1], out var percentage)) {
triggerData.Chance = Mathf.Clamp(percentage / 100f, 0, 1);
return true;
}
if (float.TryParse(arg, out var stopDuration)) {
triggerData.StopDuration = stopDuration;
return true;
}
var routeName = GetRouteNameFromArg(arg, requirePrefix : true);
if (!string.IsNullOrWhiteSpace(routeName)) {
triggerData.Route = routeName;
return true;
}
if (Enum.TryParse(arg, true, out SpeedInstruction speedInstruction)) {
var speedString = speedInstruction.ToString();
// If zero speed is already set, assume this is the departure speed.if (triggerData.Speed == SpeedInstruction.Zero.ToString())
{
triggerData.DepartureSpeed = speedString;
}
else {
triggerData.Speed = speedString;
}
return true;
}
if (Enum.TryParse(arg, true, out DirectionInstruction directionInstruction)) {
triggerData.Direction = directionInstruction.ToString();
return true;
}
if (Enum.TryParse(arg, true, out TrackSelectionInstruction trackSelectionInstruction)) {
triggerData.TrackSelection = trackSelectionInstruction.ToString();
return true;
}
ReplyToPlayer(player, errorMessageName, cmd, GetTriggerOptions(player));
return false;
}
#endregion
#region Helper Methods - Coupling
private static void UpdateAllowedCouplings(TrainCar trainCar, bool allowFront, bool allowRear) {
var coupling = trainCar.coupling;
var frontCoupling = coupling.frontCoupling;
var rearCoupling = coupling.rearCoupling;
if (trainCar.frontCoupling == null || trainCar.rearCoupling == null) {
// Some train cars do not allow coupling, such as the classic workcart.return;
}
if (!allowFront && frontCoupling.IsCoupled) {
frontCoupling.Uncouple(reflect : true);
}
if (!allowRear && rearCoupling.IsCoupled) {
rearCoupling.Uncouple(reflect : true);
}
if (frontCoupling.isValid != allowFront) {
TrainCouplingIsValidField.SetValue(frontCoupling, allowFront);
}
if (rearCoupling.isValid != allowRear) {
TrainCouplingIsValidField.SetValue(rearCoupling, allowRear);
}
}
private static void DisableTrainCoupling(CompleteTrain completeTrain) {
var firstTrainCar = completeTrain.trainCars.FirstOrDefault();
var lastTrainCar = completeTrain.trainCars.LastOrDefault();
if (firstTrainCar == null || lastTrainCar == null) return;
UpdateAllowedCouplings(firstTrainCar, firstTrainCar.coupling.IsFrontCoupled, firstTrainCar.coupling.IsRearCoupled);
if (lastTrainCar != firstTrainCar) {
UpdateAllowedCouplings(lastTrainCar, lastTrainCar.coupling.IsFrontCoupled, lastTrainCar.coupling.IsRearCoupled);
}
}
private static void EnableTrainCoupling(CompleteTrain completeTrain) {
var firstTrainCar = completeTrain.trainCars.FirstOrDefault();
var lastTrainCar = completeTrain.trainCars.LastOrDefault();
if (firstTrainCar == null || lastTrainCar == null) return;
UpdateAllowedCouplings(firstTrainCar, allowFront : true, allowRear : true);
if (lastTrainCar != firstTrainCar) {
UpdateAllowedCouplings(lastTrainCar, allowFront : true, allowRear : true);
}
}
#endregion
#region Helper Methods
private static void LogError(string message) => Interface.Oxide.LogError($"[Automated Workcarts] {message}");
private static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Automated Workcarts] {message}");
private static int GetNextTriggerId(List<TriggerData> triggerList) {
var highestTriggerId = 0;
foreach (var triggerData in triggerList) {
highestTriggerId = Math.Max(highestTriggerId, triggerData.Id);
}
return highestTriggerId + 1;
}
private static string GetRouteNameFromArg(string routeName, bool requirePrefix = true) {
if (routeName.StartsWith("@")) return routeName[1..];
return requirePrefix ? null : routeName;
}
private static float GetThrottleFraction(EngineSpeeds throttle) {
switch (throttle) {
case EngineSpeeds.Rev_Hi : return -1;
case EngineSpeeds.Rev_Med : return -0.5f;
case EngineSpeeds.Rev_Lo : return -0.2f;
case EngineSpeeds.Fwd_Lo : return 0.2f;
case EngineSpeeds.Fwd_Med : return 0.5f;
case EngineSpeeds.Fwd_Hi : return 1;
default : return 0;
}
}
private static TrainEngine GetLeadTrainEngine(CompleteTrain completeTrain) {
if (completeTrain.PrimaryTrainCar == completeTrain.trainCars[0]) {
for (var i = 0; i < completeTrain.trainCars.Count; i++) {
var trainEngine = completeTrain.trainCars[i] as TrainEngine;
if ((object)trainEngine != null) return trainEngine;
}
}
else {
for (var i = completeTrain.trainCars.Count - 1; i >= 0; i--) {
var trainEngine = completeTrain.trainCars[i] as TrainEngine;
if ((object)trainEngine != null) return trainEngine;
}
}
return null;
}
private static TrainEngine GetLeadTrainEngine(TrainCar trainCar) {
var trainEngine = trainCar as TrainEngine;
if (trainEngine != null) return trainEngine;
return GetLeadTrainEngine(trainCar.completeTrain);
}
private static void DetermineTrainCarOrientations(TrainCar trainCar, Vector3 forward, TrainCar otherTrainCar, out TrainCar forwardTrainCar) {
var position = trainCar.transform.position;
var otherPosition = otherTrainCar.transform.position;
var forwardPosition = position + forward * 100f;
forwardTrainCar = trainCar;
if ((forwardPosition - position).sqrMagnitude > (forwardPosition - otherPosition).sqrMagnitude) {
forwardTrainCar = otherTrainCar;
}
}
private static Vector3 GetTrainCarForward(TrainCar trainCar) {
return trainCar.GetTrackSpeed() >= 0 ? trainCar.transform.forward : -trainCar.transform.forward;
}
private static void EnableInvincibility(TrainCar trainCar) {
trainCar.initialSpawnTime = float.MaxValue;
}
private static void DisableInvincibility(TrainCar trainCar) {
trainCar.initialSpawnTime = Time.time;
}
private static void EnableSavingRecursive(BaseEntity entity, bool enableSaving) {
entity.EnableSaving(enableSaving);
foreach (var child in entity.children) {
if (child is BasePlayer) continue;
EnableSavingRecursive(child, enableSaving);
}
}
private static TrainCar SpawnTrainCar(string prefabName, Vector3 position, Quaternion rotation) {
var trainCar = GameManager.server.CreateEntity(prefabName, position, rotation) as TrainCar;
if (trainCar == null) return null;
// Ensure the train car does not decay for some time.trainCar.lastDecayTick = Time.realtimeSinceStartup;
trainCar.limitNetworking = true;
trainCar.EnableSaving(false);
trainCar.Spawn();
if (trainCar.IsDestroyed) return null;
// Don't let the train kill itself if there isn't quite enough space.trainCar.CancelInvoke(trainCar.KillMessage);
trainCar.Invoke(() => EnableSavingRecursive(trainCar, false), 0);
return trainCar;
}
private static float GetSplineDistance(TrainTrackSpline spline, Vector3 position) {
spline.GetDistance(position, 1, out var distanceOnSpline);
return distanceOnSpline;
}
private static TrainCar AddTrainCar(TrainCar frontTrainCar, TrainCarPrefab frontTrainCarPrefab, TrainCarPrefab trainCarPrefab, TrackSelection trackSelection) {
var rearCouplingTransform = frontTrainCarPrefab.Reverse ? frontTrainCar.frontCoupling : frontTrainCar.rearCoupling;
if (rearCouplingTransform == null) return null;
var rearWheelPos = frontTrainCarPrefab.Reverse ? frontTrainCar.GetFrontWheelPos() : frontTrainCar.GetRearWheelPos();
var wheelToRearCouplingDistance = Math.Abs(rearWheelPos.z - rearCouplingTransform.position.z);
var rearSpline = frontTrainCarPrefab.Reverse ? frontTrainCar.FrontTrackSection : frontTrainCar.RearTrackSection;
var rearWheelDistanceOnSpline = GetSplineDistance(rearSpline, rearWheelPos);
var frontTrainCarForward = frontTrainCarPrefab.Reverse ? -frontTrainCar.transform.forward : frontTrainCar.transform.forward;
var askerIsForward = rearSpline.IsForward(frontTrainCarForward, rearWheelDistanceOnSpline);
var splineInfo = new SplineInfo {
Spline = rearSpline, Distance = rearWheelDistanceOnSpline, Ascending = !askerIsForward, IsForward = askerIsForward,
}
;
// Spawn the train slightly farther away so it has space.It will be moved forward after spawn.var spawnDistanceOffset = 2;
var finalDistance = wheelToRearCouplingDistance + GetTrainCarFrontCouplingOffsetZ(trainCarPrefab);
var finalPosition = GetPositionAlongTrack(splineInfo, finalDistance, trackSelection, out var finalSplineInfo);
var resultPosition = GetPositionAlongTrack(finalSplineInfo, spawnDistanceOffset, trackSelection, out var spawnSplineInfo);
var resultRotation = GetSplineTangentRotation(spawnSplineInfo.Spline, spawnSplineInfo.Distance, frontTrainCar.transform.rotation);
if (trainCarPrefab.Reverse != frontTrainCarPrefab.Reverse) {
resultRotation = Quaternion.LookRotation(resultRotation * -Vector3.forward);
}
// TODO: Fix issue where workcarts jump on start, when first two are both reverse
var rearTrainCar = SpawnTrainCar(trainCarPrefab.PrefabPath, resultPosition, resultRotation);
if (rearTrainCar != null) {
if (rearTrainCar.FrontTrackSection == null) {
rearTrainCar.Kill();
return null;
}
rearTrainCar.MoveFrontWheelsAlongTrackSpline(rearTrainCar.FrontTrackSection, rearTrainCar.FrontWheelSplineDist, spawnDistanceOffset, rearTrainCar.RearTrackSection != rearTrainCar.FrontTrackSection ? rearTrainCar.RearTrackSection : null, trackSelection );
rearTrainCar.transform.position = finalPosition;
var frontCoupling = frontTrainCarPrefab.Reverse ? frontTrainCar.coupling.frontCoupling : frontTrainCar.coupling.rearCoupling;
var rearCoupling = trainCarPrefab.Reverse ? rearTrainCar.coupling.rearCoupling : rearTrainCar.coupling.frontCoupling;
frontCoupling.TryCouple(rearCoupling, reflect : true);
}
return rearTrainCar;
}
private static float GetTrainCarFrontCouplingOffsetZ(TrainCarPrefab trainCarPrefab) {
var prefab = GameManager.server.FindPrefab(trainCarPrefab.PrefabPath)?.GetComponent<TrainCar>();
if (prefab == null) return 0;
return trainCarPrefab.Reverse ? prefab.rearCoupling.localPosition.z : prefab.frontCoupling.localPosition.z;
}
private static ConnectedTrackInfo GetAdjacentTrackInfo(TrainTrackSpline spline, TrackSelection selection, bool isAscending = true, bool askerIsForward = true) {
var trackOptions = isAscending ? spline.nextTracks : spline.prevTracks;
if (trackOptions.Count == 0) return null;
if (trackOptions.Count == 1) return trackOptions[0];
switch (selection) {
case TrackSelection.Left : return isAscending == askerIsForward ? trackOptions.FirstOrDefault() : trackOptions.LastOrDefault();
case TrackSelection.Right : return isAscending == askerIsForward ? trackOptions.LastOrDefault() : trackOptions.FirstOrDefault();
default : return trackOptions[isAscending ? spline.straightestNextIndex : spline.straightestPrevIndex];
}
}
private static Quaternion GetSplineTangentRotation(TrainTrackSpline spline, float distanceOnSpline, Quaternion approximateRotation) {
spline.GetPositionAndTangent(distanceOnSpline, approximateRotation * Vector3.forward, out var tangentDirection);
return Quaternion.LookRotation(tangentDirection);
}
private static int CompareVectors(Vector3 a, Vector3 b) {
var result = a.y.CompareTo(b.y);
if (result != 0) return result;
result = a.x.CompareTo(b.x);
if (result != 0) return result;
return a.z.CompareTo(b.z);
}
private IEnumerator DoStartupRoutine() {
if (_config.DisableDefaultTunnelWorkcartSpawnPoints) yield return _disableSpawnPointManager.DisableSpawnPointsRoutine();
yield return _triggerManager.CreateAll();
var foundTrainEngineIds = new HashSet<ulong>();
foreach (var entity in BaseNetworkable.serverEntities) {
var trainEngine = entity as TrainEngine;
if (trainEngine == null) continue;
var trainEngineData = _data.GetTrainEngineData(trainEngine.net.ID.Value);
if (trainEngineData != null) {
foundTrainEngineIds.Add(trainEngine.net.ID.Value
// ... [Code preview truncated due to length. Full source in attached file] ...