// TODO: fix nullable here //#nullable enable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using T3.Core.Logging; using T3.Core.Model; using T3.Core.Operator.Slots; using T3.Core.Resource; using T3.Core.Settings; namespace T3.Core.Operator; /// /// Represents the definition of an operator. It can include: /// - s that references other Symbols /// - s that connect these children /// /// /// - There can be multiple s of a symbol. /// public sealed partial class Symbol : IDisposable, IResource { #region Saved Properties public readonly Guid Id; public IReadOnlyDictionary Children => _children; public IReadOnlyDictionary ChildrenCreatedFromMe => _childrenCreatedFromMe; public IEnumerable InstancesOfSelf { get { lock (_creationLock) { return _childrenCreatedFromMe.Values.SelectMany(x => x.Instances); } } } public readonly List Connections = []; /// /// Inputs of this symbol. input values are the default values (exist only once per symbol) /// public readonly List InputDefinitions = new(); public readonly List OutputDefinitions = new(); #endregion Saved Properties /// /// Number of child operators that could not be resolved when this symbol was loaded (their /// package or symbol was missing). Set during deserialization; reset by a fresh load. When /// > 0 the editor refuses to overwrite this symbol's files, so the intact on-disk copy — /// which still holds those children and their connections — is never truncated. /// public int UnresolvedChildCount { get; internal set; } public bool HasUnresolvedChildren => UnresolvedChildCount > 0; public string Name => InstanceType?.Name; public string Namespace => InstanceType.Namespace ?? SymbolPackage.AssemblyInformation.Name; public Animator Animator { get; private set; } = new(); /// /// Per-symbol project / playback configuration. Always non-null and auto-initialised /// (including on deserialization — see ). Whether the /// settings are actually active for this symbol's subtree is governed by /// , not by nullability. /// public CompositionSettings CompositionSettings { get; set; } = new(); public SymbolPackage SymbolPackage { get; set; } IResourcePackage IResource.OwningPackage => SymbolPackage; public Type InstanceType { get; private set; } private bool IsGeneric => InstanceType.IsGenericTypeDefinition; internal Symbol(Type instanceType, Guid symbolId, SymbolPackage symbolPackage) { Id = symbolId; _parentlessIdPath = [Child.CreateIdDeterministically(this, null)]; UpdateTypeWithoutUpdatingDefinitionsOrInstances(instanceType, symbolPackage); if (instanceType == typeof(object)) return; if (symbolPackage != null) { UpdateInstanceType(false); } } internal void UpdateTypeWithoutUpdatingDefinitionsOrInstances(Type type, SymbolPackage symbolPackage) { SymbolPackage = symbolPackage; // we re-assign this here because symbols can be moved from one package to another ApplyInstanceType(type); NeedsTypeUpdate = true; } private void ApplyInstanceType(Type value) { InstanceType = value; if (value == null) return; if(!value.IsAssignableTo(typeof(Instance))) { return; } // set type Symbol static field (TypeClass.StaticSymbol field) const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy; var field = value.GetField("StaticSymbol", flags); field!.SetValue(null, this); } public void Dispose() { lock (_creationLock) { var children = _children.Values.ToArray(); for (var index = 0; index < children.Length; index++) { var child = children[index]; _children.Remove(child.Id, out _); child.Dispose(); } foreach (var child in _childrenCreatedFromMe.Values) { child.Dispose(); } _childrenCreatedFromMe.Clear(); } } public int GetMultiInputIndexFor(Connection con) { return Connections.FindAll(c => c.TargetParentOrChildId == con.TargetParentOrChildId && c.TargetSlotId == con.TargetSlotId) .FindIndex(cc => cc == con); // todo: fix this mess! connection rework! } public void SortInputSlotsByDefinitionOrder() { lock (_creationLock) { foreach (var child in _childrenCreatedFromMe.Values) { child.SortInputSlotsByDefinitionOrder(); } } } public override string ToString() => $"{Namespace}.[{Name}]"; public bool IsTargetMultiInput(Connection connection) { return Children.TryGetValue(connection.TargetParentOrChildId, out var child) && child.Inputs.TryGetValue(connection.TargetSlotId, out var targetSlot) && targetSlot.IsMultiInput; } /// /// Add connection to symbol and its instances /// /// All connections of a symbol are stored in a single List, from which sorting of multi-inputs /// is define. That why inserting connections for those requires to first find the correct index within that /// larger list. /// public void AddConnection(Connection connection, int multiInputIndex = 0) { var isMultiInput = IsTargetMultiInput(connection); // Check if another connection is already existing to the target input, ignoring multi inputs for now var connectionsAtInput = Connections.FindAll(c => (c.TargetParentOrChildId == connection.TargetParentOrChildId || c.TargetParentOrChildId == Guid.Empty) && c.TargetSlotId == connection.TargetSlotId); if (multiInputIndex > connectionsAtInput.Count) { // todo - solve is to ensure that the multi-input slots aren't cleared of quantity when recompiling, or rather are populated in order Log.Error($"Trying to add a connection at the index {multiInputIndex}. Out of bound of the {connectionsAtInput.Count} existing connections."); return; } if (!isMultiInput) { // Replace existing on single inputs if (connectionsAtInput.Count > 0) { RemoveConnection(connectionsAtInput[0]); } Connections.Add(connection); } else { var append = multiInputIndex == connectionsAtInput.Count; if (append) { if (connectionsAtInput.Count == 0) { Connections.Add(connection); } else { Connections.Add( connection); } } else { // Use the target index to find the existing successor among the connections. // Compare by reference: duplicate connections from the same source are value-equal, // so == would match the first duplicate and insert at the wrong position. var existingConnection = connectionsAtInput[multiInputIndex]; var insertIndex = Connections.FindIndex(c => ReferenceEquals(c, existingConnection)); Connections.Insert(insertIndex, connection); } } lock (_creationLock) { foreach (var child in _childrenCreatedFromMe.Values) { child.AddConnectionToInstances(connection, multiInputIndex, true); } } } public void RemoveConnection(Connection connection, int multiInputIndex = 0) { var targetParentOrChildId = connection.TargetParentOrChildId; var targetSlotId = connection.TargetSlotId; List connectionsAtInput = new(); var connections = Connections; foreach (var potentialConnection in connections) { if (potentialConnection.TargetParentOrChildId == targetParentOrChildId && potentialConnection.TargetSlotId == targetSlotId) { connectionsAtInput.Add(potentialConnection); } } var connectionsAtInputCount = connectionsAtInput.Count; if (connectionsAtInputCount == 0 || multiInputIndex >= connectionsAtInputCount) { Log.Error($"Trying to remove a connection that doesn't exist. Index {multiInputIndex} of {connectionsAtInput.Count}"); return; } var existingConnection = connectionsAtInput[multiInputIndex]; // Compare by reference: duplicate connections from the same source are value-equal, // so == would remove the first duplicate and reorder the multi-input. bool removed = false; var connectionCount = connections.Count; for (var index = 0; index < connectionCount; index++) { if (ReferenceEquals(connections[index], existingConnection)) { connections.RemoveAt(index); removed = true; break; } } if (!removed) { Log.Warning($"Failed to remove connection."); return; } lock (_creationLock) { foreach (var child in _childrenCreatedFromMe.Values) { child.RemoveConnectionFromInstances(connection, multiInputIndex); } } } public void CreateOrUpdateActionsForAnimatedChildren() { foreach (var instance in InstancesOfSelf) { Animator.CreateUpdateActionsForExistingCurves(instance.Children.Values); } } internal void CreateAnimationUpdateActionsForSymbolInstances() { var foundParentsOfMyInstances = new HashSet(); foreach (var instance in InstancesOfSelf) { var parent = instance.Parent; if (parent != null) { var parentSymbol = parent.Symbol; if (foundParentsOfMyInstances.Add(parentSymbol)) { parentSymbol.CreateOrUpdateActionsForAnimatedChildren(); } } } } public bool RemoveChild(Guid childId) { // first remove all connections to or from the child Connections.RemoveAll(c => c.SourceParentOrChildId == childId || c.TargetParentOrChildId == childId); if (!_children.Remove(childId, out var symbolChild)) return false; lock (_creationLock) { foreach (var me in _childrenCreatedFromMe.Values) { me.RemoveChildInstancesOf(symbolChild); } } SymbolPackage.RemoveDependencyOn(symbolChild.Symbol); return true; } public InputDefinition GetInputMatchingType(Type type) { foreach (var inputDefinition in InputDefinitions) { if (type == null || inputDefinition.DefaultValue.ValueType == type) return inputDefinition; } return null; } public OutputDefinition GetOutputMatchingType(Type type) { foreach (var outputDefinition in OutputDefinitions) { if (type == null || outputDefinition.ValueType == type) return outputDefinition; } return null; } public void InvalidateInputInAllChildInstances(IInputSlot inputSlot) { var childId = inputSlot.Parent.SymbolChildId; var inputId = inputSlot.Id; InvalidateInputInAllChildInstances(inputId, childId); } public void InvalidateInputInAllChildInstances(Guid inputId, Guid childId) { lock (_creationLock) { foreach (var parent in _childrenCreatedFromMe.Values) { parent.InvalidateInputInChildren(inputId, childId); } } } /// /// Invalidates all instances of a symbol input (e.g. if that input's default was modified) /// public void InvalidateInputDefaultInInstances(IInputSlot inputSlot) => InvalidateInputDefaultInInstances(inputSlot.Id); public void InvalidateInputDefaultInInstances(in Guid inputId) { lock (_creationLock) { foreach (var child in _childrenCreatedFromMe.Values) { child.InvalidateInputDefaultInInstances(inputId); } } } /// /// Mirror of this symbol's editor-side SymbolUi.VersionCounter, forwarded by /// SymbolUi.FlagAsModified. Lets Core operators (and the Player, where it stays 0 because the graph /// is static) cache per-frame child scans and rebuild only when the symbol actually changes, instead of /// re-walking Children every frame. Bumps on any modification — coarser than "structure only", but /// modifications are rare relative to frames, so the occasional extra cache rebuild is cheap. /// public int VersionCounter { get; set; } internal bool NeedsTypeUpdate { get; private set; } = true; private readonly ConcurrentDictionary _children = new(); private readonly Dictionary _childrenCreatedFromMe = new(); private void ReplaceConnection(Connection con) { if(TryGetMultiInputIndexOf(con, out var foundAtConnectionIndex, out _)) { Connections.RemoveAt(foundAtConnectionIndex); Connections.Insert(foundAtConnectionIndex, con); } else { Log.Error($"Failed to replace connection {con} in symbol {this}. Connection not found."); } } internal void ReconnectAll() { lock (_creationLock) { foreach(var child in _childrenCreatedFromMe.Values) { child.ReconnectAllChildren(); } } } }