using System; using UnityEngine; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Text; 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 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 static WebServer WebServer { get; private set; } internal static SubscriptionModule Subscribers { get; } = new SubscriptionModule("/subscribe"); 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() { 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")); // No need to restart the game after changing settings :-) Port.SettingChanged += WebserverSettingsChanged; ListenOnAllInterfaces.SettingChanged += WebserverSettingsChanged; // Subscribers = new SubscriptionModule("/subscribe"); RestartWebserver(); _harmony.PatchAll(); foreach (var patchedMethod in _harmony.GetPatchedMethods()) { Log($"Patched {patchedMethod.FullDescription()}"); } Log($"Patched {_harmony.GetPatchedMethods().Count()} methods total"); } private void WebserverSettingsChanged(object _1, EventArgs _2) { RestartWebserver(); } private void RestartWebserver() { if (!_modLifecycle.IsCancellationRequested) { _modLifecycle.Cancel(); } var URL = $"http://{(ListenOnAllInterfaces.Value ? "*" : "localhost")}:{Port.Value}/"; Log($"Starting webserver on {URL}"); _modLifecycle = new CancellationTokenSource(); WebServer = new WebServer(o => { o.WithUrlPrefix(URL) .WithEmbedIOHttpListener(); }) .WithWebApi("/api/v1", m => m.WithController()) .WithModule(Subscribers); WebServer.Start(_modLifecycle.Token); } public void OnLoaded(List prefabs) { Log("Starting RemoteControl"); StartRC(); #if DEVELOPMENT_BUILD Debug.Log($"Loaded {prefabs.Count} prefabs"); #endif } public void OnUnloaded(List prefabs) { Log("Tearing down RemoteControl"); StopRC(); } 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(device); private static Device? GetDeviceJson(long referenceId) { var dev = Referencable.Find(referenceId); return dev == null ? null : new Device(dev); } private static GameDevice? GetNetworkDevice(string probeName, long referenceId) { GameDevice? dev = null; GameDevice.AllDevices.ForEach(anaDev => { if (anaDev is CableAnalyser analyser) { if (analyser.DisplayName != probeName) return false; dev = analyser.CableNetwork.DeviceList.Find(netDev => netDev.ReferenceId == referenceId); return dev != null; } return false; }); return dev; } private static List GetNetworkDevice(string probeName, string deviceName) { List result = new(); GameDevice.AllDevices.ForEach(anaDev => { if (anaDev is not CableAnalyser analyser) return; if (analyser.DisplayName != probeName) return; var found = analyser.CableNetwork.DeviceList.Find(netDev => netDev.DisplayName == deviceName); if (found is not null) { result.Add(found); } }); return result; } private static T FindUniqueDeviceForHttp(string networkName, string deviceName) { var result = default(T); bool isFound = false; // deviceName = WebUtility.UrlDecode(deviceName); // networkName = WebUtility.UrlDecode(networkName); GameDevice.AllDevices.ForEach(anaDev => { if (anaDev is not CableAnalyser analyser) return; if (analyser.DisplayName != networkName) return; var found = analyser.CableNetwork.DeviceList.Find(netDev => netDev is T && (netDev.DisplayName == deviceName || netDev.ReferenceId.ToString() == deviceName)); if (found is T item) { if (isFound) { throw HttpException.InternalServerError("More than one object matches"); } result = item; isFound = true; } }); if (!isFound) { throw HttpException.NotFound(); } return result!; } [Route(HttpVerbs.Get, "/networks")] public Task> ListNetworks() { IList probeNames = new List(); GameDevice.AllDevices.ForEach(dev => { if (dev is CableAnalyser analyser) { probeNames.Add(analyser.DisplayName); } }); return Task.FromResult(probeNames); } [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 && dev.DisplayName == networkId) { networks.Add(analyser.CableNetwork); } }); var devices = new Dictionary(); foreach (var network in networks) { foreach (var device in network.DeviceList) { 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(); var dev = GetNetworkDevice(networkId, deviceId) ?? throw HttpException.NotFound(); if (dev.CanLogicWrite(ltype)) { dev.SetLogicValue(ltype, value); } else { throw HttpException.MethodNotAllowed(); } } [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/slot/{slotId}/{varId}")] public async Task SetSlot(string networkId, string deviceId, int slotId, string varId) { LogicSlotType ltype; if (!StByName.TryGetValue(varId.ToLowerInvariant(), out ltype)) { throw HttpException.NotFound(); } var value = await HttpContext.GetRequestDataAsync(); var dev = FindUniqueDeviceForHttp(networkId, deviceId); if (dev.CanLogicWrite(ltype, slotId)) { dev.SetLogicValue(ltype, slotId, value); } else { throw HttpException.MethodNotAllowed(); } ; } [Route(HttpVerbs.Patch, "/networks/{networkId}/device/{deviceId}")] public async Task PatchDevice(string networkId, string deviceId) { var values = await HttpContext.GetRequestDataAsync>(); if (values.Keys.Any(key => !LtByName.ContainsKey(key.ToLowerInvariant()))) { throw HttpException.BadRequest(); } var dev = FindUniqueDeviceForHttp(networkId, deviceId); foreach (var keyValuePair in values) { var logicType = LtByName[keyValuePair.Key.ToLowerInvariant()]; if (dev.CanLogicWrite(logicType)) { dev.SetLogicValue(logicType, keyValuePair.Value); } } } [Route(HttpVerbs.Get, "/networks/{networkId}/device/{deviceId}/code")] public async Task GetCode(string networkId, string deviceId) { var dev = FindUniqueDeviceForHttp(networkId, deviceId); var code = dev.GetSourceCode(); HttpContext.Response.ContentType = "text/plain"; var codeBytes = Encoding.UTF8.GetBytes(code); await HttpContext.Response.OutputStream.WriteAsync(codeBytes, 0, codeBytes.Length); } [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/code")] public async Task SetCode(string networkId, string deviceId) { var code = await HttpContext.GetRequestBodyAsByteArrayAsync(); var dev = FindUniqueDeviceForHttp(networkId, deviceId); dev.SetSourceCode(Encoding.UTF8.GetString(code)); } } }