Files
Stationeers-RemoteControl/Scripts/Remotecontrol.cs
2026-01-15 00:02:26 +01:00

313 lines
11 KiB
C#

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 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; 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");
Subscribers = subscriptionModule;
WebServer = new WebServer(o =>
o.WithUrlPrefix($"http://{(ListenOnAllInterfaces.Value ? "0.0.0.0" : "localhost")}:{Port.Value}/")
.WithEmbedIOHttpListener()
)
.WithWebApi("/api/v1", m => m.WithController<ApiController>())
.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<GameObject> prefabs)
{
// Start();
#if DEVELOPMENT_BUILD
Debug.Log($"Loaded {prefabs.Count} prefabs");
#endif
}
public void OnUnloaded(List<GameObject> 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<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;
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() => Task.FromResult(SubscriptionManager.GetProbes());
[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 UniTask<string> GetCode(string networkId, string deviceId)
{
var dev = FindUniqueDeviceForHttp<ICircuitHolder>(networkId, deviceId);
return UniTask.FromResult(dev.GetSourceCode());
}
[Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/code")]
public UniTask<string> SetCode(string networkId, string deviceId, string code)
{
var dev = FindUniqueDeviceForHttp<ICircuitHolder>(networkId, deviceId);
return UniTask.FromResult(dev.GetSourceCode());
}
}
}