using T3.Core.Utils; // Note: The original code had a using statement for a specific animation library. // This is kept for completeness, but may not be necessary if TriggerAnim is not used elsewhere. // using static Lib.numbers.anim.animators.TriggerAnim; namespace Lib.numbers.ints { [Guid("ca6f09ec-bbc4-4365-8210-bc10cd8d9f94")] internal sealed class MergeIntLists : Instance, IStatusProvider { private readonly List _ltpCombinedList = new(); // Persistent state for LTP private readonly List> _previousSourceLists = new(); // Persistent state for FailOver change detection [Input(Guid = "24855D2A-595B-4E3E-81C5-65481C262F64")] public readonly InputSlot Enabled = new(); [Input(Guid = "BDFE5576-2F45-473D-BB9D-95FC453FC774")] public readonly MultiInputSlot> InputLists = new(); [Input(Guid = "9E60F3E7-A891-4E3E-81C5-65481C262F64")] // Using a new GUID as MaxSize conflicts (24855D2A-595B-4E3E-81C5-65481C262F64) public readonly InputSlot MaxSize = new(); [Input(Guid = "e3315721-2853-449a-af6a-43cd18400470", MappedType = typeof(MergeModes))] public readonly InputSlot MergeMode = new(); [Output(Guid = "F28370F0-F0C6-418F-8FBF-167A7D1035FE")] public readonly Slot> Result = new(); [Input(Guid = "387FB1DB-944F-4EB1-BB6F-B149E4A51A42")] public readonly InputSlot> StartIndices = new(); // --- STATE-TRACKING FIELDS --- private int _activeFailoverIndex; private string _lastErrorMessage = string.Empty; public MergeIntLists() { Result.UpdateAction += Update; } public IStatusProvider.StatusLevel GetStatusLevel() { return string.IsNullOrEmpty(_lastErrorMessage) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Warning; } public string GetStatusMessage() { return _lastErrorMessage; } private void Update(EvaluationContext context) { _lastErrorMessage = string.Empty; var inputListSlots = InputLists.GetCollectedTypedInputs(); if (inputListSlots == null || inputListSlots.Count == 0) { Result.Value?.Clear(); _activeFailoverIndex = 0; // Reset FailOver state when disconnected _ltpCombinedList.Clear(); // Reset LTP state when disconnected _previousSourceLists.Clear(); // Reset FailOver change detection state when disconnected return; } Result.Value ??= new List(); var resultList = Result.Value; resultList.Clear(); // Clear the output list for the current frame var mergeModesEnabled = Enabled.GetValue(context); if (!mergeModesEnabled) { UpdateAppend(context, resultList, inputListSlots); return; } var mode = (MergeModes)MergeMode.GetValue(context); // Note: We get the lists themselves here, not the slots, for processing // Filter out null lists at this stage to simplify subsequent logic var sourceLists = inputListSlots.Select(slot => slot.GetValue(context)).ToList(); // If no valid source lists after filtering, handle gracefully if (!sourceLists.Any(l => l != null && l.Any())) // Check if there's *any* non-empty list { // Only clear _ltpCombinedList if it's truly empty or if there's no active input to justify its state // However, the general update logic already clears on full disconnect. // For LTP, if all inputs suddenly become empty, it should retain its last state (values persist) // but the output might be empty or reflect previous values. // Let's refine this - if `sourceLists` contains ONLY nulls or empty lists, we should still handle `_ltpCombinedList` // but `resultList` will likely be empty. // The current LTP implementation below effectively handles this: `currentMaxInputLength` would be 0, so _ltpCombinedList doesn't grow, // and it would output the _ltpCombinedList, which would be empty if it started empty. // If `validLists.Any()` is false inside `UpdateLtp`, it would leave `_ltpCombinedList` as is. // This block is only if there are NO inputs *at all*, the one above handles that. // If inputs exist but are all empty, LTP/HTP/Avg might produce an empty list, FailOver might keep last state. // The current approach delegates specific handling to each UpdateX method. } try { switch (mode) { case MergeModes.Htp: UpdateHtp(sourceLists, resultList); break; case MergeModes.Ltp: UpdateLtp(sourceLists, resultList); // Corrected LTP method break; case MergeModes.FailOver: UpdateFailOver(sourceLists, resultList); // Corrected FailOver method break; case MergeModes.Average: UpdateAverage(sourceLists, resultList); break; case MergeModes.Append: default: // The Append method needs the slots for its logic, not the processed lists UpdateAppend(context, resultList, inputListSlots); break; } } catch (Exception e) { Log.Warning("Failed to merge lists: " + e.Message, this); _lastErrorMessage = e.Message; } } private void UpdateHtp(List> sourceLists, List resultList) { var validLists = sourceLists.Where(l => l != null).ToList(); if (!validLists.Any()) return; var maxLength = validLists.Max(list => list.Count); for (var i = 0; i < maxLength; i++) { var maxValue = int.MinValue; var valueFound = false; foreach (var sourceList in validLists) { if (i < sourceList.Count) { if (!valueFound || sourceList[i] > maxValue) { maxValue = sourceList[i]; } valueFound = true; } } resultList.Add(valueFound ? maxValue : 0); } } // Corrected LTP method with state persistence private void UpdateLtp(List> sourceLists, List resultList) { var validLists = sourceLists.Where(l => l != null).ToList(); // 1. Determine the maximum length required by any currently connected valid input list. int currentMaxInputLength = 0; if (validLists.Any()) { currentMaxInputLength = validLists.Max(list => list.Count); } // 2. Ensure _ltpCombinedList is large enough to accommodate at least the current maximum input length. // It only grows here, preserving any values at indices beyond current inputs from previous frames. while (_ltpCombinedList.Count < currentMaxInputLength) { _ltpCombinedList.Add(0); // Pad new elements with a default value } // 3. Update _ltpCombinedList with values from current inputs. // The order of 'validLists' (derived from InputLists) determines precedence. // Values from later lists will overwrite values from earlier lists at the same index. foreach (var sourceList in validLists) { for (var i = 0; i < sourceList.Count; i++) { // Ensure we only write within the current bounds of _ltpCombinedList, // which has already been expanded to at least currentMaxInputLength. // This means values at indices not covered by current inputs will persist. _ltpCombinedList[i] = sourceList[i]; } } // 4. The resultList should reflect the full current state of _ltpCombinedList. // This ensures that persistent values (even if current inputs are shorter) are output. resultList.AddRange(_ltpCombinedList); } // Corrected FailOver method with change detection and prioritization private void UpdateFailOver(List> sourceLists, List resultList) { // Ensure our state-tracking list has the same number of entries as the source lists while (_previousSourceLists.Count < sourceLists.Count) { _previousSourceLists.Add(null); } while (_previousSourceLists.Count > sourceLists.Count) { _previousSourceLists.RemoveAt(_previousSourceLists.Count - 1); } // Determine if the currently active list has changed var activeListHasChanged = false; if (_activeFailoverIndex >= 0 && _activeFailoverIndex < sourceLists.Count) { var currentActiveList = sourceLists[_activeFailoverIndex]; var previousActiveList = _previousSourceLists[_activeFailoverIndex]; activeListHasChanged = currentActiveList is { Count: > 0 } && !currentActiveList.SequenceEqual(previousActiveList ?? Empty()); } // High-priority check: ALWAYS check if the first list is active again (non-empty and changing). // This ensures it returns to primary when it recovers. var firstList = sourceLists.FirstOrDefault(); var previousFirstList = _previousSourceLists.FirstOrDefault(); if (firstList is { Count: > 0 } && !firstList.SequenceEqual(previousFirstList ?? Empty())) { _activeFailoverIndex = 0; // Switch back to the primary list } // If the primary isn't active, and our current active list has stopped changing, find the next active one. else if (!activeListHasChanged) { var foundNextActive = false; // Search for the first valid (non-null, non-empty, and changing) list in order of priority. for (var i = 0; i < sourceLists.Count; i++) { var nextList = sourceLists[i]; var prevNextList = (i < _previousSourceLists.Count) ? _previousSourceLists[i] : null; if (nextList is { Count: > 0 } && !nextList.SequenceEqual(prevNextList ?? Empty())) { _activeFailoverIndex = i; // Switch to the new active list foundNextActive = true; break; } } // If no list has changed, stick to the current index unless it's invalid. // If the current index is out of bounds (e.g., input counts changed), reset to 0. if (!foundNextActive && _activeFailoverIndex >= sourceLists.Count) { _activeFailoverIndex = 0; } // If no list is active and currently selected list becomes empty, default to 0. if (!foundNextActive && (_activeFailoverIndex >= sourceLists.Count || sourceLists[_activeFailoverIndex] is not { Count: > 0 })) { _activeFailoverIndex = 0; } } // Use the determined active list for the output if (_activeFailoverIndex >= 0 && _activeFailoverIndex < sourceLists.Count) { var finalList = sourceLists[_activeFailoverIndex]; if (finalList != null) { resultList.AddRange(finalList); } } // Crucial final step: update the previous state for the next frame's comparison. // Deep copy lists to prevent mutation issues. for (var i = 0; i < sourceLists.Count; i++) { _previousSourceLists[i] = sourceLists[i] != null ? new List(sourceLists[i]) : null; } } private void UpdateAverage(List> sourceLists, List resultList) { var validLists = sourceLists.Where(l => l != null).ToList(); if (!validLists.Any()) return; var maxLength = validLists.Max(list => list.Count); for (var i = 0; i < maxLength; i++) { long sum = 0; var count = 0; foreach (var sourceList in validLists) { if (i < sourceList.Count) { sum += sourceList[i]; count++; } } resultList.Add(count > 0 ? (int)(sum / count) : 0); } } private void UpdateAppend(EvaluationContext context, List list, List>> inputListSlots) { var listNeedsCleanup = StartIndices.DirtyFlag.IsDirty; var maxSize = MaxSize.GetValue(context); var useMaxSize = maxSize >= 0; if (useMaxSize && maxSize != list.Count || listNeedsCleanup) { list.Clear(); list.Capacity = maxSize.Clamp(8, 1024 * 1024); for (var i = 0; i < maxSize; i++) { list.Add(0); } } var startIndices = StartIndices.GetValue(context) ?? new List(); var writeIndex = 0; for (var listIndex = 0; listIndex < inputListSlots.Count; listIndex++) { var source = inputListSlots[listIndex].GetValue(context); if (source == null || source.Count == 0) continue; if (listIndex < startIndices.Count) { var newStartIndex = startIndices[listIndex]; if (newStartIndex < 0) { _lastErrorMessage = $"Skipped negative start index {newStartIndex}"; } else if (useMaxSize && newStartIndex >= maxSize) { _lastErrorMessage = $"Skipped start index {newStartIndex} exceeding maxSize {maxSize}"; } else { writeIndex = newStartIndex; } } if (useMaxSize) { for (var indexInSource = 0; indexInSource < source.Count && writeIndex < maxSize; indexInSource++) { if (writeIndex >= 0) list[writeIndex] = source[indexInSource]; writeIndex++; } if (writeIndex >= maxSize) { _lastErrorMessage = $"Index exceeds max size of {maxSize}"; } } else { for (var indexInSource = 0; indexInSource < source.Count; indexInSource++) { var value = source[indexInSource]; if (writeIndex < list.Count) { list[writeIndex++] = value; } else { while (writeIndex > list.Count) { list.Add(-1); // Padding for non-contiguous appends } list.Add(value); writeIndex++; } } } } } private enum MergeModes { Append, Htp, Ltp, FailOver, Average } } }