#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using T3.Core.Logging; using T3.Core.Operator; using T3.Core.Operator.Slots; using T3.Core.Resource.Assets; using T3.Core.Stats; using T3.Core.Utils; namespace T3.Core.Resource; internal interface IResource { public IResourcePackage? OwningPackage { get; } } public sealed class Resource : IDisposable, IResource { IResourcePackage? IResource.OwningPackage => _owner?.Package; #region Constructors public Resource(InputSlot slot, TryGenerate tryGenerate, bool allowDisposal = true, EqualityComparer? comparer = null) : this(slot.Value, slot.Parent, tryGenerate, allowDisposal, comparer) { _slot = slot; } internal Resource(string? address, IResourceConsumer? owner, TryGenerate tryGenerate, bool allowDisposal = true, EqualityComparer? equalityComparer = null) { ArgumentNullException.ThrowIfNull(tryGenerate, nameof(tryGenerate)); _tryGenerate = tryGenerate; _equalityComparer = equalityComparer ?? EqualityComparer.Default; _owner = owner; _allowDisposal = allowDisposal; _onFileChanged = OnFileUpdate; _valueFactory = ValueFactory; _onDispose = OnOwnerDisposed; if (owner != null) owner.Disposing += _onDispose; SetAddress(address); _lazyValue = new Lazy(_valueFactory, ThreadSafetyMode); } #endregion public bool IsDisposed => _isDisposed; public T? Value { get { #if DEBUG if (_slot is not null && !_hasWarned) { Log.Warning($"[{_owner}] It is recommended to use {nameof(GetValue)} instead of the Value property when using a slot." + $" This is because the Value property will not update the slot value when the file is moved. " + $"Slot: {_slot}", _owner!); _hasWarned = true; } #endif ObjectDisposedException.ThrowIf(_isDisposed, this); return _lazyValue.Value; } } public T? GetValue(EvaluationContext context) { ObjectDisposedException.ThrowIf(_isDisposed, this); if (_slot is not { IsDirty: true }) return _lazyValue.Value; var oldValue = _slot.Value; var newAddress = _slot.GetValue(context); var hasChanged = oldValue != newAddress; if(!hasChanged) return _lazyValue.Value; if (!string.IsNullOrWhiteSpace(newAddress)) { if (_slot.HasInputConnections) { newAddress = newAddress.ToForwardSlashes(); } else { newAddress.ToForwardSlashesUnsafe(); } } SetAddress(newAddress); MarkFileAsChanged(); return _lazyValue.Value; } public bool TryGetValue(EvaluationContext context, [NotNullWhen(true)] out T? value) { value = GetValue(context); return !_equalityComparer.Equals(value, default); } public void AddDependentSlots(ISlot outputSlot) { if (outputSlot is IInputSlot) { throw new InvalidOperationException("Cannot add an input slot as a dependent slot - only outputs are permitted."); } _dependentSlots.Add(outputSlot); } public void AddDependentSlots(params ISlot[] slots) { foreach (var slot in slots) { AddDependentSlots(slot); } } #region File Handling public void MarkFileAsChanged() { ResetLazyValue(); // force-invalidate all dependent slots // it must be forced since this is called outside of the main invalidation loop // this isn't the prettiest solution but it works for now. foreach (var slot in _dependentSlots) slot.DirtyFlag.ForceInvalidate(); Changed?.Invoke(); return; void ResetLazyValue() { if (!_lazyValue.IsValueCreated) return; DisposeValue(); _lazyValue = new Lazy(_valueFactory, ThreadSafetyMode); } } /// /// Replaces the current file path with a new one and ensures we have a FileResource if possible /// /// Path to replace the current value of /// True if we have a file resource private bool SetAddress(string? newAddress) { // 1. Check if the address actually changed if (_asset != null && _userPath == newAddress) return true; ReleaseFileResource(); _userPath = newAddress; // 2. Use AssetRegistry as the primary lookup if (!AssetRegistry.TryGetAsset(newAddress, out var asset)) { // Fallback: If it's not in the registry, it might be an absolute path // or a new file not yet indexed. if (!FileResource.TryGetFileResource(newAddress, _owner, out var fileResource)) return false; _fileResource = fileResource; } else { _asset = asset; // 3. Map the Asset to a FileResource for change tracking if (!FileResource.TryGetFileResource(asset.Address, _owner, out var fileResource)) return false; _fileResource = fileResource; } _fileResource.Claim(this); _fileResource.FileChanged += _onFileChanged; return true; } private void ReleaseFileResource() { if (_fileResource == null) return; _fileResource.FileChanged -= _onFileChanged; _fileResource.Release(this); _fileResource = null; } private void OnFileUpdate(object? sender, WatcherChangeTypes changeTypes) { //Log.Debug($"Resource file '{_fileResource!.AbsolutePath}' changed: {changeTypes}"); // change type-in value to the new path if (changeTypes.WasMoved()) { // We know the fileResource is not null because this is only called by the file resource var newPath = _fileResource!.AbsolutePath; if (AssetRegistry.TryConvertFilepathToAddress(newPath, false, out var relativePath, out _)) { newPath = relativePath; } _userPath = newPath; if (_slot is { HasInputConnections: false }) _slot.SetTypedInputValue(newPath.ToForwardSlashes()); } MarkFileAsChanged(); } #endregion #region Value Evaluation private T? ValueFactory() { string? failureReason; bool success; T? newValue; var hasFileResource = _fileResource != null || SetAddress(_userPath); if (hasFileResource && _fileResource != null && _fileResource!.FileInfo is { Exists : true }) { try { // get current value without creating it if it hasn't been created yet var currentValue = _lazyValue.IsValueCreated ? _lazyValue.Value : default; success = _tryGenerate.Invoke(_fileResource, currentValue, out newValue, out failureReason); } catch (Exception e) { failureReason = e.Message; success = false; newValue = default; } } else { success = false; // #if !DEBUG failureReason = "File not found"; // #else // failureReason = "File not found:\n" + Environment.StackTrace; // #endif newValue = default; } if (success) return newValue; // Log error state with readable warning var absolute = _fileResource?.AbsolutePath !=null ? $"'{_fileResource?.AbsolutePath}'" :string.Empty; if (string.IsNullOrEmpty(absolute)) return newValue; var reason = failureReason ?? "Failed"; var errorLog = $"{reason} '{_userPath}' {absolute} for {typeof(T).Name}"; if (_owner != null) { if (_owner is Instance instance) { instance.LogErrorState(errorLog); } else { Log.Error(errorLog, _owner); } } else { Log.Error(errorLog); } return newValue; } #endregion #region Value Disposal private void OnOwnerDisposed(IResourceConsumer owner) { if(owner != _owner) throw new Exception($"Owner mismatch: {owner} != {_owner}"); Dispose(); } public void Dispose() { if (_isDisposed) return; _isDisposed = true; ReleaseFileResource(); if (DisposeValue()) Changed?.Invoke(); if (_owner != null) _owner.Disposing -= _onDispose; Changed = null; } /// /// Returns true if item was disposed and changed /// /// private bool DisposeValue() { if (!_allowDisposal || !IsDisposableType) return false; if (_lazyValue is not { IsValueCreated: true }) return false; var value = _lazyValue.Value; if (_equalityComparer.Equals(value, default)) return false; ((IDisposable)value!).Dispose(); return true; } #endregion // type handling private static readonly bool IsDisposableType = typeof(IDisposable).IsAssignableFrom(typeof(T)); private readonly EqualityComparer _equalityComparer; // file handling private Asset? _asset; // New: Reference the Asset directly private FileResource? _fileResource; // We keep this for now to handle live hooks private readonly EventHandler _onFileChanged; private readonly IResourceConsumer? _owner; private string? _userPath; // The address (e.g., "lib:shaders/test.hlsl") private readonly InputSlot? _slot; // Value handling private Lazy _lazyValue; private readonly Func _valueFactory; private bool _isDisposed; private readonly TryGenerate _tryGenerate; private readonly Action _onDispose; private readonly bool _allowDisposal; private const LazyThreadSafetyMode ThreadSafetyMode = LazyThreadSafetyMode.None; public event Action? Changed; private readonly List _dependentSlots = new(); #if DEBUG private bool _hasWarned; #endif } public delegate bool TryGenerate(FileResource file, T? currentValue, [NotNullWhen(true)] out T? newValue, [NotNullWhen(false)] out string? failureReason);