Compare commits

...

4 Commits

@ -16,3 +16,9 @@ CriticalPathAnalyzer.AnalyzePathEnd:
DefaultBinding:
Options:
- O
CriticalPathAnalyzer.SelectNode:
Heading: "CriticalPathAnalyzer"
DefaultBinding:
Options:
- T

@ -14,3 +14,9 @@ FancyInput.Trigger.CriticalPathAnalyzer.AnalyzePathEnd: "Select end of path"
FancyInput.Trigger.CriticalPathAnalyzer.AnalyzePathEnd.Description: """
Press to select the end of the path to be analyzed.
"""
FancyInput.Trigger.CriticalPathAnalyzer.SelectNode: "Select node to be analyzed"
FancyInput.Trigger.CriticalPathAnalyzer.SelectNode.Description: """
Press to select a node to be analyzed.
Can only be used after obtaining circuit data from the server by analyzing a path.
"""

@ -1,7 +1,7 @@
ID: CriticalPathAnalyzer
Name: CriticalPathAnalyzer
Author: D4VID
Version: 0.0.4
Version: 0.2.1
Priority: 0
Dependencies:
- HarmonyForLogicWorld

@ -24,6 +24,7 @@ namespace CriticalPathAnalyzer.Client {
UITrigger.Back,
CriticalPathAnalyzerTrigger.AnalyzePathStart,
CriticalPathAnalyzerTrigger.AnalyzePathEnd,
CriticalPathAnalyzerTrigger.SelectNode,
};
/// <summary>
@ -43,6 +44,8 @@ namespace CriticalPathAnalyzer.Client {
CriticalPathAnalyzerTool.SelectPathStart();
} else if (CustomInput.DownThisFrame(CriticalPathAnalyzerTrigger.AnalyzePathEnd)) {
CriticalPathAnalyzerTool.SelectPathEnd();
} else if (CustomInput.DownThisFrame(CriticalPathAnalyzerTrigger.SelectNode)) {
CriticalPathAnalyzerTool.SelectNodeToAnalyze();
}
}

@ -5,5 +5,6 @@ namespace CriticalPathAnalyzer.Client.Keybindings {
OpenPathAnalyzer,
AnalyzePathStart,
AnalyzePathEnd,
SelectNode,
}
}

@ -18,12 +18,14 @@ namespace CriticalPathAnalyzer.Client.Tool {
private static Guid _currentRequestGuid = Guid.NewGuid();
private static AnalyzePathResponse _response;
private static Node _selectedNode;
private static readonly OutlineData StartOutline = new OutlineData(new Color24(0x00ff00));
private static readonly OutlineData EndOutline = new OutlineData(new Color24(0x00aaff));
public static void Init(ILogicLogger logger) {
_logger = logger;
PathHighlighter.Init(logger);
}
/// <summary>
@ -80,6 +82,35 @@ namespace CriticalPathAnalyzer.Client.Tool {
}
}
}
/// <summary>
/// Try to select the start of the path.
/// </summary>
public static void SelectNodeToAnalyze() {
if (_response == null) {
_logger.Info("No circuit data");
// Cannot do anything without data
return;
}
PegAddress pegAddress = RayCastPeg();
if (!pegAddress.IsEmpty()) {
Node node = _response.Nodes.Find(n => n.Pegs.Contains(pegAddress));
if (node != null) {
PathHighlighter.RemoveHighlighting();
_logger.Info("Highlighting selected node");
PathHighlighter.HighlightAnalyzedNode(node);
_selectedNode = node;
} else {
_logger.Info("Peg not found in circuit data");
}
} else {
// Deselect
_logger.Info("Didn't hit anything");
PathHighlighter.RemoveHighlighting();
_selectedNode = null;
}
}
/// <summary>
/// Try to select the end of the path.
@ -129,12 +160,13 @@ namespace CriticalPathAnalyzer.Client.Tool {
_response = response;
PathHighlighter.RemoveHighlighting();
PathHighlighter.SetNodes(_response.Nodes);
if (_response.LoopingClusters.Count > 0) {
PathHighlighter.HighlightWires(_response.LoopingClusters);
if (_response.LoopingNodes.Count > 0) {
PathHighlighter.HighlightClusterNodes(_response.LoopingNodes);
} else {
_logger.Info($"Critical path length is: {_response.CriticalPathLength}");
PathHighlighter.HighlightWires(_response.Clusters);
PathHighlighter.HighlightClusterNodes(_response.Nodes);
}
}
}

