using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.Client.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Robust.Client.ResourceManagement;
///
/// Handles caching of
///
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
private readonly Dictionary _cachedResources = new();
private readonly Dictionary _fallbacks = new();
public T GetResource(string path, bool useFallback = true) where T : BaseResource, new()
{
return GetResource(new ResPath(path), useFallback);
}
public T GetResource(ResPath path, bool useFallback = true) where T : BaseResource, new()
{
var cache = GetTypeData();
if (cache.Resources.TryGetValue(path, out var cached))
{
return (T) cached;
}
var resource = new T();
try
{
var dependencies = IoCManager.Instance!;
resource.Load(dependencies, path);
cache.Resources[path] = resource;
return resource;
}
catch (Exception e)
{
if (useFallback && resource.Fallback != null)
{
Sawmill.Error(
$"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}");
return GetResource(resource.Fallback.Value, false);
}
else
{
Sawmill.Error(
$"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}");
throw;
}
}
}
public bool TryGetResource(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
return TryGetResource(new ResPath(path), out resource);
}
public bool TryGetResource(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
var cache = GetTypeData();
if (cache.Resources.TryGetValue(path, out var cached))
{
resource = (T) cached;
return true;
}
if (cache.NonExistent.Contains(path))
{
resource = null;
return false;
}
var _resource = new T();
try
{
var dependencies = IoCManager.Instance!;
_resource.Load(dependencies, path);
resource = _resource;
cache.Resources[path] = resource;
return true;
}
catch (FileNotFoundException)
{
cache.NonExistent.Add(path);
resource = null;
return false;
}
catch (Exception e)
{
Sawmill.Error($"Exception while loading resource {typeof(T)} at '{path}'\n{e}");
resource = null;
return false;
}
}
public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioResource? resource)
{
resource = new AudioResource(stream);
return true;
}
public bool TryRemoveResource(string path) where T : BaseResource, IBaseResource, new()
=> TryRemoveResource(new ResPath(path));
public bool TryRemoveResource(ResPath path) where T : BaseResource, IBaseResource, new()
{
if (!T.CanBeRemoved)
throw new NotSupportedException($"Resource type '{typeof(T)}' does not support deterministic removal.");
if (new T().Fallback == path)
return false;
var cache = GetTypeData();
if (!cache.Resources.Remove(path, out var resource))
return false;
cache.NonExistent.Remove(path);
resource.Dispose();
return true;
}
public void ReloadResource(string path) where T : BaseResource, new()
{
ReloadResource(new ResPath(path));
}
public void ReloadResource(ResPath path) where T : BaseResource, new()
{
var cache = GetTypeData();
if (!cache.Resources.TryGetValue(path, out var res))
{
return;
}
try
{
var dependencies = IoCManager.Instance!;
res.Reload(dependencies, path);
}
catch (Exception e)
{
Sawmill.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}");
throw;
}
}
public bool HasResource(string path) where T : BaseResource, new()
{
return HasResource(new ResPath(path));
}
public bool HasResource(ResPath path) where T : BaseResource, new()
{
return TryGetResource(path, out var _);
}
public void CacheResource(string path, T resource) where T : BaseResource, new()
{
CacheResource(new ResPath(path), resource);
}
public void CacheResource(ResPath path, T resource) where T : BaseResource, new()
{
GetTypeData().Resources[path] = resource;
}
public T GetFallback() where T : BaseResource, new()
{
if (_fallbacks.TryGetValue(typeof(T), out var fallback))
{
return (T) fallback;
}
var res = new T();
if (res.Fallback == null)
{
throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback.");
}
fallback = GetResource(res.Fallback.Value, useFallback: false);
_fallbacks.Add(typeof(T), fallback);
return (T) fallback;
}
public IEnumerable> GetAllResources() where T : BaseResource, new()
{
return GetTypeData().Resources.Select(p => new KeyValuePair(p.Key, (T) p.Value));
}
public event Action? OnRawTextureLoaded;
public event Action? OnRsiLoaded;
#region IDisposable Members
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Resources.Values))
{
res.Dispose();
}
}
disposed = true;
}
~ResourceCache()
{
Dispose(false);
}
#endregion IDisposable Members
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TypeData GetTypeData()
{
return _cachedResources.GetOrNew(typeof(T));
}
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
{
OnRawTextureLoaded?.Invoke(eventArgs);
}
public void RsiLoaded(RsiLoadedEventArgs eventArgs)
{
OnRsiLoaded?.Invoke(eventArgs);
}
private sealed class TypeData
{
public readonly Dictionary Resources = new();
// List of resources which DON'T exist.
// Needed to avoid innocuous TryGet calls repeatedly trying to re-load non-existent resources from disk.
public readonly HashSet NonExistent = new();
}
}