355 lines
12 KiB
C#
355 lines
12 KiB
C#
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<int> Port;
|
|
|
|
// ReSharper disable once MemberCanBePrivate.Global
|
|
public static ConfigEntry<bool> 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<ApiController>())
|
|
.WithModule(Subscribers);
|
|
WebServer.Start(_modLifecycle.Token);
|
|
}
|
|
|
|
public void OnLoaded(List<GameObject> prefabs)
|
|
{
|
|
Log("Starting RemoteControl");
|
|
StartRC();
|
|
#if DEVELOPMENT_BUILD
|
|
Debug.Log($"Loaded {prefabs.Count} prefabs");
|
|
#endif
|
|
}
|
|
|
|
public void OnUnloaded(List<GameObject> 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<string, LogicType> LtByName = EnumCollections.LogicTypes.AsLookupDict(true);
|
|
|
|
private static readonly Dictionary<string, LogicSlotType> StByName =
|
|
EnumCollections.LogicSlotTypes.AsLookupDict(true);
|
|
|
|
private static Device GetDeviceJson(GameDevice device) => new(device);
|
|
|
|
|
|
private static Device? GetDeviceJson(long referenceId)
|
|
{
|
|
var dev = Referencable.Find<GameDevice>(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<GameDevice> GetNetworkDevice(string probeName, string deviceName)
|
|
{
|
|
List<GameDevice> 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<T>(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<IList<string>> ListNetworks()
|
|
{
|
|
IList<string> probeNames = new List<string>();
|
|
GameDevice.AllDevices.ForEach(dev =>
|
|
{
|
|
if (dev is CableAnalyser analyser)
|
|
{
|
|
probeNames.Add(analyser.DisplayName);
|
|
}
|
|
});
|
|
return Task.FromResult(probeNames);
|
|
}
|
|
|
|
[Route(HttpVerbs.Get, "/networks/{networkId}")]
|
|
public Task<IDictionary<long, Device>> ListDevices(string networkId)
|
|
{
|
|
// lock (SubscriptionManager.Lock)
|
|
using (new LogTimer("ListDevices"))
|
|
{
|
|
var networks = new HashSet<CableNetwork>();
|
|
GameDevice.AllDevices.ForEach(dev =>
|
|
{
|
|
if (dev is CableAnalyser analyser && dev.DisplayName == networkId)
|
|
{
|
|
networks.Add(analyser.CableNetwork);
|
|
}
|
|
});
|
|
|
|
var devices = new Dictionary<long, Device>();
|
|
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<IDictionary<long, Device>>(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<double>();
|
|
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<double>();
|
|
var dev = FindUniqueDeviceForHttp<GameDevice>(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<Dictionary<string, double>>();
|
|
if (values.Keys.Any(key => !LtByName.ContainsKey(key.ToLowerInvariant())))
|
|
{
|
|
throw HttpException.BadRequest();
|
|
}
|
|
|
|
var dev = FindUniqueDeviceForHttp<GameDevice>(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<ICircuitHolder>(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<ICircuitHolder>(networkId, deviceId);
|
|
dev.SetSourceCode(Encoding.UTF8.GetString(code));
|
|
}
|
|
}
|
|
} |