Files
2026-07-13 13:15:05 +08:00

380 lines
12 KiB
C#

#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<T> : IDisposable, IResource
{
IResourcePackage? IResource.OwningPackage => _owner?.Package;
#region Constructors
public Resource(InputSlot<string> slot,
TryGenerate<T> tryGenerate,
bool allowDisposal = true,
EqualityComparer<T?>? comparer = null)
: this(slot.Value, slot.Parent, tryGenerate, allowDisposal, comparer)
{
_slot = slot;
}
internal Resource(string? address, IResourceConsumer? owner, TryGenerate<T> tryGenerate,
bool allowDisposal = true, EqualityComparer<T?>? equalityComparer = null)
{
ArgumentNullException.ThrowIfNull(tryGenerate, nameof(tryGenerate));
_tryGenerate = tryGenerate;
_equalityComparer = equalityComparer ?? EqualityComparer<T?>.Default;
_owner = owner;
_allowDisposal = allowDisposal;
_onFileChanged = OnFileUpdate;
_valueFactory = ValueFactory;
_onDispose = OnOwnerDisposed;
if (owner != null)
owner.Disposing += _onDispose;
SetAddress(address);
_lazyValue = new Lazy<T?>(_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<T?>(_valueFactory, ThreadSafetyMode);
}
}
/// <summary>
/// Replaces the current file path with a new one and ensures we have a FileResource if possible
/// </summary>
/// <param name="newAddress">Path to replace the current value of <see cref="_userPath"/></param>
/// <returns>True if we have a file resource</returns>
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;
}
/// <summary>
/// Returns true if item was disposed and changed
/// </summary>
/// <returns></returns>
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<T?> _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<WatcherChangeTypes> _onFileChanged;
private readonly IResourceConsumer? _owner;
private string? _userPath; // The address (e.g., "lib:shaders/test.hlsl")
private readonly InputSlot<string>? _slot;
// Value handling
private Lazy<T?> _lazyValue;
private readonly Func<T?> _valueFactory;
private bool _isDisposed;
private readonly TryGenerate<T> _tryGenerate;
private readonly Action<IResourceConsumer> _onDispose;
private readonly bool _allowDisposal;
private const LazyThreadSafetyMode ThreadSafetyMode = LazyThreadSafetyMode.None;
public event Action? Changed;
private readonly List<ISlot> _dependentSlots = new();
#if DEBUG
private bool _hasWarned;
#endif
}
public delegate bool TryGenerate<T>(FileResource file,
T? currentValue,
[NotNullWhen(true)] out T? newValue,
[NotNullWhen(false)] out string? failureReason);