using System; using UnityEngine; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Assets.Scripts; using Assets.Scripts.Networks; using Assets.Scripts.Objects.Electrical; using Assets.Scripts.Objects.Motherboards; using BepInEx; using BepInEx.Configuration; using Cysharp.Threading.Tasks; using HarmonyLib; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; using RemoteControl.Message; using RemoteControl.Utils; using Swan; using GameDevice = Assets.Scripts.Objects.Pipes.Device; namespace RemoteControl { [BepInPlugin(pluginGuid, pluginName, pluginVersion)] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class RemoteControl : BepInEx.BaseUnityPlugin { // ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global public static ConfigEntry Port; // ReSharper disable once MemberCanBePrivate.Global public static ConfigEntry ListenOnAllInterfaces; private readonly Harmony _harmony = new Harmony(pluginGuid); public WebServer WebServer { get; private set; } private CancellationTokenSource _modLifecycle = new(); public const string pluginGuid = "com.thequux.stationeers.RemoteControl"; public const string pluginName = "RemoteControl"; public const string pluginVersion = "1.0"; public static void Log(string line) { Debug.Log("[" + pluginName + "]: " + line); } private void StartRC() { if (!_modLifecycle.IsCancellationRequested) { _modLifecycle.Cancel(); } _modLifecycle = new CancellationTokenSource(); Port = Config.Bind(new ConfigDefinition("Server", "Port"), 39125, new ConfigDescription("The port to listen on")); ListenOnAllInterfaces = Config.Bind( new ConfigDefinition("Server", "ListenOnAllInterfaces"), false, new ConfigDescription("If set, listen on all interfaces. Otherwise, listen only on localhost")); var subscriptionModule = new SubscriptionModule("/subscribe"); WebServer = new WebServer(o => o.WithUrlPrefix($"http://{(ListenOnAllInterfaces.Value ? "0.0.0.0" : "localhost")}:{Port.Value}/") .WithEmbedIOHttpListener() ) .WithWebApi("/api/v1", m => m.WithController()) .WithModule(subscriptionModule); WebServer.Start(_modLifecycle.Token); _harmony.PatchAll(); foreach (var patchedMethod in _harmony.GetPatchedMethods()) { Log($"Patched {patchedMethod.FullDescription()}"); } Log($"Patched {_harmony.GetPatchedMethods().Count()} methods total"); } public void OnLoaded(List prefabs) { // Start(); #if DEVELOPMENT_BUILD Debug.Log($"Loaded {prefabs.Count} prefabs"); #endif } public void OnUnloaded(List prefabs) { // Stop(); } public void StopRC() { _modLifecycle.Cancel(); _harmony.UnpatchSelf(); } public void Awake() { Log("Starting RemoteControl"); StartRC(); } private void OnDestroy() { Log("Tearing down RemoteControl"); StopRC(); } } internal class ApiController : WebApiController { private static readonly Dictionary LtByName = EnumCollections.LogicTypes.AsLookupDict(true); private static readonly Dictionary StByName = EnumCollections.LogicSlotTypes.AsLookupDict(true); private static Device GetDeviceJson(GameDevice device) => new() { ReferenceId = device.ReferenceId, Name = device.DisplayName, NameHash = device.GetNameHash(), PrefabHash = device.PrefabHash, PrefabName = device.PrefabName, LogicValues = EnumCollections.LogicTypes.Values.Where(ty => device.CanLogicRead(ty)) .ToDictionary(ty => ty, ty => device.GetLogicValue(ty)), Slots = Enumerable.Range(0, device.TotalSlots) .Select(slot => EnumCollections.LogicSlotTypes.Values.Where(sty => device.CanLogicRead(sty, slot)) .ToDictionary(ty => ty, sty=> device.GetLogicValue(sty, slot))) .ToList() }; private static Device? GetDeviceJson(long referenceId) { var dev = Referencable.Find(referenceId); return dev == null ? null : GetDeviceJson(dev); } [Route(HttpVerbs.Get, "/networks")] public Task> ListNetworks() => Task.FromResult(SubscriptionManager.GetProbes()); [Route(HttpVerbs.Get, "/networks/{networkId}")] public Task> ListDevices(string networkId) { // lock (SubscriptionManager.Lock) using (new LogTimer("ListDevices")) { var networks = new HashSet(); GameDevice.AllDevices.ForEach(dev => { if (dev is CableAnalyser analyser) { Debug.Log($"Found CA {analyser.DisplayName}: {analyser.ReferenceId}"); networks.Add(analyser.CableNetwork); } }); var devices = new Dictionary(); foreach (var network in networks) { foreach (var device in network.DeviceList) { RemoteControl.Log($"Found {device.PrefabName}: {device.DisplayName}({device.ReferenceId})"); if (devices.ContainsKey(device.ReferenceId)) continue; devices.Add(device.ReferenceId, GetDeviceJson(device)); } } return Task.FromResult>(devices); } } [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/logic/{varId}")] public async Task SetLogic(string networkId, long deviceId, string varId) { LogicType ltype; if (!LtByName.TryGetValue(varId.ToLowerInvariant(), out ltype)) { throw HttpException.NotFound(); } var value = await HttpContext.GetRequestDataAsync(); SubscriptionManager.SetLogic(networkId, deviceId, ltype, value); } [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/slot/{slotId}/{varId}")] public async Task SetSlot(string networkId, long deviceId, int slotId, string varId) { LogicSlotType ltype; if (!StByName.TryGetValue(varId.ToLowerInvariant(), out ltype)) { throw HttpException.NotFound(); } var value = await HttpContext.GetRequestDataAsync(); SubscriptionManager.SetSlot(networkId, deviceId, slotId, ltype, value); } [Route(HttpVerbs.Patch, "/networks/{networkId}/device/{deviceId}")] public async Task PatchDevice(string networkId, long deviceId) { var values = await HttpContext.GetRequestDataAsync>(); if (values.Keys.Any(key => !LtByName.ContainsKey(key.ToLowerInvariant()))) { throw HttpException.BadRequest(); } foreach (var keyValuePair in values) { SubscriptionManager.SetLogic(networkId, deviceId, LtByName[keyValuePair.Key.ToLowerInvariant()], keyValuePair.Value); } } [Route(HttpVerbs.Get, "/networks/{networkId}/device/{deviceId}/code")] public async UniTask GetCode(string networkId, long deviceId) { await UniTask.SwitchToMainThread(HttpContext.CancellationToken); string source; // lock (SubscriptionManager.Lock) { var device = SubscriptionManager.GetDevice(networkId, deviceId) ?? throw HttpException.NotFound(); if (device is not ICircuitHolder) { throw HttpException.NotFound(); } source = ((ICircuitHolder)device).GetSourceCode(); } return source; } } }