@ -1,33 +1,56 @@
using System;
using System.Collections.Generic;
using CriticalPathAnalyzer.Shared.Packets.S2C;
using JimmysUnityUtilities;
using LogicAPI.Data;
using LogicAPI.Services;
using LogicLog;
using LogicWorld.Interfaces;
using LogicWorld.Outlines;
namespace CriticalPathAnalyzer.Client.Tool {
public class PathHighlighter {
private static List<ClusterDetails> _clusters = new List<ClusterDetails>();
private static ILogicLogger _logger;
private static List<Node> _nodes = new List<Node>();
private static List<WireAddress> _highlightedWires = new List<WireAddress>();
public static void Init(ILogicLogger logger) {
_logger = logger;
}
public static void SetNodes(List<Node> nodes) {
_nodes = nodes;
}
public static void HighlightWires(List<ClusterDetails> clusters) {
_clusters = clusters;
public static void HighlightClusterNodes(List<Node> nodes) {
foreach (Node node in nodes) {
HighlightNode(node, HsvToRgb(node.Time * 20, 1.0f, 1.0f));
}
}
foreach (ClusterDetails clusterDetails in _clusters) {
HighlightCluster(clusterDetails);
public static void HighlightAnalyzedNode(Node node) {
_logger.Info($"Selected node: {node}");
HighlightNode(node, 0x00ff00);
foreach (int prevNodeIndex in node.PrevNodeIndexes) {
_logger.Info($"Highlighting prev node index {prevNodeIndex}");
HighlightNode(_nodes[prevNodeIndex], 0xff0000);
}
foreach ((int nextNodeIndex, int delay) in node.NextNodes) {
_logger.Info($"Highlighting next node index {nextNodeIndex}");
HighlightNode(_nodes[nextNodeIndex], 0x0000ff);
}
}
/// <summary>
/// Highlight all pegs and wires of the given cluster.
/// </summary>
private static void HighlightCluster(ClusterDetails cluster) {
private static void HighlightNode(Node node, int color) {
IWorldData world = Instances.MainWorld.Data;
var outline = new OutlineData(new Color24(cluster.Color));
var outline = new OutlineData(new Color24(color));
foreach (ComponentAddress address in cluster.ConnectingComponents) {
foreach (ComponentAddress address in node.ConnectingComponents) {
if (!world.Contains(address)) {
continue;
}
@ -35,7 +58,7 @@ namespace CriticalPathAnalyzer.Client.Tool {
Outliner.Outline(address, outline);
}
foreach (PegAddress pegAddress in cluster.Pegs) {
foreach (PegAddress pegAddress in node.Pegs) {
if (!world.Contains(pegAddress.ComponentAddress)) {
continue;
}
@ -63,30 +86,80 @@ namespace CriticalPathAnalyzer.Client.Tool {
}
public static void RemoveHighlighting() {
if (_clusters == null) {
if (_nodes == null) {
return;
}
foreach (ClusterDetails cluster in _clusters) {
UnhighlightCluster(cluster);
foreach (Node cluster in _nodes) {
UnhighlightNode(cluster);
}
foreach (WireAddress wireAddress in _highlightedWires) {
Outliner.RemoveOutline(wireAddress);
}
_clusters = null;
_highlightedWires.Clear();
}
private static void UnhighlightCluster(ClusterDetails cluster) {
foreach (PegAddress address in cluster.Pegs) {
private static void UnhighlightNode(Node node) {
foreach (PegAddress address in node.Pegs) {
Outliner.RemoveOutline(address);
}
foreach (ComponentAddress address in cluster.ConnectingComponents) {
foreach (ComponentAddress address in node.ConnectingComponents) {
Outliner.RemoveOutline(address);
}
}
/// <summary>
/// Convert HSB to RGB
/// </summary>
/// <param name="h">0 - 360</param>
/// <param name="s">0.0 - 1.0</param>
/// <param name="v">0.0 - 1.0</param>
/// <returns>0xrrggbb</returns>
private static int HsvToRgb(int h, float s, float v) {
s = Math.Clamp(s, 0, 1);
v = Math.Clamp(v, 0, 1);
h = (h % 360 + 360) % 360; // Ensure hue is within 0-359 range
float c = v * s;
float x = c * (1 - Math.Abs((h / 60.0f) % 2 - 1));
float m = v - c;
float r, g, b;
if (h < 60) {
r = c;
g = x;
b = 0;
} else if (h < 120) {
r = x;
g = c;
b = 0;
} else if (h < 180) {
r = 0;
g = c;
b = x;
} else if (h < 240) {
r = 0;
g = x;
b = c;
} else if (h < 300) {
r = x;
g = 0;
b = c;
} else {
r = c;
g = 0;
b = x;
}
int rInt = (byte) ((r + m) * 255);
int gInt = (byte) ((g + m) * 255);
int bInt = (byte) ((b + m) * 255);
return (rInt << 16) | (gInt << 8) | bInt;
}
}
}

@ -3,10 +3,29 @@ using LogicWorld.Server.Circuitry;
namespace CriticalPathAnalyzer.Server.Tool {
public class ClusterNode {
public int Index; // "identifier"
public HashSet<Cluster> Clusters; // bidirectionally connected clusters without delay
public int Time; // what point in time has been this cluster last updated
public Dictionary<int, int> NextNodes; // next node index / delay between them
public HashSet<int> PrevNodeIndexes; // node indexes that update this one
/// <summary>
/// Identifier
/// </summary>
public int Index;
/// <summary>
/// All bidirectionally connected clusters without delay that form this node
/// </summary>
public HashSet<Cluster> Clusters;
/// <summary>
/// Next node index / delay between them
/// </summary>
public Dictionary<int, int> NextNodes;
/// <summary>
/// Node indexes that update this one
/// </summary>
public HashSet<int> PrevNodeIndexes;
/// <summary>
/// After how many ticks has been this cluster last updated
/// </summary>
public int Time;
}
}

@ -109,14 +109,16 @@ namespace CriticalPathAnalyzer.Server.Tool {
}
}
foreach (ClusterNode node in clusterNodes) {
FillNodeInformation(node);
}
// Collect information about each cluster:
response = new AnalyzePathResponse() {
RequestGuid = requestGuid,
Clusters = clusterNodes.SelectMany(node =>
node.Clusters.Select(cluster => CollectClusterInformation(cluster, node.Time))).ToList(),
Nodes = clusterNodes.Select(FillNodeInformation).ToList(),
CriticalPathLength = criticalPathLength,
LoopingClusters = loopingNodes.SelectMany(loopingNode => loopingNode.Clusters
.Select(cluster => CollectClusterInformation(cluster, 0))).ToList(),
LoopingNodes = loopingNodes.Select(FillNodeInformation).ToList(),
};
logger.Info("Trace end");
@ -133,6 +135,7 @@ namespace CriticalPathAnalyzer.Server.Tool {
if (iterations % 1000 == 0) {
CriticalPathAnalyzerServer.LoggerInstance.Info($"{iterations} propagation iterations");
}
ClusterNode node = clusterNodes[index];
foreach ((int nextNodeIndex, int delay) in node.NextNodes) {
clusterNodes[nextNodeIndex].Time = node.Time + delay;
@ -169,6 +172,7 @@ namespace CriticalPathAnalyzer.Server.Tool {
if (iterations % 1000 == 0) {
CriticalPathAnalyzerServer.LoggerInstance.Info($"{iterations} loop iterations");
}
foreach ((int nextNodeIndex, int _) in node.NextNodes) {
ClusterNode nextNode = clusterNodes[nextNodeIndex];
nextNode.PrevNodeIndexes.Remove(node.Index);
@ -250,24 +254,40 @@ namespace CriticalPathAnalyzer.Server.Tool {
logger.Info($"Finished after {iterations} iterations");
}
private static HashSet<Cluster> GetClustersConnectedThroughRelays(Cluster cluster) {
var clusters = new HashSet<Cluster>() {cluster};
foreach (InputPeg inputPeg in cluster.ConnectedInputs) {
LogicComponent component = inputPeg.LogicComponent;
if (component == null) {
// These are regular pegs, they are not attached to any logic component, skip them
continue;
}
private static HashSet<Cluster> GetClustersConnectedThroughRelays(Cluster origin) {
var clusters = new HashSet<Cluster> {origin};
var queue = new Queue<Cluster>();
queue.Enqueue(origin);
if (component.GetType() == typeof(Relay)) {
// Collect all next clusters connected via a relay (from the control input side)
if (inputPeg.Address == component.Inputs[1].Address) {
// one of the sides
// TODO: need to recursively check for further relays
CollectPegClusters(component.Inputs[2].Address, clusters);
} else if (inputPeg.Address == component.Inputs[2].Address) {
// the other side
CollectPegClusters(component.Inputs[1].Address, clusters);
while (queue.TryDequeue(out Cluster cluster)) {
foreach (InputPeg inputPeg in cluster.ConnectedInputs) {
LogicComponent component = inputPeg.LogicComponent;
if (component == null) {
// These are regular pegs, they are not attached to any logic component, skip them
continue;
}
if (component.GetType() == typeof(Relay)) {
// Collect all next clusters connected via a relay (from the control input side)
if (inputPeg.Address == component.Inputs[1].Address) {
// one of the sides
if (component.Inputs[2].Address.IsInputAddress(out InputAddress inputAddress)) {
Cluster nextCluster = GetClusterAt(inputAddress);
if (clusters.Add(nextCluster)) {
// found a new cluster, continue searching
queue.Enqueue(nextCluster);
}
}
} else if (inputPeg.Address == component.Inputs[2].Address) {
// the other side
if (component.Inputs[1].Address.IsInputAddress(out InputAddress inputAddress)) {
Cluster nextCluster = GetClusterAt(inputAddress);
if (clusters.Add(nextCluster)) {
// found a new cluster, continue searching
queue.Enqueue(nextCluster);
}
}
}
}
}
}
@ -381,78 +401,37 @@ namespace CriticalPathAnalyzer.Server.Tool {
}
private static ClusterDetails CollectClusterInformation(Cluster cluster, int time) {
var details = new ClusterDetails {
/// <summary>
/// Obtain additional information about the clusters:
/// Collect all their pegs and sockets.
/// </summary>
private static Node FillNodeInformation(ClusterNode node) {
var clientNode = new Node() {
Index = node.Index,
NextNodes = node.NextNodes,
PrevNodeIndexes = node.PrevNodeIndexes,
Time = node.Time,
Pegs = new List<PegAddress>(),
ConnectingComponents = new List<ComponentAddress>(),
Color = HsvToRgb(time * 20, 1, 1),
};
// Two lists are never null, according to how it is created and used:
IReadOnlyList<InputPeg> inputPegs = cluster.ConnectedInputs;
IReadOnlyList<OutputPeg> outputPegs = cluster.ConnectedOutputs;
foreach (Cluster cluster in node.Clusters) {
// Two lists are never null, according to how it is created and used:
IReadOnlyList<InputPeg> inputPegs = cluster.ConnectedInputs;
IReadOnlyList<OutputPeg> outputPegs = cluster.ConnectedOutputs;
foreach (InputPeg peg in inputPegs) {
details.Pegs.Add(peg.Address);
if (peg.SecretLinks != null && peg.SecretLinks.Any()) {
// Socket
details.ConnectingComponents.Add(peg.Address.ComponentAddress);
foreach (InputPeg peg in inputPegs) {
clientNode.Pegs.Add(peg.Address);
if (peg.SecretLinks != null && peg.SecretLinks.Any()) {
// Socket
clientNode.ConnectingComponents.Add(peg.Address.ComponentAddress);
}
}
}
foreach (OutputPeg peg in outputPegs) {
details.Pegs.Add(peg.Address);
clientNode.Pegs.AddRange(outputPegs.Select(peg => peg.Address));
}
return details;
}
/// <param name="h">0 - 360</param>
/// <param name="s">0.0 - 1.0</param>
/// <param name="v">0.0 - 1.0</param>
/// <returns>0xrrggbb</returns>
private static int HsvToRgb(int h, float s, float v) {
s = Math.Clamp(s, 0, 1);
v = Math.Clamp(v, 0, 1);
h = (h % 360 + 360) % 360; // Ensure hue is within 0-359 range
float c = v * s;
float x = c * (1 - Math.Abs((h / 60.0f) % 2 - 1));
float m = v - c;
float r, g, b;
if (h < 60) {
r = c;
g = x;
b = 0;
} else if (h < 120) {
r = x;
g = c;
b = 0;
} else if (h < 180) {
r = 0;
g = c;
b = x;
} else if (h < 240) {
r = 0;
g = x;
b = c;
} else if (h < 300) {
r = x;
g = 0;
b = c;
} else {
r = c;
g = 0;
b = x;
}
int rInt = (byte) ((r + m) * 255);
int gInt = (byte) ((g + m) * 255);
int bInt = (byte) ((b + m) * 255);
return (rInt << 16) | (gInt << 8) | bInt;
return clientNode;
}
}
}

@ -8,25 +8,42 @@ namespace CriticalPathAnalyzer.Shared.Packets.S2C {
[MessagePackObject]
public class AnalyzePathResponse : Packet {
[Key(0)] public Guid RequestGuid;
[Key(1)] public List<ClusterDetails> Clusters;
[Key(1)] public List<Node> Nodes;
[Key(2)] public int CriticalPathLength;
[Key(3)] public List<ClusterDetails> LoopingClusters;
[Key(3)] public List<Node> LoopingNodes;
}
[MessagePackObject]
public sealed class ClusterDetails {
public sealed class Node {
/// <summary>
/// Identifier
/// </summary>
[Key(0)] public int Index;
/// <summary>
/// Next node index / delay between them
/// </summary>
[Key(1)] public Dictionary<int, int> NextNodes;
/// <summary>
/// Node indexes that update this one
/// </summary>
[Key(2)] public HashSet<int> PrevNodeIndexes;
/// <summary>
/// After how many ticks has been this cluster last updated
/// </summary>
[Key(3)] public int Time;
/// <summary>
/// List of all pegs inside the cluster
/// </summary>
[Key(0)] public List<PegAddress> Pegs;
[Key(4)] public List<PegAddress> Pegs;
/// <summary>
/// Sockets
/// </summary>
[Key(1)] public List<ComponentAddress> ConnectingComponents;
[Key(3)] public int Color;
[Key(5)] public List<ComponentAddress> ConnectingComponents;
}
}
Loading…
Cancel
Save