From 5b542dbcc80edf29b657bb26a593af0b9e30f466 Mon Sep 17 00:00:00 2001 From: D4VID Date: Mon, 17 Feb 2025 20:48:31 +0100 Subject: [PATCH] Get cluster info --- .../CriticalPathAnalyzer.csproj | 3 + .../src/client/CriticalPathAnalyzerClient.cs | 16 +- .../client/CriticalPathAnalyzerGameState.cs | 1 + .../client/tool/CriticalPathAnalyzerTool.cs | 38 +-- .../src/client/tool/PathHighLighter.cs | 111 ++++++++ .../src/client/tool/WireTracerColors.cs | 34 +++ .../src/server/CriticalPathAnalyzerServer.cs | 12 +- .../src/server/ServerPathTracer.cs | 238 ++++++++++++++++++ .../shared/packets/s2c/AnalyzePathResponse.cs | 23 +- 9 files changed, 440 insertions(+), 36 deletions(-) create mode 100644 CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/PathHighLighter.cs create mode 100644 CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/WireTracerColors.cs create mode 100644 CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/ServerPathTracer.cs diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer.csproj b/CriticalPathAnalyzer/CriticalPathAnalyzer.csproj index f63a892..42d8301 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer.csproj +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer.csproj @@ -14,6 +14,9 @@ ..\LogicWorld\Logic_World_Data\Managed\FancyInput.dll + + ..\LogicWorld\Logic_World_Data\Managed\JimmysUnityUtilities.dll + ..\LogicWorld\Logic_World_Data\Managed\LogicAPI.dll diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerClient.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerClient.cs index 36526ee..4ecad61 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerClient.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerClient.cs @@ -33,7 +33,21 @@ namespace CriticalPathAnalyzer.Client { } public static void OnAnalyzePathResponse(AnalyzePathResponse response) { - LoggerInstance.Info($"Got response from server: {response.Message}"); + LoggerInstance.Info($"Got response from server"); + PathHighLighter.HighlightWires(response); + // if(!CurrentRequestID.HasValue || response.requestGuid != currentRequestID.Value) + // { + // //Not matching Guid, old or wrong request, discard. + // return; + // } + // currentRequestID = null; //Received response, clear GUID. + // + // //Clear up all data immediately: + // if(currentTracer != null) + // { + // currentTracer.stop(); + // currentTracer = null; + // } } } } \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs index cbfa027..a859192 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs @@ -34,6 +34,7 @@ namespace CriticalPathAnalyzer.Client { public override void OnExit() { CriticalPathAnalyzerClient.LoggerInstance.Info("CPA exit"); + PathHighLighter.RemoveHighLighting(); } } } \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs index 42bb191..8f945d8 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using CriticalPathAnalyzer.Shared.Packets.C2S; using LogicAPI.Data; using LogicLog; @@ -29,12 +28,12 @@ namespace CriticalPathAnalyzer.Client.Tool { // Resolve hit target: if (hitInfo.HitPeg) { - CriticalPathAnalyzerClient.LoggerInstance.Info("Hit peg"); + _logger.Info("Hit peg"); return _startPegAddress = hitInfo.pAddress; } if (hitInfo.HitWire) { - CriticalPathAnalyzerClient.LoggerInstance.Info("Hit wire"); + _logger.Info("Hit wire"); WireAddress wireAddress = hitInfo.wAddress; Wire wire = Instances.MainWorld.Data.Lookup(wireAddress); // Assume that wire is never null, as we did just ray-casted it. @@ -45,7 +44,7 @@ namespace CriticalPathAnalyzer.Client.Tool { } public static void SelectPathStart() { - CriticalPathAnalyzerClient.LoggerInstance.Info("Analyze Path Start"); + _logger.Info("Analyze Path Start"); PegAddress? pegAddress = RayCastPeg(); if (pegAddress != null) { _startPegAddress = pegAddress.Value; @@ -56,7 +55,7 @@ namespace CriticalPathAnalyzer.Client.Tool { } public static void SelectPathEnd() { - CriticalPathAnalyzerClient.LoggerInstance.Info("Analyze Path End"); + _logger.Info("Analyze Path End"); PegAddress? pegAddress = RayCastPeg(); if (pegAddress != null) { _endPegAddress = pegAddress.Value; @@ -67,35 +66,16 @@ namespace CriticalPathAnalyzer.Client.Tool { } private static void CalculateCriticalPath() { + if (_startPegAddress == null || _endPegAddress == null) { + _logger.Error("Invalid pegs"); + return; + } + Instances.SendData.Send(new AnalyzePathRequest { RequestGuid = Guid.NewGuid(), StartPegAddress = _startPegAddress.Value, EndPegAddress = _endPegAddress.Value, }); - HashSet wires = Instances.MainWorld.Data.LookupPegWires(_startPegAddress.Value); - _logger.Info($"Start peg has {wires.Count} wires"); - - ComponentType andGate = Instances.MainWorld.ComponentTypes.GetComponentType("MHG.AndGate"); - ComponentType xorGate = Instances.MainWorld.ComponentTypes.GetComponentType("MHG.XorGate"); - - foreach (WireAddress wireAddress in wires) { - Wire wire = Instances.MainWorld.Data.Lookup(wireAddress); - PegAddress nextPeg = wire.Point1 == _startPegAddress ? wire.Point2 : wire.Point1; - if (nextPeg.PegType != PegType.Input) { - // ignore - continue; - } - - IComponentInWorld component = Instances.MainWorld.Data.Lookup(nextPeg.ComponentAddress); - ComponentType type = component.Data.Type; - if (type == andGate) { - _logger.Info($"Connected to AND gate"); - } else if (type == xorGate) { - _logger.Info($"Connected to XOR gate"); - } else { - _logger.Info($"Connected to unknown gate"); - } - } } } } \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/PathHighLighter.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/PathHighLighter.cs new file mode 100644 index 0000000..f6e5f5b --- /dev/null +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/PathHighLighter.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using CriticalPathAnalyzer.client.tool; +using CriticalPathAnalyzer.Shared.Packets.S2C; +using LogicAPI.Data; +using LogicAPI.Services; +using LogicWorld.Interfaces; +using LogicWorld.Outlines; + +namespace CriticalPathAnalyzer.Client.Tool { + public class PathHighLighter { + private static List _clusters; + + public static void HighlightWires(AnalyzePathResponse response) { + _clusters = response.SelectedClusters; + + IWorldData world = Instances.MainWorld.Data; + + foreach (ClusterDetails clusterDetails in _clusters) { + foreach (ComponentAddress address in clusterDetails.ConnectingComponents) { + if (!world.Contains(address)) { + continue; + } + + Outliner.Outline(address, WireTracerColors.primaryConnected); + } + + foreach (ComponentAddress address in clusterDetails.LinkingComponents) { + if (!world.Contains(address)) { + continue; + } + + Outliner.Outline(address, WireTracerColors.linking); + } + + clusterDetails.HighlightedWires = new List(); + clusterDetails.HighlightedOutputWires = new List(); + + foreach (PegAddress pegAddress in clusterDetails.Pegs) { + if (!world.Contains(pegAddress.ComponentAddress)) { + continue; + } + + if (!pegAddress.IsInputAddress()) { + Outliner.Outline(pegAddress, WireTracerColors.primaryOutput); + continue; + } + + Outliner.Outline(pegAddress, WireTracerColors.primaryNormal); + + HashSet wires = Instances.MainWorld.Data.LookupPegWires(pegAddress); + if (wires == null) { + continue; + } + + foreach (WireAddress wireAddress in wires) { + Wire wire = Instances.MainWorld.Data.Lookup(wireAddress); + if (wire == null) { + return; + } + + // We do not collect wires from output pegs. So if the first is an output peg, the other side must be an input -> collect. + if (wire.Point1 == pegAddress || !wire.Point1.IsInputAddress()) { + if (wire.Point1.IsInputAddress() && wire.Point2.IsInputAddress()) { + clusterDetails.HighlightedWires.Add(wireAddress); + } else { + clusterDetails.HighlightedOutputWires.Add(wireAddress); + } + } + } + } + + foreach (WireAddress address in clusterDetails.HighlightedWires) { + Outliner.Outline(address, WireTracerColors.primaryNormal); + } + + foreach (WireAddress address in clusterDetails.HighlightedOutputWires) { + Outliner.Outline(address, WireTracerColors.primaryOutput); + } + } + } + + public static void RemoveHighLighting() { + if (_clusters == null) return; + foreach (ClusterDetails cluster in _clusters) { + UnhighlightCluster(cluster); + } + } + + private static void UnhighlightCluster(ClusterDetails cluster) { + foreach (PegAddress address in cluster.Pegs) { + Outliner.RemoveOutline(address); + } + + foreach (ComponentAddress address in cluster.ConnectingComponents) { + Outliner.RemoveOutline(address); + } + + foreach (ComponentAddress address in cluster.LinkingComponents) { + Outliner.RemoveOutline(address); + } + + foreach (WireAddress address in cluster.HighlightedWires) { + Outliner.RemoveOutline(address); + } + + foreach (WireAddress address in cluster.HighlightedOutputWires) { + Outliner.RemoveOutline(address); + } + } + } +} \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/WireTracerColors.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/WireTracerColors.cs new file mode 100644 index 0000000..7faec00 --- /dev/null +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/WireTracerColors.cs @@ -0,0 +1,34 @@ +using JimmysUnityUtilities; +using LogicWorld.Outlines; + +namespace CriticalPathAnalyzer.client.tool +{ + public static class WireTracerColors + { + //All clusters: + //Linking color is the intersection between clusters, it does not make sense to have this once per cluster type. + // The only change that could be done is to give linking separators between two non-primary clusters a different color. + // That is currently not supported nor detected. + public static readonly OutlineData linking = new OutlineData(new Color24(200, 200, 200)); + + //Primary cluster: + public static readonly OutlineData primaryNormal = new OutlineData(new Color24( 50, 255, 50)); + public static readonly OutlineData primaryConnected = new OutlineData(new Color24( 20, 150, 20)); + public static readonly OutlineData primaryOutput = new OutlineData(new Color24( 50, 50, 255)); + + //Sourcing cluster: + public static readonly OutlineData sourcingNormal = new OutlineData(new Color24(255, 50, 255)); + public static readonly OutlineData sourcingConnected = new OutlineData(new Color24(150, 20, 150)); + public static readonly OutlineData sourcingOutput = new OutlineData(new Color24( 80, 0, 255)); + + //Connected cluster: + public static readonly OutlineData connectedNormal = new OutlineData(new Color24(255, 255, 50)); + public static readonly OutlineData connectedConnected = new OutlineData(new Color24(150, 150, 20)); + public static readonly OutlineData connectedOutput = new OutlineData(new Color24( 80, 80, 255)); + + //Draining cluster: + public static readonly OutlineData drainingNormal = new OutlineData(new Color24( 50, 255, 255)); + public static readonly OutlineData drainingConnected = new OutlineData(new Color24( 20, 150, 150)); + public static readonly OutlineData drainingOutput = new OutlineData(new Color24( 0, 50, 150)); + } +} diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/CriticalPathAnalyzerServer.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/CriticalPathAnalyzerServer.cs index 6551dde..919dca3 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/CriticalPathAnalyzerServer.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/CriticalPathAnalyzerServer.cs @@ -16,6 +16,7 @@ using LogicWorld.Server; using LogicWorld.SharedCode.Networking; namespace CriticalPathAnalyzer.Server { + // ReSharper disable once ClassNeverInstantiated.Global public class CriticalPathAnalyzerServer : ServerMod, IClientVerifier { private const string ModId = "CriticalPathAnalyzer"; private const string ModVersion = "0.0.2"; @@ -57,12 +58,13 @@ namespace CriticalPathAnalyzer.Server { } } - public void AnalyzePath(Connection sender, Guid packetRequestGuid, PegAddress start, PegAddress end) { + public void AnalyzePath(Connection sender, Guid requestGuid, PegAddress start, PegAddress end) { LoggerInstance.Info("Got AnalyzePath request"); - AnalyzePathResponse response = new AnalyzePathResponse() { - RequestGuid = packetRequestGuid, - Message = "Lmao Yeet", - }; + if (!ServerPathTracer.TracePath(requestGuid, start, end, out AnalyzePathResponse response)) { + Logger.Error("Failed to trace path"); + return; + } + _networkServer.Send(sender, response); } } diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/ServerPathTracer.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/ServerPathTracer.cs new file mode 100644 index 0000000..3c5da03 --- /dev/null +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/ServerPathTracer.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CriticalPathAnalyzer.Shared.Packets.S2C; +using EccsLogicWorldAPI.Server; +using EccsLogicWorldAPI.Shared.AccessHelper; +using LogicAPI.Data; +using LogicAPI.Services; +using LogicWorld.Server.Circuitry; + +namespace CriticalPathAnalyzer.Server { + public class ServerPathTracer { + // Reflection/Delegate access helpers: + private static readonly Func GetCluster; + private static readonly Func GetLinker; + private static readonly Func> GetLeaders; + + private static readonly Func> GetFollowers; + + // Services needed to lookup wires/pegs: + private static readonly ICircuitryManager Circuits; + private static readonly IWorldData World; + + static ServerPathTracer() { + GetCluster = Delegator.createPropertyGetter( + Properties.getPrivate(typeof(InputPeg), "Cluster") + ); + GetLinker = Delegator.createFieldGetter( + Fields.getPrivate(typeof(Cluster), "Linker") + ); + GetLeaders = Delegator.createFieldGetter>( + Fields.getPrivate(typeof(ClusterLinker), "LinkedLeaders") + ); + GetFollowers = Delegator.createFieldGetter>( + Fields.getPrivate(typeof(ClusterLinker), "LinkedFollowers") + ); + + Circuits = ServiceGetter.getService(); + World = ServiceGetter.getService(); + } + + public static bool TracePath( + Guid requestGuid, + PegAddress start, + PegAddress end, + out AnalyzePathResponse response + ) { + response = null; + + CriticalPathAnalyzerServer.LoggerInstance.Info("Trace start"); + + // Validate, that the peg is actually existing: + if (!PegExists(start) || !PegExists(end)) { + CriticalPathAnalyzerServer.LoggerInstance.Error("Peg not found"); + return false; + } + + // An input peg, only has a single cluster. + // An output peg however can be connected to multiple clusters. + // It only makes sense to then select all these clusters as primary cluster. + if (!CollectMainClusters(start, out HashSet primaryClusters)) { + return false; // Whoops, cannot collect the primary clusters, probably probing an output peg. + } + + // Collect clusters that get powered by the original cluster or will power it. + var collectedSources = new HashSet(); + var collectedDrains = new HashSet(); + foreach (Cluster cluster in primaryClusters) { + CollectClusters(cluster, collectedSources, GetLeaders); + CollectClusters(cluster, collectedDrains, GetFollowers); + } + + foreach (Cluster cluster in primaryClusters) { + collectedSources.Remove(cluster); + collectedDrains.Remove(cluster); + } + + // Collect and filter clusters that are both source and drain: + var collectedEquals = new HashSet(); + foreach (Cluster collectedSource in collectedSources) { + if (collectedDrains.Remove(collectedSource)) { + collectedEquals.Add(collectedSource); + } + } + + foreach (Cluster collectedEqual in collectedEquals) { + collectedSources.Remove(collectedEqual); + } + + //Collect information about each cluster: + response = new AnalyzePathResponse() { + RequestGuid = requestGuid, + SelectedClusters = new List(), + }; + foreach (Cluster cluster in primaryClusters) { + response.SelectedClusters.Add(CollectClusterInformation(cluster)); + } + + // response.sourcingClusters = new List(); + // foreach (var cluster in collectedSources) { + // response.sourcingClusters.Add(CollectClusterInformation(cluster)); + // } + // + // response.connectedClusters = new List(); + // foreach (var cluster in collectedEquals) { + // response.connectedClusters.Add(CollectClusterInformation(cluster)); + // } + // + // response.drainingClusters = new List(); + // foreach (var cluster in collectedDrains) { + // response.drainingClusters.Add(CollectClusterInformation(cluster)); + // } + + + return true; + } + + + private static bool PegExists(PegAddress address) { + IComponentInWorld component = World.Lookup(address.ComponentAddress); + if (component == null) { + return false; // Component of the peg does not exist in world. + } + + int pegAmount = address.IsInputAddress() ? component.Data.InputCount : component.Data.OutputCount; + //If false: Component does not have this peg, as the peg index is bigger than the actual components input/output count. + return pegAmount >= address.PegIndex; + } + + private static Cluster GetClusterAt(InputAddress peg) { + InputPeg originPeg = Circuits.LookupInput(peg); + if (originPeg == null) { + throw new Exception( + "Tried to lookup cluster on input peg, but the peg was not present in the circuit model! This should never happen, as the peg is present in the world."); + } + + Cluster cluster = GetCluster(originPeg); + if (cluster == null) { + throw new Exception( + "Tried to lookup cluster on input peg, but the cluster was 'null', this should never happen! As the peg is present in the world."); + } + + return cluster; + } + + private static bool GetLinkerAt(Cluster cluster, out ClusterLinker linker) { + linker = GetLinker(cluster); + return linker != null; + } + + private static bool CollectMainClusters(PegAddress pegAddress, out HashSet primaryClusters) { + primaryClusters = new HashSet(); + if (pegAddress.IsInputAddress(out InputAddress inputAddress)) { + primaryClusters.Add(GetClusterAt(inputAddress)); + } else { + HashSet wires = World.LookupPegWires(pegAddress); + if (wires == null) { + return true; + } + + foreach (WireAddress wireAddress in wires) { + Wire wire = World.Lookup(wireAddress); + if (wire == null) { + throw new Exception( + "Tried to lookup wire given its address, but the world did not contain it. World must be corrupted."); + } + + PegAddress otherSide = wire.Point1 == pegAddress ? wire.Point2 : wire.Point1; + if (!otherSide.IsInputAddress(out var otherSideInputAddress)) { + continue; //Not supported, this wire would be invalid anyway. + } + + primaryClusters.Add(GetClusterAt(otherSideInputAddress)); + } + } + + return true; + } + + private static void CollectClusters( + Cluster startingPoint, + HashSet collectedClusters, + Func> linkedLinkerGetter + ) { + var clustersToProcess = new Queue(); + if (!GetLinkerAt(startingPoint, out ClusterLinker startingLinker)) { + return; // No linker on this cluster => no link => nothing to collect + } + + clustersToProcess.Enqueue(startingLinker); + // While the starting cluster is no source, the algorithm needs to skip it when encountered. + collectedClusters.Add(startingPoint); + while (clustersToProcess.TryDequeue(out ClusterLinker linkerToCheck)) { + List listOfLinkedLinkers = linkedLinkerGetter(linkerToCheck); // Is never null. + foreach (ClusterLinker linkedLinker in listOfLinkedLinkers) { + Cluster clusterOfLinkedLinker = linkedLinker.ClusterBeingLinked; // Should never be null. + if (collectedClusters.Add(clusterOfLinkedLinker)) { + // Element was not yet present in the array, so keep looking into it! + clustersToProcess.Enqueue(linkedLinker); + } + } + } + } + + private static ClusterDetails CollectClusterInformation(Cluster cluster) { + var details = new ClusterDetails { + Pegs = new List(), + ConnectingComponents = new List(), + LinkingComponents = new List(), + }; + + // Two lists are never null, according to how it is created and used: + IReadOnlyList inputPegs = cluster.ConnectedInputs; + IReadOnlyList outputPegs = cluster.ConnectedOutputs; + + foreach (InputPeg peg in inputPegs) { + details.Pegs.Add(peg.Address); + if (peg.SecretLinks != null && peg.SecretLinks.Any()) { + // Highlight this component somehow. + details.ConnectingComponents.Add(peg.Address.ComponentAddress); + } + + if ((peg.PhasicLinks != null && peg.PhasicLinks.Any()) + || (peg.OneWayPhasicLinksFollowers != null && peg.OneWayPhasicLinksFollowers.Any()) + || (peg.OneWayPhasicLinksLeaders != null && peg.OneWayPhasicLinksLeaders.Any()) + ) { + details.LinkingComponents.Add(peg.Address.ComponentAddress); + } + } + + foreach (OutputPeg peg in outputPegs) { + details.Pegs.Add(peg.Address); + } + + return details; + } + } +} \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs index 102a753..05056ad 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using LogicAPI.Data; using LogicAPI.Networking.Packets; using MessagePack; @@ -6,6 +8,25 @@ namespace CriticalPathAnalyzer.Shared.Packets.S2C { [MessagePackObject] public class AnalyzePathResponse : Packet { [Key(0)] public Guid RequestGuid; - [Key(1)] public string Message; + [Key(1)] + public List SelectedClusters; } + + [MessagePackObject] + public sealed class ClusterDetails + { + [Key(0)] + public List Pegs; + [Key(1)] + public List ConnectingComponents; + [Key(2)] + public List LinkingComponents; + + // The following two entries are used on the client to temporary store wires until their outline is being removed again. + // This information is just not collected on the server, hence the client collects them. + [IgnoreMember] + public List HighlightedWires; + [IgnoreMember] + public List HighlightedOutputWires; + } } \ No newline at end of file