summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs22
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs14
-rw-r--r--src/SMAPI/Framework/ContentPack.cs38
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModContentHelper.cs16
-rw-r--r--src/SMAPI/Framework/SCore.cs15
-rw-r--r--src/SMAPI/Utilities/CaseInsensitivePathCache.cs124
6 files changed, 191 insertions, 38 deletions
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 8483d45d..cd1de4a8 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -15,7 +15,7 @@ using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization;
-using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.GameData;
using xTile;
@@ -80,6 +80,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The cached asset load/edit operations to apply, indexed by asset name.</summary>
private readonly TickCacheDictionary<IAssetName, AssetOperationGroup[]> AssetOperationsByKey = new();
+ /// <summary>The previously created case-insensitive path caches by root path.</summary>
+ private readonly Dictionary<string, CaseInsensitivePathCache> CaseInsensitivePathCaches = new(StringComparer.OrdinalIgnoreCase);
+
/*********
** Accessors
@@ -91,9 +94,11 @@ namespace StardewModdingAPI.Framework
public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language;
/// <summary>Interceptors which provide the initial versions of matching assets.</summary>
+ [Obsolete]
public IList<ModLinked<IAssetLoader>> Loaders { get; } = new List<ModLinked<IAssetLoader>>();
/// <summary>Interceptors which edit matching assets after they're loaded.</summary>
+ [Obsolete]
public IList<ModLinked<IAssetEditor>> Editors { get; } = new List<ModLinked<IAssetEditor>>();
/// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
@@ -205,7 +210,8 @@ namespace StardewModdingAPI.Framework
reflection: this.Reflection,
jsonHelper: this.JsonHelper,
onDisposing: this.OnDisposing,
- aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations
+ aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations,
+ relativePathCache: this.GetCaseInsensitivePathCache(rootDirectory)
);
this.ContentManagers.Add(manager);
return manager;
@@ -477,6 +483,18 @@ namespace StardewModdingAPI.Framework
});
}
+ /// <summary>Get a dictionary of relative paths within a root path, for case-insensitive file lookups.</summary>
+ /// <param name="rootPath">The root path to scan.</param>
+ public CaseInsensitivePathCache GetCaseInsensitivePathCache(string rootPath)
+ {
+ rootPath = PathUtilities.NormalizePath(rootPath);
+
+ if (!this.CaseInsensitivePathCaches.TryGetValue(rootPath, out CaseInsensitivePathCache cache))
+ this.CaseInsensitivePathCaches[rootPath] = cache = new CaseInsensitivePathCache(rootPath);
+
+ return cache;
+ }
+
/// <summary>Get the tilesheet ID order used by the unmodified version of a map asset.</summary>
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
public TilesheetReference[] GetVanillaTilesheetIds(string assetName)
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 9ed989da..a451fd7c 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -9,7 +9,7 @@ using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Serialization;
-using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Utilities;
using StardewValley;
using xTile;
using xTile.Format;
@@ -32,6 +32,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The game content manager used for map tilesheets not provided by the mod.</summary>
private readonly IContentManager GameContentManager;
+ /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
+ private readonly CaseInsensitivePathCache RelativePathCache;
+
/// <summary>If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder.</summary>
private readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" };
@@ -52,10 +55,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
/// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
- public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, bool aggressiveMemoryOptimizations)
+ /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="rootDirectory"/>.</param>
+ public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, bool aggressiveMemoryOptimizations, CaseInsensitivePathCache relativePathCache)
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations)
{
this.GameContentManager = gameContentManager;
+ this.RelativePathCache = relativePathCache;
this.JsonHelper = jsonHelper;
this.ModName = modName;
@@ -198,7 +203,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
public IAssetName GetInternalAssetKey(string key)
{
FileInfo file = this.GetModFile(key);
- string relativePath = PathUtilities.GetRelativePath(this.RootDirectory, file.FullName);
+ string relativePath = Path.GetRelativePath(this.RootDirectory, file.FullName);
string internalKey = Path.Combine(this.Name, relativePath);
return this.Coordinator.ParseAssetName(internalKey, allowLocales: false);
@@ -212,6 +217,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="path">The asset path relative to the content folder.</param>
private FileInfo GetModFile(string path)
{
+ // map to case-insensitive path if needed
+ path = this.RelativePathCache.GetFilePath(path);
+
// try exact match
FileInfo file = new(Path.Combine(this.FullRootDirectory, path));
diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index 3920354e..e02ef88b 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -1,9 +1,8 @@
using System;
-using System.Collections.Generic;
using System.IO;
using StardewModdingAPI.Framework.ModHelpers;
using StardewModdingAPI.Toolkit.Serialization;
-using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Utilities;
namespace StardewModdingAPI.Framework
{
@@ -16,8 +15,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
- /// <summary>A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/macOS.</summary>
- private readonly IDictionary<string, string> RelativePaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ /// <summary>A case-insensitive lookup of relative paths within the <see cref="DirectoryPath"/>.</summary>
+ private readonly CaseInsensitivePathCache RelativePathCache;
/*********
@@ -48,19 +47,15 @@ namespace StardewModdingAPI.Framework
/// <param name="content">Provides an API for loading content assets from the content pack's folder.</param>
/// <param name="translation">Provides translations stored in the content pack's <c>i18n</c> folder.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
- public ContentPack(string directoryPath, IManifest manifest, IModContentHelper content, TranslationHelper translation, JsonHelper jsonHelper)
+ /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="directoryPath"/>.</param>
+ public ContentPack(string directoryPath, IManifest manifest, IModContentHelper content, TranslationHelper translation, JsonHelper jsonHelper, CaseInsensitivePathCache relativePathCache)
{
this.DirectoryPath = directoryPath;
this.Manifest = manifest;
this.ModContent = content;
this.TranslationImpl = translation;
this.JsonHelper = jsonHelper;
-
- foreach (string path in Directory.EnumerateFiles(this.DirectoryPath, "*", SearchOption.AllDirectories))
- {
- string relativePath = path.Substring(this.DirectoryPath.Length + 1);
- this.RelativePaths[relativePath] = relativePath;
- }
+ this.RelativePathCache = relativePathCache;
}
/// <inheritdoc />
@@ -90,8 +85,7 @@ namespace StardewModdingAPI.Framework
FileInfo file = this.GetFile(path, out path);
this.JsonHelper.WriteJsonFile(file.FullName, data);
- if (!this.RelativePaths.ContainsKey(path))
- this.RelativePaths[path] = path;
+ this.RelativePathCache.Add(path);
}
/// <inheritdoc />
@@ -112,18 +106,6 @@ namespace StardewModdingAPI.Framework
/*********
** Private methods
*********/
- /// <summary>Get the real relative path from a case-insensitive path.</summary>
- /// <param name="relativePath">The normalized relative path.</param>
- private string GetCaseInsensitiveRelativePath(string relativePath)
- {
- if (!PathUtilities.IsSafeRelativePath(relativePath))
- throw new InvalidOperationException($"You must call {nameof(IContentPack)} methods with a relative path.");
-
- return !string.IsNullOrWhiteSpace(relativePath) && this.RelativePaths.TryGetValue(relativePath, out string caseInsensitivePath)
- ? caseInsensitivePath
- : relativePath;
- }
-
/// <summary>Get the underlying file info.</summary>
/// <param name="relativePath">The normalized file path relative to the content pack directory.</param>
private FileInfo GetFile(string relativePath)
@@ -136,7 +118,11 @@ namespace StardewModdingAPI.Framework
/// <param name="actualRelativePath">The relative path after case-insensitive matching.</param>
private FileInfo GetFile(string relativePath, out string actualRelativePath)
{
- actualRelativePath = this.GetCaseInsensitiveRelativePath(relativePath);
+ if (!PathUtilities.IsSafeRelativePath(relativePath))
+ throw new InvalidOperationException($"You must call {nameof(IContentPack)} methods with a relative path.");
+
+ actualRelativePath = this.RelativePathCache.GetFilePath(relativePath);
+
return new FileInfo(Path.Combine(this.DirectoryPath, actualRelativePath));
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
index 2379583c..7468cda1 100644
--- a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
@@ -1,7 +1,9 @@
using System;
+using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Utilities;
namespace StardewModdingAPI.Framework.ModHelpers
{
@@ -20,6 +22,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName;
+ /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
+ private readonly CaseInsensitivePathCache RelativePathCache;
+
/*********
** Public methods
@@ -30,7 +35,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="modName">The friendly mod name for use in errors.</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
- public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IContentManager gameContentManager)
+ /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="relativePathCache"/>.</param>
+ public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IContentManager gameContentManager, CaseInsensitivePathCache relativePathCache)
: base(modID)
{
string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
@@ -38,11 +44,14 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ContentCore = contentCore;
this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, gameContentManager);
this.ModName = modName;
+ this.RelativePathCache = relativePathCache;
}
/// <inheritdoc />
public T Load<T>(string relativePath)
{
+ relativePath = this.RelativePathCache.GetAssetName(relativePath);
+
IAssetName assetName = this.ContentCore.ParseAssetName(relativePath, allowLocales: false);
try
@@ -58,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <inheritdoc />
public IAssetName GetInternalAssetName(string relativePath)
{
+ relativePath = this.RelativePathCache.GetAssetName(relativePath);
return this.ModContentManager.GetInternalAssetKey(relativePath);
}
@@ -67,7 +77,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
if (data == null)
throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
- relativePath ??= $"temp/{Guid.NewGuid():N}";
+ relativePath = relativePath != null
+ ? this.RelativePathCache.GetAssetName(relativePath)
+ : $"temp/{Guid.NewGuid():N}";
return new AssetDataForObject(this.ContentCore.GetLocale(), this.ContentCore.ParseAssetName(relativePath, allowLocales: false), data, key => this.ContentCore.ParseAssetName(key, allowLocales: false).Name);
}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index b4aa3595..7fd5bcd3 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1775,10 +1775,11 @@ namespace StardewModdingAPI.Framework
{
IManifest manifest = mod.Manifest;
IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
+ CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(mod.DirectoryPath);
GameContentHelper gameContentHelper = new(this.ContentCore, manifest.UniqueID, mod.DisplayName, monitor);
- IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager());
+ IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache);
TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
- IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, modContentHelper, translationHelper, jsonHelper);
+ IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, modContentHelper, translationHelper, jsonHelper, relativePathCache);
mod.SetMod(contentPack, monitor, translationHelper);
this.ModRegistry.Add(mod);
@@ -1856,11 +1857,14 @@ namespace StardewModdingAPI.Framework
IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
{
IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name);
+
+ CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(packDirPath);
+
GameContentHelper gameContentHelper = new(contentCore, packManifest.UniqueID, packManifest.Name, packMonitor);
- IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, gameContentHelper.GetUnderlyingContentManager());
+ IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), relativePathCache);
TranslationHelper packTranslationHelper = new(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
- ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper);
+ ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper, relativePathCache);
this.ReloadTranslationsForTemporaryContentPack(mod, contentPack);
mod.FakeContentPacks.Add(new WeakReference<ContentPack>(contentPack));
return contentPack;
@@ -1868,9 +1872,10 @@ namespace StardewModdingAPI.Framework
IModEvents events = new ModEvents(mod, this.EventManager);
ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager);
+ CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(mod.DirectoryPath);
IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor);
GameContentHelper gameContentHelper = new(contentCore, manifest.UniqueID, mod.DisplayName, monitor);
- IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager());
+ IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache);
IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack);
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
diff --git a/src/SMAPI/Utilities/CaseInsensitivePathCache.cs b/src/SMAPI/Utilities/CaseInsensitivePathCache.cs
new file mode 100644
index 00000000..1d947b53
--- /dev/null
+++ b/src/SMAPI/Utilities/CaseInsensitivePathCache.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace StardewModdingAPI.Utilities
+{
+ /// <summary>Provides an API for case-insensitive relative path lookups within a root directory.</summary>
+ internal class CaseInsensitivePathCache
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The root directory path for relative paths.</summary>
+ private readonly string RootPath;
+
+ /// <summary>A case-insensitive lookup of file paths within the <see cref="RootPath"/>. Each path is listed in both file path and asset name format, so it's usable in both contexts without needing to re-parse paths.</summary>
+ private readonly Lazy<Dictionary<string, string>> RelativePathCache;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="rootPath">The root directory path for relative paths.</param>
+ public CaseInsensitivePathCache(string rootPath)
+ {
+ this.RootPath = rootPath;
+ this.RelativePathCache = new(this.GetRelativePathCache);
+ }
+
+ /// <summary>Get the exact capitalization for a given relative file path.</summary>
+ /// <param name="relativePath">The relative path.</param>
+ /// <remarks>Returns the resolved path in file path format, else the normalized <paramref name="relativePath"/>.</remarks>
+ public string GetFilePath(string relativePath)
+ {
+ return this.GetImpl(PathUtilities.NormalizePath(relativePath));
+ }
+
+ /// <summary>Get the exact capitalization for a given asset name.</summary>
+ /// <param name="relativePath">The relative path.</param>
+ /// <remarks>Returns the resolved path in asset name format, else the normalized <paramref name="relativePath"/>.</remarks>
+ public string GetAssetName(string relativePath)
+ {
+ return this.GetImpl(PathUtilities.NormalizeAssetName(relativePath));
+ }
+
+ /// <summary>Add a relative path that was just created by a SMAPI API.</summary>
+ /// <param name="relativePath">The relative path. This must already be normalized in asset name or file path format.</param>
+ public void Add(string relativePath)
+ {
+ // skip if cache isn't created yet (no need to add files manually in that case)
+ if (!this.RelativePathCache.IsValueCreated)
+ return;
+
+ // skip if already cached
+ if (this.RelativePathCache.Value.ContainsKey(relativePath))
+ return;
+
+ // make sure path exists
+ relativePath = PathUtilities.NormalizePath(relativePath);
+ if (!File.Exists(Path.Combine(this.RootPath, relativePath)))
+ throw new InvalidOperationException($"Can't add relative path '{relativePath}' to the case-insensitive cache for '{this.RootPath}' because that file doesn't exist.");
+
+ // cache path
+ this.CacheRawPath(this.RelativePathCache.Value, relativePath);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the exact capitalization for a given relative path.</summary>
+ /// <param name="relativePath">The relative path. This must already be normalized into asset name or file path format (i.e. using <see cref="PathUtilities.NormalizeAssetName"/> or <see cref="PathUtilities.NormalizePath"/> respectively).</param>
+ /// <remarks>Returns the resolved path in the same format if found, else returns the path as-is.</remarks>
+ private string GetImpl(string relativePath)
+ {
+ // invalid path
+ if (string.IsNullOrWhiteSpace(relativePath))
+ return relativePath;
+
+ // already cached
+ if (this.RelativePathCache.Value.TryGetValue(relativePath, out string resolved))
+ return resolved;
+
+ // file exists but isn't cached for some reason
+ // cache it now so any later references to it are case-insensitive
+ if (File.Exists(Path.Combine(this.RootPath, relativePath)))
+ {
+ this.CacheRawPath(this.RelativePathCache.Value, relativePath);
+ return relativePath;
+ }
+
+ // no such file, keep capitalization as-is
+ return relativePath;
+ }
+
+ /// <summary>Get a case-insensitive lookup of file paths (see <see cref="RelativePathCache"/>).</summary>
+ private Dictionary<string, string> GetRelativePathCache()
+ {
+ Dictionary<string, string> cache = new(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string path in Directory.EnumerateFiles(this.RootPath, "*", SearchOption.AllDirectories))
+ {
+ string relativePath = path.Substring(this.RootPath.Length + 1);
+
+ this.CacheRawPath(cache, relativePath);
+ }
+
+ return cache;
+ }
+
+ /// <summary>Add a raw relative path to the cache.</summary>
+ /// <param name="cache">The cache to update.</param>
+ /// <param name="relativePath">The relative path to cache, with its exact filesystem capitalization.</param>
+ private void CacheRawPath(IDictionary<string, string> cache, string relativePath)
+ {
+ string filePath = PathUtilities.NormalizePath(relativePath);
+ string assetName = PathUtilities.NormalizeAssetName(relativePath);
+
+ cache[filePath] = filePath;
+ cache[assetName] = assetName;
+ }
+ }
+}