diff --git a/Patches/Tick.cs b/Patches/Tick.cs index 2cca631..09b827b 100644 --- a/Patches/Tick.cs +++ b/Patches/Tick.cs @@ -1,13 +1,18 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Assets.Scripts; using Assets.Scripts.Objects.Electrical; using HarmonyLib; using JetBrains.Annotations; +using RemoteControl.Message; +using Swan; +using GameDevice = Assets.Scripts.Objects.Pipes.Device; namespace RemoteControl.Patches { - [HarmonyPatch(typeof(LogicStack), nameof(LogicStack.LogicStackTick))] + [HarmonyPatch(typeof(RoomController), nameof(RoomController.ThreadedWork))] [SuppressMessage("ReSharper", "InconsistentNaming")] // ReSharper disable once InconsistentNaming public class LogicStack_LogicStackTick @@ -17,7 +22,8 @@ namespace RemoteControl.Patches internal object LockObj; internal bool Taken; } - + + [UsedImplicitly] private static void Prefix(out State __state) { @@ -26,12 +32,11 @@ namespace RemoteControl.Patches LockObj = SubscriptionManager.Lock, Taken = false }; - if (!GameManager.RunSimulation) return; // System.Threading.Monitor.Enter(__state.LockObj, ref __state.Taken); try { RemoteControl.Log("logic stack tick: start prefix"); - SubscriptionManager.ApplyUpdates(); + RemoteControl.Subscribers.SendUpdate(); RemoteControl.Log("logic stack tick: end prefix"); } catch (Exception e) @@ -46,7 +51,10 @@ namespace RemoteControl.Patches try { if (!GameManager.RunSimulation) return; + RemoteControl.Log("logic stack tick: start prefix"); SubscriptionManager.RescanNetworks(); + RemoteControl.Log("logic stack tick: start postfix"); + } catch (Exception e) { diff --git a/Scripts/Remotecontrol.cs b/Scripts/Remotecontrol.cs index 3da3b87..2383d6b 100644 --- a/Scripts/Remotecontrol.cs +++ b/Scripts/Remotecontrol.cs @@ -18,7 +18,6 @@ using EmbedIO.Routing; using EmbedIO.WebApi; using RemoteControl.Message; using RemoteControl.Utils; -using Swan; using GameDevice = Assets.Scripts.Objects.Pipes.Device; @@ -38,7 +37,8 @@ namespace RemoteControl private readonly Harmony _harmony = new Harmony(pluginGuid); - public WebServer WebServer { get; private set; } + public static WebServer WebServer { get; private set; } + internal static SubscriptionModule Subscribers { get; private set; } private CancellationTokenSource _modLifecycle = new(); @@ -70,6 +70,7 @@ namespace RemoteControl var subscriptionModule = new SubscriptionModule("/subscribe"); + Subscribers = subscriptionModule; WebServer = new WebServer(o => o.WithUrlPrefix($"http://{(ListenOnAllInterfaces.Value ? "0.0.0.0" : "localhost")}:{Port.Value}/") .WithEmbedIOHttpListener() @@ -125,28 +126,83 @@ namespace RemoteControl 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(GameDevice device) => new(device); + private static Device? GetDeviceJson(long referenceId) { var dev = Referencable.Find(referenceId); - return dev == null ? null : GetDeviceJson(dev); + 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; + + 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() => Task.FromResult(SubscriptionManager.GetProbes()); @@ -159,11 +215,9 @@ namespace RemoteControl var networks = new HashSet(); GameDevice.AllDevices.ForEach(dev => { - if (dev is CableAnalyser analyser) + if (dev is CableAnalyser analyser && dev.DisplayName == networkId) { - Debug.Log($"Found CA {analyser.DisplayName}: {analyser.ReferenceId}"); networks.Add(analyser.CableNetwork); - } }); @@ -172,7 +226,6 @@ namespace RemoteControl { 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)); } @@ -191,12 +244,19 @@ namespace RemoteControl } var value = await HttpContext.GetRequestDataAsync(); - - SubscriptionManager.SetLogic(networkId, deviceId, ltype, value); + 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, long deviceId, int slotId, string varId) + public async Task SetSlot(string networkId, string deviceId, int slotId, string varId) { LogicSlotType ltype; if (!StByName.TryGetValue(varId.ToLowerInvariant(), out ltype)) @@ -205,11 +265,19 @@ namespace RemoteControl } var value = await HttpContext.GetRequestDataAsync(); - SubscriptionManager.SetSlot(networkId, deviceId, slotId, ltype, value); + 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, long deviceId) + public async Task PatchDevice(string networkId, string deviceId) { var values = await HttpContext.GetRequestDataAsync>(); if (values.Keys.Any(key => !LtByName.ContainsKey(key.ToLowerInvariant()))) @@ -217,30 +285,29 @@ namespace RemoteControl throw HttpException.BadRequest(); } + var dev = FindUniqueDeviceForHttp(networkId, deviceId); foreach (var keyValuePair in values) { - SubscriptionManager.SetLogic(networkId, deviceId, LtByName[keyValuePair.Key.ToLowerInvariant()], keyValuePair.Value); + 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 UniTask GetCode(string networkId, long deviceId) + public UniTask GetCode(string networkId, string deviceId) { - await UniTask.SwitchToMainThread(HttpContext.CancellationToken); + var dev = FindUniqueDeviceForHttp(networkId, deviceId); + return UniTask.FromResult(dev.GetSourceCode()); + } - 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; + [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/code")] + public UniTask SetCode(string networkId, string deviceId, string code) + { + var dev = FindUniqueDeviceForHttp(networkId, deviceId); + return UniTask.FromResult(dev.GetSourceCode()); } } } \ No newline at end of file diff --git a/Scripts/SubscriptionModule.cs b/Scripts/SubscriptionModule.cs index 351693d..d8d28ba 100644 --- a/Scripts/SubscriptionModule.cs +++ b/Scripts/SubscriptionModule.cs @@ -1,8 +1,15 @@ using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; +using Assets.Scripts; +using Assets.Scripts.Objects.Electrical; using Assets.Scripts.Objects.Motherboards; using EmbedIO.WebSockets; +using RemoteControl.Message; using Swan; +using Swan.Diagnostics; +using GameDevice = Assets.Scripts.Objects.Pipes.Device; namespace RemoteControl { @@ -24,7 +31,6 @@ namespace RemoteControl public SubscriptionModule(string urlPath, bool enableConnectionWatchdog = true) : base(urlPath, enableConnectionWatchdog) { - AddProtocols("json"); } protected override Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result) @@ -43,12 +49,52 @@ namespace RemoteControl _jsonSubscribers.Remove(context); return base.OnClientDisconnectedAsync(context); } + + internal static Update GetUpdate() + { + Dictionary cache = new(); + Dictionary networks = new(); + + + // lock (SubscriptionManager.Lock) + var timer = new HighResolutionTimer(); + timer.Start(); + GameDevice.AllDevices.ForEach(dev => + { + if (dev is CableAnalyser analyser) + { + var network = networks.GetOrAdd(analyser.CableNetwork.ReferenceId, (long _) => new Message.Network + { + ReferenceId = analyser.CableNetwork.ReferenceId, + Devices = analyser.CableNetwork.DeviceList + .Select(subdev => cache.GetOrAdd(subdev.ReferenceId, _ => new Device(subdev))) + .ToList() + }); + network.Probes.Add(analyser.DisplayName); + } + }); + + Update update = new() + { + Networks = networks.Values.ToList(), + CalculationTime = timer.ElapsedMicroseconds, + }; + + + return update; + } + + internal void SendUpdate() + { + SendUpdate(GetUpdate()); + } } namespace Message { public class Update { + public long CalculationTime { get; set; } public List Networks { get; set; } = new(); } @@ -62,6 +108,24 @@ namespace RemoteControl public class Device { + public Device() {} + + public Device(GameDevice device) + { + ReferenceId = device.ReferenceId; + Name = device.DisplayName; + NameHash = device.GetNameHash(); + PrefabHash = device.PrefabHash; + PrefabName = device.PrefabName; + LogicValues = EnumCollections.LogicTypes.Values.Where(device.CanLogicRead) + .ToDictionary(ty => ty, device.GetLogicValue); + 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(); + IsCircuitHolder = device is ICircuitHolder; + } + public long ReferenceId { get; set; } public long NameHash{ get; set; } public string Name{ get; set; } @@ -69,6 +133,7 @@ namespace RemoteControl public string PrefabName{ get; set; } public Dictionary LogicValues { get; set; } = new(); public List> Slots { get; set; }= new(); + public bool IsCircuitHolder { get; set; } } } } \ No newline at end of file diff --git a/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs b/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs index a4cef0a..e2ca6aa 100644 --- a/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs +++ b/Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs @@ -49,6 +49,7 @@ namespace EmbedIO.WebSockets private TimeSpan _keepAliveInterval; private Encoding _encoding; private PeriodicTask? _connectionWatchdog; + private bool _allowNullProtocol = false; /// /// Initializes a new instance of the class.