using System; using System.Collections.Generic; using System.IO; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework { /// <summary>Manages access to a content pack's metadata and files.</summary> internal class ContentPack : IContentPack { /********* ** Fields *********/ /// <summary>Provides an API for loading content assets.</summary> private readonly IContentHelper Content; /// <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/Mac.</summary> private readonly IDictionary<string, string> RelativePaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); /********* ** Accessors *********/ /// <inheritdoc /> public string DirectoryPath { get; } /// <inheritdoc /> public IManifest Manifest { get; } /// <inheritdoc /> public ITranslationHelper Translation { get; } /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="directoryPath">The full path to the content pack's folder.</param> /// <param name="manifest">The content pack's manifest.</param> /// <param name="content">Provides an API for loading content assets.</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, IContentHelper content, ITranslationHelper translation, JsonHelper jsonHelper) { this.DirectoryPath = directoryPath; this.Manifest = manifest; this.Content = content; this.Translation = 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; } } /// <inheritdoc /> public bool HasFile(string path) { path = PathUtilities.NormalizePath(path); return this.GetFile(path).Exists; } /// <inheritdoc /> public TModel ReadJsonFile<TModel>(string path) where TModel : class { path = PathUtilities.NormalizePath(path); FileInfo file = this.GetFile(path); return file.Exists && this.JsonHelper.ReadJsonFileIfExists(file.FullName, out TModel model) ? model : null; } /// <inheritdoc /> public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class { path = PathUtilities.NormalizePath(path); FileInfo file = this.GetFile(path, out path); this.JsonHelper.WriteJsonFile(file.FullName, data); if (!this.RelativePaths.ContainsKey(path)) this.RelativePaths[path] = path; } /// <inheritdoc /> public T LoadAsset<T>(string key) { key = PathUtilities.NormalizePath(key); key = this.GetCaseInsensitiveRelativePath(key); return this.Content.Load<T>(key, ContentSource.ModFolder); } /// <inheritdoc /> public string GetActualAssetKey(string key) { key = PathUtilities.NormalizePath(key); key = this.GetCaseInsensitiveRelativePath(key); return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); } /********* ** 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 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) { return this.GetFile(relativePath, out _); } /// <summary>Get the underlying file info.</summary> /// <param name="relativePath">The normalized file path relative to the content pack directory.</param> /// <param name="actualRelativePath">The relative path after case-insensitive matching.</param> private FileInfo GetFile(string relativePath, out string actualRelativePath) { actualRelativePath = this.GetCaseInsensitiveRelativePath(relativePath); return new FileInfo(Path.Combine(this.DirectoryPath, actualRelativePath)); } } }