using System; using System.Collections.Generic; using System.IO; using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides simplified APIs for writing mods. internal class ModHelper : BaseHelper, IModHelper, IDisposable { /********* ** Properties *********/ /// The content packs loaded for this mod. private readonly IContentPack[] ContentPacks; /// Create a transitional content pack. private readonly Func CreateContentPack; /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; /********* ** Accessors *********/ /// The full path to the mod's folder. public string DirectoryPath { get; } /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. public IModEvents Events { get; } /// An API for loading content assets. public IContentHelper Content { get; } /// An API for reading and writing persistent mod data. public IDataHelper Data { get; } /// An API for checking and changing input state. public IInputHelper Input { get; } /// An API for accessing private game code. public IReflectionHelper Reflection { get; } /// an API for fetching metadata about loaded mods. public IModRegistry ModRegistry { get; } /// An API for managing console commands. public ICommandHelper ConsoleCommands { get; } /// Provides multiplayer utilities. public IMultiplayerHelper Multiplayer { get; } /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). public ITranslationHelper Translation { get; } /********* ** Public methods *********/ /// Construct an instance. /// The mod's unique ID. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. /// Manages the game's input state. /// Manages access to events raised by SMAPI. /// An API for loading content assets. /// An API for managing console commands. /// An API for reading and writing persistent mod data. /// an API for fetching metadata about loaded mods. /// An API for accessing private game code. /// Provides multiplayer utilities. /// An API for reading translations stored in the mod's i18n folder. /// The content packs loaded for this mod. /// Create a transitional content pack. /// Manages deprecation warnings. /// An argument is null or empty. /// The path does not exist on disk. public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, IEnumerable contentPacks, Func createContentPack, DeprecationManager deprecationManager) : base(modID) { // validate directory if (string.IsNullOrWhiteSpace(modDirectory)) throw new ArgumentNullException(nameof(modDirectory)); if (!Directory.Exists(modDirectory)) throw new InvalidOperationException("The specified mod directory does not exist."); // initialise this.DirectoryPath = modDirectory; this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper)); this.Input = new InputHelper(modID, inputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); this.ContentPacks = contentPacks.ToArray(); this.CreateContentPack = createContentPack; this.DeprecationManager = deprecationManager; this.Events = events; } /**** ** Mod config file ****/ /// Read the mod's configuration file (and create it if needed). /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. public TConfig ReadConfig() where TConfig : class, new() { TConfig config = this.Data.ReadJsonFile("config.json") ?? new TConfig(); this.WriteConfig(config); // create file or fill in missing fields return config; } /// Save to the mod's configuration file. /// The config class type. /// The config settings to save. public void WriteConfig(TConfig config) where TConfig : class, new() { this.Data.WriteJsonFile("config.json", config); } /**** ** Generic JSON files ****/ /// Read a JSON file. /// The model type. /// The file path relative to the mod directory. /// Returns the deserialised model, or null if the file doesn't exist or is empty. [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] public TModel ReadJsonFile(string path) where TModel : class { path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) ? data : null; } /// Save to a JSON file. /// The model type. /// The file path relative to the mod directory. /// The model to save. [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] public void WriteJsonFile(string path, TModel model) where TModel : class { path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, model); } /**** ** Content packs ****/ /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. /// The absolute directory path containing the content pack files. /// The content pack's unique ID. /// The content pack name. /// The content pack description. /// The content pack author's name. /// The content pack version. [Obsolete("This method supports mods which previously had their own content packs, and shouldn't be used by new mods. It will be removed in SMAPI 3.0.")] public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) { // raise deprecation notice this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice); // validate if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException(nameof(directoryPath)); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); if (!Directory.Exists(directoryPath)) throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); // create manifest IManifest manifest = new Manifest( uniqueID: id, name: name, author: author, description: description, version: version, contentPackFor: this.ModID ); // create content pack return this.CreateContentPack(directoryPath, manifest); } /// Get all content packs loaded for this mod. public IEnumerable GetContentPacks() { return this.ContentPacks; } /**** ** Disposal ****/ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { // nothing to dispose yet } } }