using System; using System.Collections.Generic; using System.IO; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework { /// Manages access to a content pack's metadata and files. internal class ContentPack : IContentPack { /********* ** Fields *********/ /// Provides an API for loading content assets. private readonly IContentHelper Content; /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; /// A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/macOS. private readonly IDictionary RelativePaths = new Dictionary(StringComparer.OrdinalIgnoreCase); /********* ** Accessors *********/ /// public string DirectoryPath { get; } /// public IManifest Manifest { get; } /// public ITranslationHelper Translation { get; } /********* ** Public methods *********/ /// Construct an instance. /// The full path to the content pack's folder. /// The content pack's manifest. /// Provides an API for loading content assets. /// Provides translations stored in the content pack's i18n folder. /// Encapsulates SMAPI's JSON file parsing. 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; } } /// public bool HasFile(string path) { path = PathUtilities.NormalizePath(path); return this.GetFile(path).Exists; } /// public TModel ReadJsonFile(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; } /// public void WriteJsonFile(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; } /// public T LoadAsset(string key) { key = PathUtilities.NormalizePath(key); key = this.GetCaseInsensitiveRelativePath(key); return this.Content.Load(key, ContentSource.ModFolder); } /// public string GetActualAssetKey(string key) { key = PathUtilities.NormalizePath(key); key = this.GetCaseInsensitiveRelativePath(key); return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); } /********* ** Private methods *********/ /// Get the real relative path from a case-insensitive path. /// The normalized relative path. 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; } /// Get the underlying file info. /// The normalized file path relative to the content pack directory. private FileInfo GetFile(string relativePath) { return this.GetFile(relativePath, out _); } /// Get the underlying file info. /// The normalized file path relative to the content pack directory. /// The relative path after case-insensitive matching. private FileInfo GetFile(string relativePath, out string actualRelativePath) { actualRelativePath = this.GetCaseInsensitiveRelativePath(relativePath); return new FileInfo(Path.Combine(this.DirectoryPath, actualRelativePath)); } } }