using System; using System.Collections.Generic; using System.Reflection; using BlockSpace.Voxels; using TMPro; using UnityEngine; public class Node : MonoBehaviour { private const float PortHighlightScale = 1.25f; private const float PortPulseScale = 0.22f; private const float PortPulseDuration = 0.16f; private const float SelectedScale = 1.035f; private static readonly Color SelectedTextColor = new Color(0.55f, 0.92f, 1f, 1f); [Header("Identity")] [SerializeField] private int nodeId; [Header("Definition")] public NodeDefinition Definition; [Header("Runtime")] public List InputPorts = new List(); public List OutputPorts = new List(); private NodeBehavior behavior; private NodeGraphManager graphManager; private bool selected; private bool visibleInEditMode = true; private readonly List pendingSerializedConnections = new List(); private readonly Dictionary pendingInputValues = new Dictionary(StringComparer.Ordinal); private readonly Dictionary pendingOutputValues = new Dictionary(StringComparer.Ordinal); private readonly List cachedPortTexts = new List(); private readonly Dictionary defaultTextColors = new Dictionary(); private readonly List cachedRenderers = new List(); private Vector3 defaultScale = Vector3.one; [Header("Node Name")] public TMP_Text NodeNameText; [Header("Boards")] public GameObject SmallBoard; public GameObject MediumBoard; public GameObject LargeBoard; public GameObject ExtraLargeBoard; [Header("Input Port Parents")] public Transform InputRunPortParent; public Transform InputValuePortParent; [Header("Output Port Parents")] public Transform OutputRunPortParent; public Transform OutputValuePortParent; [Header("Input Text Parent")] public Transform InputTextParent; [Header("Output Text Parent")] public Transform OutputTextParent; [Header("Port Prefabs")] public GameObject InputRunPortPrefab; public GameObject InputValuePortPrefab; public GameObject OutputRunPortPrefab; public GameObject OutputValuePortPrefab; [Header("Text Prefab")] public TMP_Text PortTextPrefab; [Header("Port Layout")] public float FirstPortY; public float PortYOffset = -1.25f; [Header("Text Layout")] public float FirstTextY = 0.0869f; public float TextYOffset = -0.0618f; private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); private static readonly int ColorId = Shader.PropertyToID("_Color"); private static readonly int TintColorId = Shader.PropertyToID("_TintColor"); public NodeBoardSize BoardSize { get; private set; } public int NodeId => nodeId; private void Awake() { defaultScale = base.transform.localScale; CaptureSerializedRuntimeState(); ReinitializeFromDefinition(); } private void Start() { NodeGraphManager.Instance?.RegisterNode(this); } private void LateUpdate() { UpdatePortVisualState(); UpdatePortTintState(); } public void AssignGraph(NodeGraphManager manager) { graphManager = manager; for (int i = 0; i < InputPorts.Count; i++) { InputPorts[i].Owner = this; } for (int j = 0; j < OutputPorts.Count; j++) { OutputPorts[j].Owner = this; } } public void SetNodeId(int id) { nodeId = Mathf.Max(0, id); } public void ReinitializeFromDefinition() { if (Definition == null) { InputPorts.Clear(); OutputPorts.Clear(); ClearVisualCaches(); return; } BuildPorts(); BoardSize = Definition.GetBoardSize(); BuildVisuals(); CreateBehavior(); ApplySelectionVisual(); ApplyVisibility(); } public bool TryGetPort(string portName, PortDirection direction, out NodePort port) { List list = ((direction == PortDirection.Input) ? InputPorts : OutputPorts); for (int i = 0; i < list.Count; i++) { NodePort nodePort = list[i]; if (nodePort != null && string.Equals(nodePort.Name, portName, StringComparison.Ordinal)) { port = nodePort; return true; } } port = null; return false; } public bool TryGetPort(string portName, PortDirection direction, PortKind kind, out NodePort port) { if (TryGetPort(portName, direction, out port) && port.Kind == kind) { return true; } port = null; return false; } public BSValue GetInputValue(string portName) { if (!TryGetPort(portName, PortDirection.Input, PortKind.Value, out var port)) { return BSValue.None; } return port.Value; } public BSValue GetOutputValue(string portName) { if (!TryGetPort(portName, PortDirection.Output, PortKind.Value, out var port)) { return BSValue.None; } return port.Value; } public bool TryGetInputGroup(string portName, out VoxelGroup group) { group = null; if (TryGetPort(portName, PortDirection.Input, PortKind.Value, out var port)) { return port.Value.TryResolveGroup(out group); } return false; } public VoxelGroup GetInputGroup(string portName) { TryGetInputGroup(portName, out var group); return group; } public void SetInputValue(string portName, BSValue value) { if (TryGetPort(portName, PortDirection.Input, PortKind.Value, out var port)) { if (graphManager != null) { graphManager.AssignInputValue(this, port, value, broadcast: true); } else { SetInputValueLocal(port, value, pulsePort: true); } } } public void SetOutputValue(string portName, BSValue value) { if (TryGetPort(portName, PortDirection.Output, PortKind.Value, out var port)) { if (graphManager != null) { graphManager.ApplyOutputValue(this, port, value, broadcast: false); } else { SetOutputValueLocal(port, value); } } } public void SetOutputValueLocalOnly(string portName, BSValue value) { if (TryGetPort(portName, PortDirection.Output, PortKind.Value, out var port)) { if (graphManager != null) { graphManager.ApplyOutputValue(this, port, value, broadcast: false); } else { SetOutputValueLocal(port, value); } } } public void TriggerRun(string outputPortName) { if (TryGetPort(outputPortName, PortDirection.Output, PortKind.Run, out var port)) { if (graphManager != null) { graphManager.TriggerOutputRun(this, port, broadcast: false); } else { TriggerRunLocal(port); } } } public void ReceiveRun(string inputPortName) { if (TryGetPort(inputPortName, PortDirection.Input, PortKind.Run, out var port)) { PulsePort(port); behavior?.OnRun(inputPortName); } } public void TickBehavior() { behavior?.OnGraphUpdate(); } public bool Connect(string outputPortName, Node targetNode, string inputPortName) { if (graphManager == null || targetNode == null) { return false; } return graphManager.Connect(this, outputPortName, targetNode, inputPortName, broadcast: true); } public void Disconnect(string outputPortName, Node targetNode, string inputPortName) { if (!(graphManager == null) && !(targetNode == null)) { graphManager.Disconnect(this, outputPortName, targetNode, inputPortName, broadcast: true); } } public void SetSelected(bool isSelected) { if (selected != isSelected) { selected = isSelected; ApplySelectionVisual(); } } public void SetVisibleInEditMode(bool visible) { if (visibleInEditMode != visible) { visibleInEditMode = visible; ApplyVisibility(); } } public Bounds GetWorldBounds() { if (cachedRenderers.Count == 0) { CacheVisuals(); } if (cachedRenderers.Count == 0) { return new Bounds(base.transform.position, Vector3.one * 0.2f); } Bounds result = new Bounds(cachedRenderers[0].bounds.center, cachedRenderers[0].bounds.size); for (int i = 1; i < cachedRenderers.Count; i++) { if (cachedRenderers[i] != null) { result.Encapsulate(cachedRenderers[i].bounds); } } return result; } public Vector3 GetPortWorldPosition(NodePort port) { if (port != null && port.VisualTransform != null) { return port.VisualTransform.position; } return base.transform.position; } public Vector3 GetPortWorldDirection(NodePort port) { if (port == null) { return base.transform.forward; } if (port.VisualTransform != null) { if (port.Direction != PortDirection.Output) { return -port.VisualTransform.right; } return port.VisualTransform.right; } if (port.Direction != PortDirection.Output) { return -base.transform.right; } return base.transform.right; } internal void SetInputValueLocal(NodePort port, BSValue value, bool pulsePort) { if (port != null && port.Direction == PortDirection.Input && port.Kind == PortKind.Value) { BSValue bSValue = value.Normalize(); bool num = !BSValueEquals(port.Value, bSValue); port.Value = bSValue; UpdatePortLabel(port); if (pulsePort) { PulsePort(port); } if (num) { behavior?.OnInputValueChanged(port.Name, port.Value); } } } internal void SetOutputValueLocal(NodePort port, BSValue value) { if (port == null || port.Direction != PortDirection.Output || port.Kind != PortKind.Value) { return; } port.Value = value.Normalize(); UpdatePortLabel(port); PulsePort(port); for (int i = 0; i < port.Connections.Count; i++) { NodePort nodePort = port.Connections[i]; if (nodePort != null && !(nodePort.Owner == null)) { nodePort.Owner.SetInputValueLocal(nodePort, port.Value, pulsePort: true); } } } internal void TriggerRunLocal(NodePort port) { if (port == null || port.Direction != PortDirection.Output || port.Kind != PortKind.Run) { return; } PulsePort(port); for (int i = 0; i < port.Connections.Count; i++) { NodePort nodePort = port.Connections[i]; if (nodePort != null && !(nodePort.Owner == null)) { nodePort.Owner.ReceiveRun(nodePort.Name); } } } internal void PulsePort(NodePort port) { if (port != null) { port.PulseUntil = Mathf.Max(port.PulseUntil, Time.unscaledTime + 0.16f); } } internal void SetPortHighlight(NodePort port, bool highlighted) { if (port != null) { port.Highlighted = highlighted; } } internal List ConsumePendingSerializedConnections() { List result = new List(pendingSerializedConnections); pendingSerializedConnections.Clear(); return result; } private void CaptureSerializedRuntimeState() { pendingSerializedConnections.Clear(); pendingInputValues.Clear(); pendingOutputValues.Clear(); CapturePortState(InputPorts, pendingInputValues, captureConnections: false); CapturePortState(OutputPorts, pendingOutputValues, captureConnections: true); } private void CapturePortState(List ports, Dictionary valueStore, bool captureConnections) { if (ports == null) { return; } for (int i = 0; i < ports.Count; i++) { NodePort nodePort = ports[i]; if (nodePort == null || string.IsNullOrEmpty(nodePort.Name)) { continue; } valueStore[nodePort.Name] = nodePort.Value.Normalize(); if (!captureConnections || nodePort.Direction != PortDirection.Output || nodePort.Connections == null) { continue; } for (int j = 0; j < nodePort.Connections.Count; j++) { NodePort nodePort2 = nodePort.Connections[j]; if (nodePort2 != null && !(nodePort2.Owner == null) && !string.IsNullOrEmpty(nodePort2.Name)) { pendingSerializedConnections.Add(new NodeSerializedConnection { outputPortName = nodePort.Name, inputNode = nodePort2.Owner, inputPortName = nodePort2.Name }); } } } } private void BuildPorts() { InputPorts.Clear(); OutputPorts.Clear(); for (int i = 0; i < Definition.InputPorts.Count; i++) { NodePortDefinition nodePortDefinition = Definition.InputPorts[i]; pendingInputValues.TryGetValue(nodePortDefinition.PortName, out var value); InputPorts.Add(new NodePort { Index = i, Name = nodePortDefinition.PortName, Direction = PortDirection.Input, Kind = nodePortDefinition.Kind, Value = (value.IsInitialized ? value.Normalize() : BSValue.FromText(nodePortDefinition.DefaultValue)), Owner = this }); } for (int j = 0; j < Definition.OutputPorts.Count; j++) { NodePortDefinition nodePortDefinition2 = Definition.OutputPorts[j]; pendingOutputValues.TryGetValue(nodePortDefinition2.PortName, out var value2); OutputPorts.Add(new NodePort { Index = j, Name = nodePortDefinition2.PortName, Direction = PortDirection.Output, Kind = nodePortDefinition2.Kind, Value = (value2.IsInitialized ? value2.Normalize() : BSValue.FromText(nodePortDefinition2.DefaultValue)), Owner = this }); } } private void BuildVisuals() { ApplyBoardSize(); ApplyNodeName(); ClearChildren(InputRunPortParent); ClearChildren(InputValuePortParent); ClearChildren(OutputRunPortParent); ClearChildren(OutputValuePortParent); ClearChildren(InputTextParent); ClearChildren(OutputTextParent); ClearVisualCaches(); BuildInputVisuals(); BuildOutputVisuals(); CacheVisuals(); } private void ApplyBoardSize() { if (SmallBoard != null) { SmallBoard.SetActive(value: false); } if (MediumBoard != null) { MediumBoard.SetActive(value: false); } if (LargeBoard != null) { LargeBoard.SetActive(value: false); } if (ExtraLargeBoard != null) { ExtraLargeBoard.SetActive(value: false); } switch (BoardSize) { case NodeBoardSize.Small: if (SmallBoard != null) { SmallBoard.SetActive(value: true); } break; case NodeBoardSize.Medium: if (MediumBoard != null) { MediumBoard.SetActive(value: true); } break; case NodeBoardSize.Large: if (LargeBoard != null) { LargeBoard.SetActive(value: true); } break; case NodeBoardSize.ExtraLarge: if (ExtraLargeBoard != null) { ExtraLargeBoard.SetActive(value: true); } break; } } private void ApplyNodeName() { if (NodeNameText != null) { NodeNameText.text = ((Definition != null) ? Definition.NodeName : "Node"); } } private void BuildInputVisuals() { for (int i = 0; i < InputPorts.Count; i++) { NodePort nodePort = InputPorts[i]; bool num = nodePort.Kind == PortKind.Run; Transform transform = (num ? InputRunPortParent : InputValuePortParent); GameObject gameObject = (num ? InputRunPortPrefab : InputValuePortPrefab); if (!(transform == null) && !(gameObject == null)) { GameObject gameObject2 = UnityEngine.Object.Instantiate(gameObject, transform); gameObject2.name = nodePort.Name; Vector3 localPosition = gameObject2.transform.localPosition; localPosition.y = FirstPortY + PortYOffset * (float)i; gameObject2.transform.localPosition = localPosition; nodePort.AttachVisual(gameObject2.transform); nodePort.AttachVisualRenderers(gameObject2.GetComponentsInChildren(includeInactive: true)); if (PortTextPrefab != null && InputTextParent != null) { TMP_Text tMP_Text = UnityEngine.Object.Instantiate(PortTextPrefab, InputTextParent); tMP_Text.name = nodePort.Name + " Text"; Vector3 localPosition2 = tMP_Text.transform.localPosition; localPosition2.y = FirstTextY + TextYOffset * (float)i; tMP_Text.transform.localPosition = localPosition2; nodePort.AttachLabel(tMP_Text); UpdatePortLabel(nodePort); RegisterPortText(tMP_Text); } } } } private void BuildOutputVisuals() { for (int i = 0; i < OutputPorts.Count; i++) { NodePort nodePort = OutputPorts[i]; bool num = nodePort.Kind == PortKind.Run; Transform transform = (num ? OutputRunPortParent : OutputValuePortParent); GameObject gameObject = (num ? OutputRunPortPrefab : OutputValuePortPrefab); if (!(transform == null) && !(gameObject == null)) { GameObject gameObject2 = UnityEngine.Object.Instantiate(gameObject, transform); gameObject2.name = nodePort.Name; Vector3 localPosition = gameObject2.transform.localPosition; localPosition.y = FirstPortY + PortYOffset * (float)i; gameObject2.transform.localPosition = localPosition; nodePort.AttachVisual(gameObject2.transform); nodePort.AttachVisualRenderers(gameObject2.GetComponentsInChildren(includeInactive: true)); if (PortTextPrefab != null && OutputTextParent != null) { TMP_Text tMP_Text = UnityEngine.Object.Instantiate(PortTextPrefab, OutputTextParent); tMP_Text.name = nodePort.Name + " Text"; Vector3 localPosition2 = tMP_Text.transform.localPosition; localPosition2.y = FirstTextY + TextYOffset * (float)i; tMP_Text.transform.localPosition = localPosition2; nodePort.AttachLabel(tMP_Text); UpdatePortLabel(nodePort); RegisterPortText(tMP_Text); } } } } private void UpdatePortLabel(NodePort port) { if (port != null && !(port.LabelText == null)) { port.LabelText.text = ((port.Kind == PortKind.Value) ? (port.Name + ": " + FormatPortValue(port.Value)) : port.Name); } } private static string FormatPortValue(BSValue value) { value = value.Normalize(); return value.Kind switch { BSValueKind.None => "None", BSValueKind.Number => value.AsText(), BSValueKind.Text => string.IsNullOrEmpty(value.AsText()) ? "\"\"" : value.AsText(), BSValueKind.Bool => value.AsBool() ? "True" : "False", BSValueKind.Player => $"Player {value.AsPlayerId()}", BSValueKind.Group => $"Group {value.AsGroupId()}", _ => value.ToString(), }; } private void RegisterPortText(TMP_Text text) { if (!(text == null)) { cachedPortTexts.Add(text); if (!defaultTextColors.ContainsKey(text)) { defaultTextColors.Add(text, text.color); } } } private void ClearVisualCaches() { cachedPortTexts.Clear(); defaultTextColors.Clear(); cachedRenderers.Clear(); } private void CacheVisuals() { cachedRenderers.Clear(); cachedRenderers.AddRange(GetComponentsInChildren(includeInactive: true)); cachedPortTexts.Clear(); defaultTextColors.Clear(); TMP_Text[] componentsInChildren = GetComponentsInChildren(includeInactive: true); for (int i = 0; i < componentsInChildren.Length; i++) { RegisterPortText(componentsInChildren[i]); } } private void ApplySelectionVisual() { base.transform.localScale = defaultScale * (selected ? 1.035f : 1f); for (int i = 0; i < cachedPortTexts.Count; i++) { TMP_Text tMP_Text = cachedPortTexts[i]; if (!(tMP_Text == null)) { Color value; Color color = (defaultTextColors.TryGetValue(tMP_Text, out value) ? value : Color.white); tMP_Text.color = (selected ? SelectedTextColor : color); } } } private void ApplyVisibility() { for (int i = 0; i < cachedRenderers.Count; i++) { if (cachedRenderers[i] != null) { cachedRenderers[i].enabled = visibleInEditMode; } } for (int j = 0; j < cachedPortTexts.Count; j++) { if (cachedPortTexts[j] != null) { cachedPortTexts[j].enabled = visibleInEditMode; } } } private void UpdatePortVisualState() { UpdatePortVisualState(InputPorts); UpdatePortVisualState(OutputPorts); } private void UpdatePortTintState() { UpdatePortTintState(InputPorts); UpdatePortTintState(OutputPorts); } private void UpdatePortTintState(List ports) { for (int i = 0; i < ports.Count; i++) { NodePort nodePort = ports[i]; if (nodePort == null || nodePort.Kind != PortKind.Value || nodePort.VisualTransform == null) { continue; } if (nodePort.VisualRenderers == null) { nodePort.AttachVisualRenderers(nodePort.VisualTransform.GetComponentsInChildren(includeInactive: true)); } if (nodePort.VisualRenderers == null || nodePort.VisualRenderers.Length == 0) { continue; } BSValueKind kind = ((nodePort.Direction != PortDirection.Input) ? nodePort.Value.Normalize().Kind : ((nodePort.SourceGroupId > 0) ? BSValueKind.Group : ((nodePort.IncomingConnection == null) ? nodePort.Value.Normalize().Kind : nodePort.IncomingConnection.Value.Normalize().Kind))); Color color = NodeWirePalette.ForValueKind(kind); if (nodePort.LastTint == color) { continue; } nodePort.LastTint = color; NodePort nodePort2 = nodePort; if (nodePort2.VisualBlock == null) { nodePort2.VisualBlock = new MaterialPropertyBlock(); } nodePort.VisualBlock.Clear(); nodePort.VisualBlock.SetColor(BaseColorId, color); nodePort.VisualBlock.SetColor(ColorId, color); nodePort.VisualBlock.SetColor(TintColorId, color); for (int j = 0; j < nodePort.VisualRenderers.Length; j++) { Renderer renderer = nodePort.VisualRenderers[j]; if (!(renderer == null)) { renderer.SetPropertyBlock(nodePort.VisualBlock); Material sharedMaterial = renderer.sharedMaterial; if (sharedMaterial != null && !sharedMaterial.HasProperty(BaseColorId) && !sharedMaterial.HasProperty(ColorId) && !sharedMaterial.HasProperty(TintColorId)) { renderer.material.color = color; } } } } } private void UpdatePortVisualState(List ports) { for (int i = 0; i < ports.Count; i++) { NodePort nodePort = ports[i]; if (nodePort != null && !(nodePort.VisualTransform == null)) { float num = 0f; if (nodePort.PulseUntil > Time.unscaledTime) { num = Mathf.Clamp01((nodePort.PulseUntil - Time.unscaledTime) / 0.16f); } float num2 = (nodePort.Highlighted ? 1.25f : 1f); num2 += num * 0.22f; nodePort.VisualTransform.localScale = nodePort.BaseLocalScale * num2; } } } private void ClearChildren(Transform parent) { if (parent == null) { return; } for (int num = parent.childCount - 1; num >= 0; num--) { Transform child = parent.GetChild(num); if (!(child == null)) { if (Application.isPlaying) { UnityEngine.Object.Destroy(child.gameObject); } else { UnityEngine.Object.DestroyImmediate(child.gameObject); } } } } private void CreateBehavior() { if (behavior != null) { if (Application.isPlaying) { UnityEngine.Object.Destroy(behavior); } else { UnityEngine.Object.DestroyImmediate(behavior); } behavior = null; } if (Definition == null) { return; } Type type = FindType(Definition.BehaviorClassName); if (!(type == null) && typeof(NodeBehavior).IsAssignableFrom(type)) { behavior = base.gameObject.AddComponent(type) as NodeBehavior; if (behavior != null) { behavior.Node = this; } } } private static Type FindType(string typeName) { Type type = Type.GetType(typeName); if (type != null) { return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { type = assemblies[i].GetType(typeName); if (type != null) { return type; } } return null; } private static bool BSValueEquals(BSValue a, BSValue b) { a = a.Normalize(); b = b.Normalize(); if (a.Kind != b.Kind) { return false; } return a.Kind switch { BSValueKind.Number => Math.Abs(a.Number - b.Number) <= 0.0001, BSValueKind.Text => string.Equals(a.Text ?? string.Empty, b.Text ?? string.Empty, StringComparison.Ordinal), BSValueKind.Bool => a.Bool == b.Bool, BSValueKind.Player => a.PlayerId == b.PlayerId, BSValueKind.Group => a.GroupId == b.GroupId, _ => true, }; } }