[
  {
    "path": ".editorconfig",
    "content": "# topmost editorconfig\nroot: true\n\n##########\n## General formatting\n## documentation: http://editorconfig.org\n##########\n[*]\nindent_style = space\nindent_size = 4\ninsert_final_newline = true\ntrim_trailing_whitespace = true\ncharset = utf-8\n\n[*.{csproj,nuspec,targets}]\nindent_size = 2\n\n[*.csproj]\ncharset = utf-8-bom\ninsert_final_newline = false\n\n##########\n## C# formatting\n## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference\n##########\n[*.cs]\n\n#sort 'system' usings first\ndotnet_sort_system_directives_first = true\n\n# use 'this.' qualifier\ndotnet_style_qualification_for_field = true:error\ndotnet_style_qualification_for_property = true:error\ndotnet_style_qualification_for_method = true:error\ndotnet_style_qualification_for_event = true:error\n\n# use language keywords (like int) instead of type (like Int32)\ndotnet_style_predefined_type_for_locals_parameters_members = true:error\ndotnet_style_predefined_type_for_member_access = true:error\n\n# don't use 'var' for language keywords\ncsharp_style_var_for_built_in_types = false:error\n\n# suggest modern C# features where simpler\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\ncsharp_style_conditional_delegate_call = true:suggestion\ncsharp_prefer_simple_default_expression = true:suggestion\n\n# prefer method block bodies\ncsharp_style_expression_bodied_methods = false:suggestion\ncsharp_style_expression_bodied_constructors = false:suggestion\n\n# prefer property expression bodies\ncsharp_style_expression_bodied_properties = true:suggestion\ncsharp_style_expression_bodied_indexers = true:suggestion\ncsharp_style_expression_bodied_accessors = true:suggestion\n\n# prefer inline out variables\ncsharp_style_inlined_variable_declaration = true:warning\n\n# avoid superfluous braces\ncsharp_prefer_braces = false:suggestion\n"
  },
  {
    "path": ".gitattributes",
    "content": "# always normalise line endings\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "content": "# user-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# build results\n[Dd]ebug/\n[Rr]elease/\n[Bb]in/\n[Oo]bj/\n\n# Visual Studio cache/options\n.vs/\n\n# ReSharper\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# Rider\n.idea/\n\n# NuGet packages\n*.nupkg\n**/packages/*\n*.nuget.props\n*.nuget.targets\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2019 Pathoschild\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "**StardewXnbHack** is a one-way XNB unpacker for Stardew Valley. It supports every Stardew Valley\nasset type, is very easy to update for game changes, and is quick at unpacking many files at once.\n\n![](StardewXnbHack/assets/icon.png)\n\n## Usage\nTo install it:\n\n1. Install [Stardew Valley](https://www.stardewvalley.net/) and [SMAPI](https://smapi.io/).\n2. From the [releases page](https://github.com/Pathoschild/StardewXnbHack/releases), download the\n   `StardewXnbHack *.zip` file for your operating system under 'assets'.\n3. Unzip it into [your Stardew Valley folder](https://stardewvalleywiki.com/Modding:Game_folder),\n   so `StardewXnbHack.exe` (Windows) or `StardewXnbHack` (Linux/macOS) is in the same folder as\n   `Stardew Valley.dll`.\n\nTo unpack the entire `Content` folder into `Content (unpacked)`, just double-click on\n`StardewXnbHack.exe` (Windows) or `StardewXnbHack` (Linux/macOS).\n\n## FAQs\n### How does this compare to other XNB unpackers?\nStardewXnbHack reads files through a temporary game instance, unlike other unpackers which read\nthem directly. That lets it support custom Stardew Valley formats, but it can't repack files (which\nis [rarely needed anyway](https://stardewvalleywiki.com/Modding:Content_Patcher)) or support other\ngames.\n\nThe main differences at a glance:\n\n&nbsp;                | StardewXnbHack | [xnbcli](https://github.com/LeonBlade/xnbcli/) | [XNBExtract](https://community.playstarbound.com/threads/110976)\n--------------------- | ---------------- | ------ | -----------\nSupported asset types | ✓ images<br />✓ maps<br />✓ dictionary data<br />✓ font texture<br />✓ font XML data<br />✓ structured data | ✓ images<br />✓ maps<br />✓ dictionary data<br />✓ font textures<br />✓ font XML data<br />❑ structured data | ✓ images<br />✓ maps<br />✓ dictionary data<br />✓ font textures<br />❑ font XML data<br />❑ structured data\nExport formats | ✓ `.png` for images<br />✓ `.tmx` for maps<br />✓ `.json` for data ([CP](https://stardewvalleywiki.com/Modding:Content_Patcher)-compatible) | ✓ `.png` for images<br />✓ `.tbin` for maps¹<br />❑ `.json` for data (custom format) | ✓ `.png` for images<br />✓ `.tbin` for maps¹<br />❑ `.yaml` for data\nSupported platforms | ✓ Windows<br />✓ Linux<br />✓ Mac | ✓ Windows<br />✓ Linux<br />✓ Mac | ✓ Windows<br />❑ Linux<br />❑ Mac\nSupported operations | ✓ unpack<br />❑ pack | ✓ unpack<br />✓ pack  (uncompressed) | ✓ unpack<br />✓ pack\nMaintainable | ✓ easy to update | ❑ complex | ❑ complex, closed-source\nSample unpack time<br />(full `Content` folder) | ≈0m 43s | ≈6m 5s | ≈2m 20s\nLicense | MIT | GPL | n/a\n\n<sup>¹ `.tmx` is the [preferred map format](https://stardewvalleywiki.com/Modding:Maps#Map_formats), but you can open the `.tbin` file in Tiled and export it as `.tmx`.</sup>\n\n### When I run StardewXnbHack, nothing happens or it quickly exits?\nThat means it crashed for some reason.\n\nFirst, make sure you have the latest versions of SMAPI and Stardew Valley.\n\nIf it still happens, here's how to see what the error is:\n\n<div style=\"margin-left:2em;\">\n\n1. Find [your game folder](https://stardewvalleywiki.com/Modding:Game_folder).\n2. [Open a terminal in the game folder](https://www.groovypost.com/howto/open-command-window-terminal-window-specific-folder-windows-mac-linux/).\n3. Type this command:\n   * **Windows:** `StardewXnbHack.exe` (for Command Prompt) or `./StardewXnbHack.exe` (for PowerShell or Windows Terminal)\n   * **Linux or macOS:** `./StardewXnbHack`\n4. Press enter to run the command.\n\nThat should run StardewXnbHack in the same terminal, and the window will stay open if it crashes.\n</div>\n\nYou can ask for help in [#making-mods on the Stardew Valley Discord](https://stardewvalleywiki.com/Modding:Community#Discord).\nIf you're sure it's a StardewXnbHack bug (and not a usage error), you can report it on the [issues\npage](https://github.com/Pathoschild/StardewXnbHack/issues).\n\n### Can I simplify the data files?\nBy default, unpacked data files include _all_ of the fields. This can be very noisy, and doesn't\nreally match how the data assets are formatted in the original code.\n\nYou can omit the default fields instead:\n\n1. Open a terminal in [your game folder](https://stardewvalleywiki.com/Modding:Game_folder).\n2. Run `StardewXnbHack.exe --clean` to omit the default fields.\n\nThis is still experimental, but it may become the default behavior in future versions.\n\n\n## For StardewXnbHack developers\nThis section explains how to edit or compile StardewXnbHack from the source code. Most users should\n[use the release version](#usage) instead.\n\n### Compile from source\n1. Install [Stardew Valley](https://www.stardewvalley.net/) and [SMAPI](https://smapi.io/).\n2. Open the `.sln` solution file in [Visual Studio](https://visualstudio.microsoft.com/vs/).\n3. Click _Build > Build Solution_. (If it doesn't find the Stardew Valley folder automatically, see\n   [_custom game path_ in the mod build package readme](https://smapi.io/package/custom-game-path).)\n\n### Debug a local build\nJust launch the project via _Debug > Start Debugging_. It will run from your `bin` folder, but\nshould auto-detect your game folder and unpack its `Content` folder.\n\n### Prepare a compiled release\nTo prepare a crossplatform SMAPI release:\n\n1. Update the [semantic version](https://semver.org) in `StardewXnbHack.csproj`.\n2. Run the `build-scripts/prepare-release-packages.sh` on Linux or macOS.  \n   _See the [equivalent documentation for SMAPI](https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/smapi.md#preparing-a-release)\n   for the first-time setup (including using WSL on Windows)._\n3. Release the zip files created in the root `bin` folder.\n\n## See also\n* [Release notes](release-notes.md)\n"
  },
  {
    "path": "StardewXnbHack/Framework/ConsoleProgressBar.cs",
    "content": "using System;\n\nnamespace StardewXnbHack.Framework;\n\n/// <summary>Manages a progress bar written to the console.</summary>\ninternal class ConsoleProgressBar\n{\n    /*********\n    ** Fields\n    *********/\n    /// <summary>The total number of steps to perform.</summary>\n    private readonly int TotalSteps;\n\n    /// <summary>The current step being performed.</summary>\n    private int CurrentStep;\n\n    /// <summary>The last line to which the progress bar was output, if any.</summary>\n    private int OutputLine = -1;\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Construct an instance.</summary>\n    /// <param name=\"totalSteps\">The total number of steps to perform.</param>\n    public ConsoleProgressBar(int totalSteps)\n    {\n        this.TotalSteps = totalSteps;\n    }\n\n    /// <summary>Increment the current step.</summary>\n    public void Increment()\n    {\n        this.CurrentStep++;\n    }\n\n    /// <summary>Print a progress bar to the console.</summary>\n    /// <param name=\"message\">The message to print.</param>\n    /// <param name=\"removePrevious\">Whether to remove the previously output progress bar.</param>\n    public void Print(string message, bool removePrevious = true)\n    {\n        if (removePrevious)\n            this.Erase();\n\n        int percentage = (int)((this.CurrentStep / (this.TotalSteps * 1m)) * 100);\n\n        Console.ForegroundColor = ConsoleColor.Green;\n        Console.WriteLine($\"[{\"\".PadRight(percentage / 10, '#')}{\"\".PadRight(10 - percentage / 10, ' ')} {percentage}%]  {message}\");\n        Console.ResetColor();\n\n        this.OutputLine = Console.CursorTop - 1;\n    }\n\n    /// <summary>Remove the last progress bar written to the console.</summary>\n    /// <remarks>Derived from <a href=\"https://stackoverflow.com/a/8946847/262123\" />.</remarks>\n    public void Erase()\n    {\n        if (this.OutputLine == -1)\n            return;\n\n        bool isLastLine = this.OutputLine == Console.CursorTop - 1;\n        int currentLine = isLastLine\n            ? this.OutputLine\n            : Console.CursorTop;\n\n        Console.SetCursorPosition(0, this.OutputLine);\n        Console.Write(new string(' ', Console.BufferWidth));\n        Console.SetCursorPosition(0, currentLine);\n\n        this.OutputLine = -1;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/DefaultConsoleLogger.cs",
    "content": "using System;\nusing System.Linq;\nusing StardewXnbHack.ProgressHandling;\n\nnamespace StardewXnbHack.Framework;\n\n/// <summary>Report updates to the console while the unpacker is running.</summary>\ninternal class DefaultConsoleLogger : IProgressLogger\n{\n    /*********\n    ** Fields\n    *********/\n    /// <summary>The context info for the current unpack run.</summary>\n    private readonly IUnpackContext Context;\n\n    /// <summary>Whether to show a 'press any key to exit' prompt on end.</summary>\n    private readonly bool ShowPressAnyKeyToExit;\n\n    /// <summary>The current progress bar written to the console.</summary>\n    private ConsoleProgressBar ProgressBar;\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Construct an instance.</summary>\n    /// <param name=\"context\">The context info for the current unpack run.</param>\n    /// <param name=\"showPressAnyKeyToExit\">Whether to show a 'press any key to exit' prompt on end.</param>\n    public DefaultConsoleLogger(IUnpackContext context, bool showPressAnyKeyToExit)\n    {\n        this.Context = context;\n        this.ShowPressAnyKeyToExit = showPressAnyKeyToExit;\n    }\n\n    /// <inheritdoc />\n    public void OnFatalError(string error)\n    {\n        this.PrintColor(error, ConsoleColor.Red);\n    }\n\n    /// <inheritdoc />\n    public void OnStepChanged(ProgressStep step, string message)\n    {\n        this.ProgressBar?.Erase();\n\n        if (step == ProgressStep.Done)\n            Console.WriteLine();\n\n        Console.WriteLine(message);\n    }\n\n    /// <inheritdoc />\n    public void OnFileUnpacking(string relativePath)\n    {\n        if (this.ProgressBar == null)\n            this.ProgressBar = new ConsoleProgressBar(this.Context.Files.Count());\n\n        this.ProgressBar.Increment();\n        this.ProgressBar.Print(relativePath);\n    }\n\n    /// <inheritdoc />\n    public void OnFileUnpackFailed(string relativePath, UnpackFailedReason errorCode, string errorMessage)\n    {\n        ConsoleColor color = errorCode == UnpackFailedReason.UnsupportedFileType\n            ? ConsoleColor.DarkYellow\n            : ConsoleColor.Red;\n\n        this.ProgressBar.Erase();\n        this.PrintColor($\"{relativePath} => {errorMessage}\", color);\n    }\n\n    /// <inheritdoc />\n    public void OnEnded()\n    {\n        if (this.ShowPressAnyKeyToExit)\n            DefaultConsoleLogger.PressAnyKeyToExit();\n    }\n\n    /// <summary>Show a 'press any key to exit' message and wait for a key press.</summary>\n    public static void PressAnyKeyToExit()\n    {\n        Console.WriteLine();\n        Console.WriteLine(\"Press any key to exit.\");\n        Console.ReadKey();\n    }\n\n\n    /*********\n    ** Private methods\n    *********/\n    /// <summary>Print a message to the console with a foreground color.</summary>\n    /// <param name=\"message\">The message to print.</param>\n    /// <param name=\"color\">The foreground color to use.</param>\n    private void PrintColor(string message, ConsoleColor color)\n    {\n        Console.ForegroundColor = color;\n        Console.WriteLine(message);\n        Console.ResetColor();\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/PlatformContext.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing StardewModdingAPI.Toolkit;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework;\n\n/// <summary>Provides platform-specific information.</summary>\ninternal class PlatformContext\n{\n    /*********\n    ** Accessors\n    *********/\n    /// <summary>The current platform.</summary>\n    public Platform Platform { get; } = EnvironmentUtility.DetectPlatform();\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Get whether any of the listed platforms is the current one.</summary>\n    /// <param name=\"platforms\">The platforms to match.</param>\n    public bool Is(params Platform[] platforms)\n    {\n        return platforms.Contains(this.Platform);\n    }\n\n    /// <summary>Get the absolute paths to the game and content folders, if found.</summary>\n    /// <param name=\"specifiedPath\">The game path specified by the user, if any.</param>\n    /// <param name=\"gamePath\">The absolute path to the game folder, if found.</param>\n    /// <param name=\"contentPath\">The absolute path to the content folder, if found.</param>\n    /// <returns>Returns whether both the game and content folders were found.</returns>\n    public bool TryDetectGamePaths(string specifiedPath, out string gamePath, out string contentPath)\n    {\n        gamePath = null;\n        contentPath = null;\n\n        // check possible game paths\n        foreach (string candidate in this.GetCandidateGamePaths(specifiedPath))\n        {\n            // detect paths\n            string curGamePath = this.TryGamePath(candidate);\n            string curContentPath = this.FindContentPath(curGamePath);\n\n            // valid game install found\n            if (curGamePath != null && curContentPath != null)\n            {\n                gamePath = curGamePath;\n                contentPath = curContentPath;\n                return true;\n            }\n\n            // if game folder exists without a content folder, track the first found game path (i.e. the highest-priority one)\n            gamePath ??= curGamePath;\n        }\n\n        return false;\n    }\n\n\n    /*********\n    ** Private methods\n    *********/\n    /// <summary>Get the possible game paths.</summary>\n    /// <param name=\"specifiedPath\">The game path specified by the user, if any.</param>\n    private IEnumerable<string> GetCandidateGamePaths(string specifiedPath = null)\n    {\n        // specified path\n        if (!string.IsNullOrWhiteSpace(specifiedPath))\n            yield return specifiedPath;\n\n        // current working directory\n        yield return AppDomain.CurrentDomain.BaseDirectory;\n\n        // detected game path\n        string detectedPath = new ModToolkit().GetGameFolders().FirstOrDefault()?.FullName;\n        if (detectedPath != null)\n            yield return detectedPath;\n    }\n\n    /// <summary>Get the absolute path to the game folder, if it's valid.</summary>\n    /// <param name=\"path\">The path to check for a game install.</param>\n    private string TryGamePath(string path)\n    {\n        // game path exists\n        if (path == null)\n            return null;\n        DirectoryInfo gameDir = new DirectoryInfo(path);\n        if (!gameDir.Exists)\n            return null;\n\n        // has game files\n        bool hasGameDll = File.Exists(Path.Combine(gameDir.FullName, \"Stardew Valley.dll\"));\n        if (!hasGameDll)\n            return null;\n\n        // isn't the build folder when compiled directly\n        bool isCompileFolder = File.Exists(Path.Combine(gameDir.FullName, \"StardewXnbHack.exe.config\"));\n        if (isCompileFolder)\n            return null;\n\n        return gameDir.FullName;\n    }\n\n    /// <summary>Get the absolute path to the content folder for a given game, if found.</summary>\n    /// <param name=\"gamePath\">The absolute path to the game folder.</param>\n    private string FindContentPath(string gamePath)\n    {\n        if (gamePath == null)\n            return null;\n\n        foreach (string relativePath in this.GetPossibleRelativeContentPaths())\n        {\n            DirectoryInfo folder = new DirectoryInfo(Path.Combine(gamePath, relativePath));\n            if (folder.Exists)\n                return folder.FullName;\n        }\n\n        return null;\n    }\n\n    /// <summary>Get the possible relative paths for the current platform.</summary>\n    private IEnumerable<string> GetPossibleRelativeContentPaths()\n    {\n        // under game folder on most platforms\n        if (this.Platform != Platform.Mac)\n            yield return \"Content\";\n\n        // macOS\n        else\n        {\n            // Steam paths\n            // - game path: StardewValley/Contents/MacOS\n            // - content:   StardewValley/Contents/Resources/Content\n            yield return \"../Resources/Content\";\n\n            // GOG paths\n            // - game path: Stardew Valley.app/Contents/MacOS\n            // - content:   Stardew Valley.app/Resources/Content\n            yield return \"../../Resources/Content\";\n        }\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/UnpackContext.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing StardewXnbHack.ProgressHandling;\n\nnamespace StardewXnbHack.Framework;\n\n/// <summary>The context info for the current unpack run.</summary>\ninternal class UnpackContext : IUnpackContext\n{\n    /*********\n    ** Accessors\n    *********/\n    /// <inheritdoc />\n    public string GamePath { get; set; }\n\n    /// <inheritdoc />\n    public string ContentPath { get; set; }\n\n    /// <inheritdoc />\n    public string ExportPath { get; set; }\n\n    /// <inheritdoc />\n    public IEnumerable<FileInfo> Files { get; set; }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/BaseAssetWriter.cs",
    "content": "using System;\nusing Force.DeepCloner;\nusing Microsoft.Xna.Framework.Content;\nusing Newtonsoft.Json;\nusing StardewModdingAPI.Toolkit.Serialization;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>The base class for an asset writer.</summary>\ninternal abstract class BaseAssetWriter : IAssetWriter\n{\n    /*********\n    ** Private methods\n    *********/\n    /// <summary>The settings to use when serializing JSON.</summary>\n    private readonly Lazy<JsonSerializerSettings> JsonSettings;\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    public abstract bool CanWrite(object asset);\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    public abstract bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error);\n\n\n    /*********\n    ** Protected methods\n    *********/\n    /// <summary>Construct an instance.</summary>\n    /// <param name=\"omitDefaultFields\">Whether to ignore members marked <see cref=\"ContentSerializerAttribute.Optional\"/> which match the default value.</param>\n    protected BaseAssetWriter(bool omitDefaultFields = false)\n    {\n        this.JsonSettings = new(() => BaseAssetWriter.GetJsonSerializerSettings(omitDefaultFields));\n    }\n\n    /// <summary>Get a text representation for the given asset.</summary>\n    /// <param name=\"asset\">The asset to serialize.</param>\n    protected string FormatData(object asset)\n    {\n        return JsonConvert.SerializeObject(asset, this.JsonSettings.Value);\n    }\n\n    /// <summary>Get the recommended file extension for a data file formatted with <see cref=\"FormatData\"/>.</summary>\n    protected string GetDataExtension()\n    {\n        return \"json\";\n    }\n\n    /// <summary>Get the serializer settings to apply when writing JSON.</summary>\n    /// <param name=\"omitDefaultFields\">Whether to ignore members marked <see cref=\"ContentSerializerAttribute.Optional\"/> which match the default value.</param>\n    private static JsonSerializerSettings GetJsonSerializerSettings(bool omitDefaultFields = false)\n    {\n        JsonHelper jsonHelper = new();\n        JsonSerializerSettings settings = jsonHelper.JsonSettings.DeepClone();\n\n        settings.ContractResolver = new IgnoreDefaultOptionalPropertiesResolver(omitDefaultFields);\n\n        return settings;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/DataWriter.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes <see cref=\"Dictionary{TKey,TValue}\"/> and <see cref=\"List{T}\"/> assets to disk.</summary>\ninternal class DataWriter : BaseAssetWriter\n{\n    /*********\n    ** Public methods\n    *********/\n    /// <inheritdoc />\n    public DataWriter(bool omitDefaultFields)\n        : base(omitDefaultFields) { }\n\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    public override bool CanWrite(object asset)\n    {\n        Type type = asset.GetType();\n        type = type.IsGenericType ? type.GetGenericTypeDefinition() : type;\n\n        return\n            type == typeof(Dictionary<,>)\n            || type == typeof(List<>)\n            || type.FullName?.StartsWith(\"StardewValley.GameData.\") == true;\n    }\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)\n    {\n        File.WriteAllText($\"{toPathWithoutExtension}.{this.GetDataExtension()}\", this.FormatData(asset));\n\n        error = null;\n        return true;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/IAssetWriter.cs",
    "content": "using StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes assets to disk.</summary>\ninternal interface IAssetWriter\n{\n    /*********\n    ** Methods\n    *********/\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    bool CanWrite(object asset);\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error);\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/IgnoreDefaultOptionalPropertiesResolver.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing Microsoft.Xna.Framework.Content;\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Serialization;\nusing Sickhead.Engine.Util;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>A Json.NET contract resolver which ignores properties marked with <see cref=\"ContentSerializerIgnoreAttribute\"/>, or (optionally) marked <see cref=\"ContentSerializerAttribute.Optional\"/> with the default value.</summary>\ninternal class IgnoreDefaultOptionalPropertiesResolver : DefaultContractResolver\n{\n    /*********\n    ** Fields\n    *********/\n    /// <summary>Whether to ignore members marked <see cref=\"ContentSerializerAttribute.Optional\"/> which match the default value.</summary>\n    private readonly bool OmitDefaultValues;\n\n    /// <summary>The default values for fields and properties marked <see cref=\"ContentSerializerAttribute.Optional\"/>.</summary>\n    private readonly Dictionary<string, Dictionary<string, object>> DefaultValues = new();\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Construct an instance.</summary>\n    /// <param name=\"omitDefaultValues\">Whether to ignore members marked <see cref=\"ContentSerializerAttribute.Optional\"/> which match the default value.</param>\n    public IgnoreDefaultOptionalPropertiesResolver(bool omitDefaultValues)\n    {\n        this.OmitDefaultValues = omitDefaultValues;\n    }\n\n\n    /*********\n    ** Protected methods\n    *********/\n    /// <inheritdoc />\n    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)\n    {\n        JsonProperty property = base.CreateProperty(member, memberSerialization);\n\n        // property marked ignore\n        if (member.GetCustomAttribute<ContentSerializerIgnoreAttribute>() != null)\n            property.ShouldSerialize = _ => false;\n\n        // property marked optional which matches default value\n        else if (this.OmitDefaultValues)\n        {\n            Dictionary<string, object>? optionalMembers = this.GetDefaultValues(member.DeclaringType);\n            if (optionalMembers != null && optionalMembers.TryGetValue(member.Name, out object defaultValue))\n            {\n                property.ShouldSerialize = instance =>\n                {\n                    object value = member.GetValue(instance);\n                    return !defaultValue?.Equals(value) ?? value is not null;\n                };\n            }\n        }\n\n        return property;\n    }\n\n    /// <summary>The default values for a type's fields and properties marked <see cref=\"ContentSerializerAttribute.Optional\"/>, if any.</summary>\n    /// <param name=\"type\">The type whose fields and properties to get default values for.</param>\n    /// <returns>Returns a dictionary of default values by member name if any were found, else <c>null</c>.</returns>\n    private Dictionary<string, object>? GetDefaultValues(Type type)\n    {\n        // skip invalid\n        if (!type.IsClass || type.FullName is null || type.Namespace?.StartsWith(\"StardewValley\") != true)\n            return null;\n\n        // skip if already cached\n        if (this.DefaultValues.TryGetValue(type.FullName, out Dictionary<string, object> defaults))\n            return defaults;\n\n        // get members\n        MemberInfo[] optionalMembers =\n            (type.GetFields().OfType<MemberInfo>())\n            .Concat(type.GetProperties())\n            .Where(member => member.GetCustomAttribute<ContentSerializerAttribute>()?.Optional is true)\n            .ToArray();\n        if (optionalMembers.Length == 0)\n            return this.DefaultValues[type.FullName] = null;\n\n        // get default instance\n        object defaultInstance;\n        try\n        {\n            defaultInstance = Activator.CreateInstance(type);\n        }\n        catch\n        {\n            return this.DefaultValues[type.FullName] = null;\n        }\n\n        // get default values\n        defaults = new Dictionary<string, object>();\n        foreach (MemberInfo member in optionalMembers)\n            defaults[member.Name] = member.GetValue(defaultInstance);\n        return this.DefaultValues[type.FullName] = defaults;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/MapWriter.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Xml.Linq;\nusing StardewModdingAPI.Toolkit.Utilities;\nusing StardewValley;\nusing TMXTile;\nusing xTile;\nusing xTile.Dimensions;\nusing xTile.Layers;\nusing xTile.Tiles;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes <see cref=\"Map\"/> assets to disk.</summary>\ninternal class MapWriter : BaseAssetWriter\n{\n    /*********\n    ** Fields\n    *********/\n    /// <summary>The actual size of a tile in the tilesheet.</summary>\n    const int TileSize = Game1.tileSize / Game1.pixelZoom;\n\n    /// <summary>The underlying map format handler.</summary>\n    private readonly TMXFormat Format;\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Construct an instance.</summary>\n    public MapWriter()\n    {\n        // init TMX support\n        this.Format = new TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom);\n    }\n\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    public override bool CanWrite(object asset)\n    {\n        return asset is Map;\n    }\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)\n    {\n        Map map = (Map)asset;\n\n        // fix tile sizes (game overrides them in-memory)\n        IDictionary<Layer, Size> tileSizes = new Dictionary<Layer, Size>();\n        foreach (var layer in map.Layers)\n        {\n            tileSizes[layer] = layer.TileSize;\n            layer.TileSize = new Size(MapWriter.TileSize, MapWriter.TileSize);\n        }\n\n        // fix image sources (game overrides them in-memory)\n        IDictionary<TileSheet, string> imageSources = new Dictionary<TileSheet, string>();\n        foreach (var sheet in map.TileSheets)\n        {\n            imageSources[sheet] = sheet.ImageSource;\n            sheet.ImageSource = this.GetOriginalImageSource(relativePath, sheet.ImageSource);\n        }\n\n        // save file\n        using (Stream stream = new MemoryStream())\n        {\n            // serialize to stream\n            this.Format.Store(map, stream, DataEncodingType.CSV);\n\n            // workaround: TMXTile doesn't indent the XML in newer .NET versions\n            stream.Position = 0;\n            var doc = XDocument.Load(stream);\n            File.WriteAllText($\"{toPathWithoutExtension}.tmx\", \"<?xml version=\\\"1.0\\\"?>\\n\" + doc.ToString());\n        }\n\n        // undo changes\n        foreach (var layer in map.Layers)\n            layer.TileSize = tileSizes[layer];\n        foreach (var sheet in map.TileSheets)\n            sheet.ImageSource = imageSources[sheet];\n\n        error = null;\n        return true;\n    }\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Get the image source for a map tilesheet without the game's automatic path changes.</summary>\n    /// <param name=\"relativeMapPath\">The relative path to the map file within the content folder.</param>\n    /// <param name=\"imageSource\">The tilesheet image source.</param>\n    private string GetOriginalImageSource(string relativeMapPath, string imageSource)\n    {\n        string mapDirPath = PathUtilities.NormalizePath(Path.GetDirectoryName(relativeMapPath));\n        string normalizedImageSource = PathUtilities.NormalizePath(imageSource);\n\n        return normalizedImageSource.StartsWith($\"{mapDirPath}{PathUtilities.PreferredPathSeparator}\", StringComparison.OrdinalIgnoreCase)\n            ? imageSource.Substring(mapDirPath.Length + 1)\n            : imageSource;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/SpriteFontWriter.cs",
    "content": "using System.IO;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes <see cref=\"SpriteFont\"/> assets to disk.</summary>\ninternal class SpriteFontWriter : BaseAssetWriter\n{\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    public override bool CanWrite(object asset)\n    {\n        return asset is SpriteFont;\n    }\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)\n    {\n        SpriteFont font = (SpriteFont)asset;\n\n        // get texture\n        Texture2D texture = font.Texture;\n\n        // save texture\n        using (Stream stream = File.Create($\"{toPathWithoutExtension}.png\"))\n        {\n            if (texture.Format == SurfaceFormat.Dxt3) // MonoGame can't read DXT3 textures directly, need to export through GPU\n            {\n                using RenderTarget2D renderTarget = this.RenderWithGpu(texture);\n                renderTarget.SaveAsPng(stream, texture.Width, texture.Height);\n            }\n            else\n                texture.SaveAsPng(stream, texture.Width, texture.Height);\n        }\n\n        // save font data\n        var data = new\n        {\n            font.LineSpacing,\n            font.Spacing,\n            font.DefaultCharacter,\n            font.Characters,\n            Glyphs = font.GetGlyphs()\n        };\n        File.WriteAllText($\"{toPathWithoutExtension}.{this.GetDataExtension()}\", this.FormatData(data));\n\n        error = null;\n        return true;\n    }\n\n\n    /*********\n    ** Private methods\n    *********/\n    /// <summary>Draw a texture to a GPU render target.</summary>\n    /// <param name=\"texture\">The texture to draw.</param>\n    private RenderTarget2D RenderWithGpu(Texture2D texture)\n    {\n        // set render target\n        var gpu = texture.GraphicsDevice;\n        RenderTarget2D target = new RenderTarget2D(gpu, texture.Width, texture.Height);\n        gpu.SetRenderTarget(target);\n\n        // render\n        try\n        {\n            gpu.Clear(Color.Transparent); // set transparent background\n\n            using SpriteBatch batch = new SpriteBatch(gpu);\n            batch.Begin();\n            batch.Draw(texture, Vector2.Zero, Color.White);\n            batch.End();\n        }\n        finally\n        {\n            gpu.SetRenderTarget(null);\n        }\n\n        return target;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/TextureWriter.cs",
    "content": "using System.IO;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes <see cref=\"Texture2D\"/> assets to disk.</summary>\ninternal class TextureWriter : BaseAssetWriter\n{\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    public override bool CanWrite(object asset)\n    {\n        return asset is Texture2D;\n    }\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)\n    {\n        Texture2D texture = (Texture2D)asset;\n\n        this.UnpremultiplyTransparency(texture);\n        using (Stream stream = File.Create($\"{toPathWithoutExtension}.png\"))\n            texture.SaveAsPng(stream, texture.Width, texture.Height);\n\n        error = null;\n        return true;\n    }\n\n\n    /*********\n    ** Private methods\n    *********/\n    /// <summary>Reverse premultiplication applied to an image asset by the XNA content pipeline.</summary>\n    /// <param name=\"texture\">The texture to adjust.</param>\n    private void UnpremultiplyTransparency(Texture2D texture)\n    {\n        Color[] data = new Color[texture.Width * texture.Height];\n        texture.GetData(data);\n\n        for (int i = 0; i < data.Length; i++)\n        {\n            Color pixel = data[i];\n            if (pixel.A == 0)\n                continue;\n\n            data[i] = new Color(\n                (byte)((pixel.R * 255) / pixel.A),\n                (byte)((pixel.G * 255) / pixel.A),\n                (byte)((pixel.B * 255) / pixel.A),\n                pixel.A\n            ); // don't use named parameters, which are inconsistent between MonoGame (e.g. 'alpha') and XNA (e.g. 'a')\n        }\n\n        texture.SetData(data);\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/XmlSourceWriter.cs",
    "content": "using System.IO;\nusing BmFont;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes <see cref=\"XmlSource\"/> assets to disk.</summary>\ninternal class XmlSourceWriter : BaseAssetWriter\n{\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>Whether the writer can handle a given asset.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    public override bool CanWrite(object asset)\n    {\n        return asset is XmlSource;\n    }\n\n    /// <summary>Write an asset instance to disk.</summary>\n    /// <param name=\"asset\">The asset value.</param>\n    /// <param name=\"toPathWithoutExtension\">The absolute path to the export file, without the file extension.</param>\n    /// <param name=\"relativePath\">The relative path within the content folder.</param>\n    /// <param name=\"platform\">The operating system running the unpacker.</param>\n    /// <param name=\"error\">An error phrase indicating why writing to disk failed (if applicable).</param>\n    /// <returns>Returns whether writing to disk completed successfully.</returns>\n    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)\n    {\n        XmlSource value = (XmlSource)asset;\n        File.WriteAllText($\"{toPathWithoutExtension}.fnt\", value.Source);\n\n        error = null;\n        return true;\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/Program.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Text;\nusing Microsoft.Xna.Framework.Graphics;\nusing StardewModdingAPI.Toolkit.Utilities;\nusing StardewValley;\nusing StardewXnbHack.Framework;\nusing StardewXnbHack.Framework.Writers;\nusing StardewXnbHack.ProgressHandling;\n\nnamespace StardewXnbHack;\n\n/// <summary>The console app entry point.</summary>\npublic static class Program\n{\n    /*********\n    ** Fields\n    *********/\n    /// <summary>The relative paths to search for unresolved assembly files.</summary>\n    private static readonly string[] RelativeAssemblyProbePaths =\n    {\n        \"\", // app directory\n        \"smapi-internal\"\n    };\n\n\n    /*********\n    ** Public methods\n    *********/\n    /// <summary>The console app entry method.</summary>\n    /// <param name=\"args\">The command-line arguments.</param>\n    internal static void Main(string[] args)\n    {\n        // set window title\n        Console.Title = $\"StardewXnbHack {Program.GetUnpackerVersion()}\";\n\n        // check platform\n        Program.AssertPlatform();\n\n        // Add fallback assembly resolution that loads DLLs from a 'smapi-internal' subfolder,\n        // so it can be run from the game folder. This must be set before any references to\n        // game or toolkit types (including IAssetWriter which references the toolkit's\n        // Platform enum).\n        AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve;\n\n        // launch app\n        try\n        {\n            Program.Run(args);\n        }\n        catch (Exception ex)\n        {\n            // not in game folder\n            if (ex is FileNotFoundException fileNotFoundEx)\n            {\n                AssemblyName assemblyName = new AssemblyName(fileNotFoundEx.FileName);\n                if (assemblyName.Name == \"Stardew Valley\")\n                {\n                    Console.WriteLine(\"Oops! StardewXnbHack must be placed in the Stardew Valley game folder.\\nSee instructions: https://github.com/Pathoschild/StardewXnbHack#readme.\");\n                    DefaultConsoleLogger.PressAnyKeyToExit();\n                    return;\n                }\n            }\n\n            // generic unhandled exception\n            Console.WriteLine(\"Oops! Something went wrong running the unpacker:\");\n            Console.WriteLine(ex.ToString());\n            DefaultConsoleLogger.PressAnyKeyToExit();\n        }\n    }\n\n    /// <summary>Unpack all assets in the content folder and store them in the output folder.</summary>\n    /// <param name=\"args\">The command-line arguments.</param>\n    /// <param name=\"game\">The game instance through which to unpack files, or <c>null</c> to launch a temporary internal instance.</param>\n    /// <param name=\"gamePath\">The absolute path to the game folder, or <c>null</c> to auto-detect it.</param>\n    /// <param name=\"getLogger\">Get a custom progress update logger, or <c>null</c> to use the default console logging. Receives the unpack context and default logger as arguments.</param>\n    /// <param name=\"showPressAnyKeyToExit\">Whether the default logger should show a 'press any key to exit' prompt when it finishes.</param>\n    public static void Run(string[] args, GameRunner game = null, string gamePath = null, Func<IUnpackContext, IProgressLogger, IProgressLogger> getLogger = null, bool showPressAnyKeyToExit = true)\n    {\n        // init logging\n        UnpackContext context = new UnpackContext();\n        IProgressLogger logger = new DefaultConsoleLogger(context, showPressAnyKeyToExit);\n\n        try\n        {\n            // get override logger\n            if (getLogger != null)\n                logger = getLogger(context, logger);\n\n            // read command-line arguments\n            bool omitDefaultFields = args.Contains(\"--clean\");\n\n            // start log\n            logger.OnStepChanged(ProgressStep.Started, $\"Running StardewXnbHack {Program.GetUnpackerVersion()}.{(omitDefaultFields ? \" Special options: omit default fields.\" : \"\")}\");\n\n            // start timer\n            Stopwatch timer = new Stopwatch();\n            timer.Start();\n\n            // get asset writers\n            var assetWriters = new IAssetWriter[]\n            {\n                new MapWriter(),\n                new SpriteFontWriter(),\n                new TextureWriter(),\n                new XmlSourceWriter(),\n                new DataWriter(omitDefaultFields) // check last due to more expensive CanWrite\n            };\n\n            // get paths\n            var platform = new PlatformContext();\n            {\n                if (platform.TryDetectGamePaths(gamePath, out gamePath, out string contentPath))\n                {\n                    context.GamePath = gamePath;\n                    context.ContentPath = contentPath;\n                }\n                else\n                {\n                    logger.OnFatalError(gamePath == null\n                        ? \"Can't find the Stardew Valley folder. Try running StardewXnbHack from the game folder instead.\"\n                        : $\"Can't find the content folder for the game at {gamePath}. Is the game installed correctly?\"\n                    );\n                    return;\n                }\n            }\n            context.ExportPath = Path.Combine(context.GamePath, \"Content (unpacked)\");\n            logger.OnStepChanged(ProgressStep.GameFound, $\"Found game folder: {context.GamePath}.\");\n\n            // symlink files on Linux/Mac\n            if (platform.Is(Platform.Linux, Platform.Mac))\n            {\n                foreach (string dirName in new[] { \"lib\", \"lib64\" })\n                {\n                    string fullPath = Path.Combine(context.GamePath, dirName);\n                    if (!Directory.Exists(dirName))\n                        Process.Start(\"ln\", $\"-sf \\\"{fullPath}\\\"\");\n                }\n            }\n\n            // load game instance\n            bool disposeGame = false;\n            if (game == null)\n            {\n                logger.OnStepChanged(ProgressStep.LoadingGameInstance, \"Loading game instance...\");\n                game = Program.CreateTemporaryGameInstance(platform, context.ContentPath);\n                disposeGame = true;\n            }\n\n            // unpack files\n            try\n            {\n                logger.OnStepChanged(ProgressStep.UnpackingFiles, $\"Unpacking files for Stardew Valley {Program.GetGameVersion()}...\");\n\n                // collect files\n                DirectoryInfo contentDir = new DirectoryInfo(context.ContentPath);\n                FileInfo[] files = contentDir.EnumerateFiles(\"*.xnb\", SearchOption.AllDirectories).ToArray();\n                context.Files = files;\n\n                // write assets\n                foreach (FileInfo file in files)\n                {\n                    // prepare paths\n                    string assetName = file.FullName.Substring(context.ContentPath.Length + 1, file.FullName.Length - context.ContentPath.Length - 5); // remove root path + .xnb extension\n                    string relativePath = $\"{assetName}.xnb\";\n                    string fileExportPath = Path.Combine(context.ExportPath, assetName);\n                    Directory.CreateDirectory(Path.GetDirectoryName(fileExportPath));\n\n                    // fallback logic\n                    void ExportRawXnb()\n                    {\n                        File.Copy(file.FullName, $\"{fileExportPath}.xnb\", overwrite: true);\n                    }\n\n                    // show progress bar\n                    logger.OnFileUnpacking(assetName);\n\n                    // read asset\n                    object asset;\n                    try\n                    {\n                        asset = game.Content.Load<object>(assetName);\n                    }\n                    catch (Exception ex)\n                    {\n                        if (ex.Message == \"This does not appear to be a MonoGame MGFX file!\")\n                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.UnsupportedFileType, $\"{nameof(Effect)} isn't a supported asset type.\");\n                        else\n                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.ReadError, $\"read error: {ex.Message}\");\n                        ExportRawXnb();\n                        continue;\n                    }\n\n                    // write asset\n                    try\n                    {\n                        // get writer\n                        IAssetWriter writer = assetWriters.FirstOrDefault(p => p.CanWrite(asset));\n\n                        // write file\n                        if (writer == null)\n                        {\n                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.UnsupportedFileType, $\"{asset.GetType().Name} isn't a supported asset type.\");\n                            ExportRawXnb();\n                        }\n                        else if (!writer.TryWriteFile(asset, fileExportPath, assetName, platform.Platform, out string writeError))\n                        {\n                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.WriteError, $\"{asset.GetType().Name} file could not be saved: {writeError}.\");\n                            ExportRawXnb();\n                        }\n                    }\n                    catch (Exception ex)\n                    {\n                        logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.UnknownError, $\"unhandled export error: {ex.Message}\");\n                    }\n                    finally\n                    {\n                        game.Content.Unload();\n                    }\n                }\n            }\n            finally\n            {\n                if (disposeGame)\n                    game.Dispose();\n            }\n\n            logger.OnStepChanged(ProgressStep.Done, $\"Done! Unpacked {context.Files.Count()} files in {Program.GetHumanTime(timer.Elapsed)}.\\nUnpacked into {context.ExportPath}.\");\n        }\n        catch (Exception ex)\n        {\n            logger.OnFatalError($\"Unhandled exception: {ex}\");\n        }\n        finally\n        {\n            logger.OnEnded();\n        }\n    }\n\n\n    /*********\n    ** Private methods\n    *********/\n    /// <summary>Assert that the current platform matches the one StardewXnbHack was compiled for.</summary>\n    private static void AssertPlatform()\n    {\n        bool isWindows = Environment.OSVersion.Platform != PlatformID.MacOSX && Environment.OSVersion.Platform != PlatformID.Unix;\n\n#if IS_FOR_WINDOWS\n        if (!isWindows)\n        {\n            Console.WriteLine(\"Oops! This is the Windows version of StardewXnbHack. Make sure to install the Windows version instead.\");\n            DefaultConsoleLogger.PressAnyKeyToExit();\n        }\n#else\n            if (isWindows)\n            {\n                Console.WriteLine(\"Oops! This is the Linux/macOS version of StardewXnbHack. Make sure to install the version for your OS type instead.\");\n                DefaultConsoleLogger.PressAnyKeyToExit();\n            }\n#endif\n    }\n\n    /// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary>\n    /// <param name=\"sender\">The event sender.</param>\n    /// <param name=\"e\">The event arguments.</param>\n    private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)\n    {\n        // get assembly name\n        AssemblyName name = new AssemblyName(e.Name);\n        if (name.Name == null)\n            return null;\n\n        // check search folders\n        foreach (string relativePath in Program.RelativeAssemblyProbePaths)\n        {\n            // get absolute path of search folder\n            string searchPath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), relativePath);\n            if (!Directory.Exists(searchPath))\n                continue;\n\n            // try to resolve DLL\n            try\n            {\n                foreach (FileInfo dll in new DirectoryInfo(searchPath).EnumerateFiles(\"*.dll\"))\n                {\n                    // get assembly name\n                    string dllAssemblyName;\n                    try\n                    {\n                        dllAssemblyName = AssemblyName.GetAssemblyName(dll.FullName).Name;\n                    }\n                    catch\n                    {\n                        continue;\n                    }\n\n                    // check for match\n                    if (name.Name.Equals(dllAssemblyName, StringComparison.OrdinalIgnoreCase))\n                        return Assembly.LoadFrom(dll.FullName);\n                }\n            }\n            catch (Exception ex)\n            {\n                Console.WriteLine($\"Error resolving assembly: {ex}\");\n                return null;\n            }\n        }\n\n        return null;\n    }\n\n    /// <summary>Create a temporary game instance for the unpacker.</summary>\n    /// <param name=\"platform\">The platform-specific context.</param>\n    /// <param name=\"contentPath\">The absolute path to the content folder to import.</param>\n    private static GameRunner CreateTemporaryGameInstance(PlatformContext platform, string contentPath)\n    {\n        var foregroundColor = Console.ForegroundColor;\n        Console.ForegroundColor = ConsoleColor.DarkGray;\n\n        try\n        {\n            GameRunner game = new GameRunner();\n            GameRunner.instance = game;\n\n            Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef;\n\n            game.RunOneFrame();\n\n            return game;\n        }\n        finally\n        {\n            Console.ForegroundColor = foregroundColor;\n            Console.WriteLine();\n        }\n    }\n\n    /// <summary>Get a human-readable representation of elapsed time.</summary>\n    /// <param name=\"time\">The elapsed time.</param>\n    private static string GetHumanTime(TimeSpan time)\n    {\n        List<string> parts = new List<string>(2);\n\n        if (time.TotalMinutes >= 1)\n            parts.Add($\"{time.TotalMinutes:0} minute{(time.TotalMinutes >= 2 ? \"s\" : \"\")}\");\n        if (time.Seconds > 0)\n            parts.Add($\"{time.Seconds:0} second{(time.Seconds > 1 ? \"s\" : \"\")}\");\n\n        return string.Join(\" \", parts);\n    }\n\n    /// <summary>Get the version string for StardewXnbHack.</summary>\n    private static string GetUnpackerVersion()\n    {\n        var attribute = typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();\n        return attribute?.InformationalVersion ?? \"<unknown version>\";\n    }\n\n    /// <summary>Get the version string for Stardew Valley.</summary>\n    private static string GetGameVersion()\n    {\n        StringBuilder version = new();\n\n        // base version\n        version.Append(Game1.version);\n        version.Append(\" (\");\n\n        // build number\n        {\n            string buildVersion = typeof(Game1).Assembly.GetName().Version?.ToString() ?? \"unknown\";\n            if (buildVersion.StartsWith($\"{Game1.version}.\"))\n                buildVersion = buildVersion.Substring(Game1.version.Length + 1);\n            version.Append(buildVersion);\n        }\n\n        // version label\n        if (!string.IsNullOrWhiteSpace(Game1.versionLabel))\n        {\n            version.Append(\" \\\"\");\n            version.Append(Game1.versionLabel);\n            version.Append('\"');\n        }\n\n        version.Append(')');\n\n        return version.ToString();\n    }\n}\n"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/IProgressLogger.cs",
    "content": "namespace StardewXnbHack.ProgressHandling;\n\n/// <summary>Logs updates while the unpacker is running.</summary>\npublic interface IProgressLogger\n{\n    /*********\n    ** Methods\n    *********/\n    /// <summary>Log an error which halts the unpack process (e.g. game folder missing).</summary>\n    /// <param name=\"error\">The error message indicating why the unpacker halted.</param>\n    void OnFatalError(string error);\n\n    /// <summary>Log a step transition in the overall unpack process.</summary>\n    /// <param name=\"step\">The new step.</param>\n    /// <param name=\"message\">The default log message for the step transition.</param>\n    void OnStepChanged(ProgressStep step, string message);\n\n    /// <summary>Log a file being unpacked.</summary>\n    /// <param name=\"relativePath\">The relative path of the file within the content folder.</param>\n    void OnFileUnpacking(string relativePath);\n\n    /// <summary>Log a file which couldn't be unpacked.</summary>\n    /// <param name=\"relativePath\">The relative path of the file within the content folder.</param>\n    /// <param name=\"errorCode\">An error code indicating why unpacking failed.</param>\n    /// <param name=\"errorMessage\">An error message indicating why unpacking failed.</param>\n    void OnFileUnpackFailed(string relativePath, UnpackFailedReason errorCode, string errorMessage);\n\n    /// <summary>The unpacker is done and exiting.</summary>\n    void OnEnded();\n}\n"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/IUnpackContext.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\n\nnamespace StardewXnbHack.ProgressHandling;\n\n/// <summary>The context info for the current unpack run.</summary>\npublic interface IUnpackContext\n{\n    /// <summary>The absolute path to the game folder.</summary>\n    string GamePath { get; }\n\n    /// <summary>The absolute path to the content folder.</summary>\n    string ContentPath { get; }\n\n    /// <summary>The absolute path to the folder containing exported assets.</summary>\n    string ExportPath { get; }\n\n    /// <summary>The files found to unpack.</summary>\n    IEnumerable<FileInfo> Files { get; }\n}\n"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/ProgressStep.cs",
    "content": "namespace StardewXnbHack.ProgressHandling;\n\n/// <summary>A step in the overall unpack process.</summary>\npublic enum ProgressStep\n{\n    /// <summary>The unpacker has started.</summary>\n    Started,\n\n    /// <summary>The game folder was located, but unpacking hasn't started yet.</summary>\n    GameFound,\n\n    /// <summary>The temporary game instance is being loaded.</summary>\n    LoadingGameInstance,\n\n    /// <summary>The files are being unpacked.</summary>\n    UnpackingFiles,\n\n    /// <summary>The overall unpack process completed successfully.</summary>\n    Done\n}\n"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/UnpackFailedReason.cs",
    "content": "namespace StardewXnbHack.ProgressHandling;\n\n/// <summary>An error code indicating why unpacking failed for a file.</summary>\npublic enum UnpackFailedReason\n{\n    /// <summary>An error occurred trying to read the raw XNB asset.</summary>\n    ReadError,\n\n    /// <summary>The XNB asset was successfully loaded, but its file format can't be unpacked.</summary>\n    UnsupportedFileType,\n\n    /// <summary>The XNB asset was successfully loaded, but an error occurred trying to save the unpacked file.</summary>\n    WriteError,\n\n    /// <summary>An unhandled error occurred.</summary>\n    UnknownError\n}\n"
  },
  {
    "path": "StardewXnbHack/Properties/PublishProfiles/FolderProfile.pubxml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n    <PropertyGroup>\n        <Configuration>Release</Configuration>\n        <Platform>x64</Platform>\n        <PublishDir>bin\\x64\\Release\\net6.0\\publish\\win-x64\\</PublishDir>\n        <PublishProtocol>FileSystem</PublishProtocol>\n        <TargetFramework>net6.0</TargetFramework>\n        <RuntimeIdentifier>win-x64</RuntimeIdentifier>\n        <SelfContained>true</SelfContained>\n        <PublishSingleFile>True</PublishSingleFile>\n        <PublishReadyToRun>False</PublishReadyToRun>\n        <PublishTrimmed>False</PublishTrimmed>\n    </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "StardewXnbHack/StardewXnbHack.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <!--metadata-->\n    <Authors>Pathoschild</Authors>\n    <PackageLicenseExpression>MIT</PackageLicenseExpression>\n    <RepositoryUrl>https://github.com/Pathoschild/StardewXnbHack</RepositoryUrl>\n    <RepositoryType>git</RepositoryType>\n    <Version>1.1.2</Version>\n\n    <!--build-->\n    <TargetFramework>net6.0</TargetFramework>\n    <Platforms>x64</Platforms>\n    <OutputType>Exe</OutputType>\n    <ApplicationIcon>assets/icon.ico</ApplicationIcon>\n    <DefineConstants Condition=\"$(OS) == 'Windows_NT'\">$(DefineConstants);IS_FOR_WINDOWS</DefineConstants>\n\n    <!--don't append Git commit SHA to version, since it's shown in the app-->\n    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>\n\n    <!--mod build package-->\n    <CopyModReferencesToBuildOutput>True</CopyModReferencesToBuildOutput>\n    <EnableGameDebugging>False</EnableGameDebugging>\n    <EnableModDeploy>False</EnableModDeploy>\n    <EnableModZip>False</EnableModZip>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.3\" />\n    <PackageReference Include=\"Pathoschild.Stardew.ModBuildConfig\" Version=\"4.3.1\" />\n    <PackageReference Include=\"Platonymous.TMXTile\" Version=\"1.5.9\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"BmFont\" HintPath=\"$(GamePath)\\BmFont.dll\" />\n    <Reference Include=\"SMAPI.Toolkit\" HintPath=\"$(GamePath)\\smapi-internal\\SMAPI.Toolkit.dll\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "StardewXnbHack.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.0.31912.275\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"StardewXnbHack\", \"StardewXnbHack\\StardewXnbHack.csproj\", \"{F1512B6A-75A9-4425-B4EE-26D50E7075C8}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"root\", \"root\", \"{5EDB5467-BFEE-4122-8764-FA41B4E59D7F}\"\n\tProjectSection(SolutionItems) = preProject\n\t\t.editorconfig = .editorconfig\n\t\t.gitattributes = .gitattributes\n\t\t.gitignore = .gitignore\n\t\tLICENSE = LICENSE\n\t\tREADME.md = README.md\n\t\trelease-notes.md = release-notes.md\n\tEndProjectSection\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"build-scripts\", \"build-scripts\", \"{26C61354-013D-4E87-ADD8-951C04FFB185}\"\n\tProjectSection(SolutionItems) = preProject\n\t\tbuild-scripts\\prepare-release-packages.sh = build-scripts\\prepare-release-packages.sh\n\tEndProjectSection\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|x64 = Debug|x64\n\t\tRelease|x64 = Release|x64\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Debug|x64.ActiveCfg = Debug|x64\n\t\t{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Debug|x64.Build.0 = Debug|x64\n\t\t{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Release|x64.ActiveCfg = Release|x64\n\t\t{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Release|x64.Build.0 = Release|x64\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(NestedProjects) = preSolution\n\t\t{26C61354-013D-4E87-ADD8-951C04FFB185} = {5EDB5467-BFEE-4122-8764-FA41B4E59D7F}\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {ED3F1CBF-3651-49A3-9652-9A8BCD952184}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "StardewXnbHack.sln.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeThisQualifier/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Stardew/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>"
  },
  {
    "path": "build-scripts/prepare-release-packages.sh",
    "content": "#!/bin/bash\n\n\n##########\n## Constants\n##########\ngamePath=\"/home/pathoschild/Stardew Valley\"\nbuildConfig=\"Release\"\nplatforms=(\"Linux\" \"macOS\" \"Windows\")\ndeclare -A runtimes=([\"Linux\"]=\"linux-x64\" [\"macOS\"]=\"osx-x64\" [\"Windows\"]=\"win-x64\")\ndeclare -A msBuildPlatformNames=([\"Linux\"]=\"Unix\" [\"macOS\"]=\"OSX\" [\"Windows\"]=\"Windows_NT\")\n\n\n##########\n## Move to solution root\n##########\ncd \"`dirname \"$0\"`/..\"\n\n\n##########\n## Clear old build files\n##########\necho \"Clearing old builds...\"\necho \"-----------------------\"\nfor path in **/bin **/obj; do\n    echo \"$path\"\n    rm -rf \"$path\"\ndone\nrm -rf \"bin\"\necho \"\"\n\n\n##########\n## Compile files\n##########\nversion=\"$1\"\nif [ $# -eq 0 ]; then\n    echo \"StardewXnbHack release version (like '2.0.0'):\"\n    read version\nfi\n\n\n##########\n## Compile files\n##########\nfor platform in ${platforms[@]}; do\n    # constants\n    runtime=${runtimes[$platform]}\n    msbuildPlatformName=${msBuildPlatformNames[$platform]}\n    binPath=\"StardewXnbHack/bin/Release/net6.0/$runtime/publish\"\n    folderName=\"StardewXnbHack $version for $platform\"\n    bundlePath=\"bin/$folderName\"\n\n    # compile\n    echo \"Compiling for $platform...\"\n    echo \"--------------------------\"\n    dotnet publish StardewXnbHack --configuration $buildConfig -v minimal --runtime \"$runtime\" -p:OS=\"$msbuildPlatformName\" -p:GamePath=\"$gamePath\" -p:PublishSingleFile=True --self-contained true\n    echo \"\"\n    echo \"\"\n\n    # build package folder\n    echo \"Preparing package for $platform...\"\n    echo \"----------------------------------\"\n    mkdir \"$bundlePath\" --parents\n    cp \"$binPath/StardewXnbHack\"* \"$bundlePath\"\n\n    if [ -t \"$bundlePath/StardewXnbHack\" ]; then\n        chmod 755 \"$bundlePath/StardewXnbHack\"\n    fi\n\n    # zip package\n    pushd bin > /dev/null\n    zip -9 \"$folderName.zip\" \"$folderName\" --recurse-paths --quiet\n    popd > /dev/null\n    echo \"Package created at $(pwd)/bin/$folderName.zip\"\n    echo \"\"\n    echo \"\"\ndone\nexit\n\necho \"Done!\"\n"
  },
  {
    "path": "release-notes.md",
    "content": "[← back to readme](README.md)\n\n# Release notes\n## 1.1.2\nReleased 05 November 2024.\n\n* Updated for SMAPI 4.1.4.\n\n## 1.1.1\nReleased 20 August 2024.\n\n* Updated for Stardew Valley 1.6.9 and SMAPI 4.1.0.\n\n## 1.1.0\nReleased 08 April 2024.\n\n* You can now omit data fields which match their default value by calling `StardewXnbHack.exe --clean`.\n* Updated for SMAPI 4.0.6.\n* Fixed StardewXnbHack version shown in the console including a Git commit SHA in recent versions.\n\n## 1.0.8\nReleased 19 March 2024.\n\n* Updated for Stardew Valley 1.6.\n* Added StardewXnbHack & game versions to console output.\n* Data model properties marked with `[ContentSerializerIgnore]` are now omitted from output `.json` files.\n\n## 1.0.7\nReleased 28 December 2021.\n\n* Fixed exported `.tmx` files no longer indented.\n\n## 1.0.6\nReleased 04 December 2021.\n\n* StardewXnbHack no longer needs .NET to be installed.\n* Updated to .NET 6.\n* Fixed launch errors on macOS and Windows.\n\n## 1.0.5\nReleased 15 September 2021.\n\n* Updated for Stardew Valley 1.5.5 and .NET 5.\n* Fixed some textures not unpacked correctly on Linux/macOS.\n* Fixed _cannot be loaded into a Reach GraphicsDevice_ error for some users.\n\n## 1.0.4\nReleased 23 March 2021.\n\n* Added a descriptive error if you install the wrong version (e.g. the Windows version on macOS).\n* Updated for SMAPI 3.9.5 (thanks to nyrdosh!).\n\n## 1.0.3\nReleased 21 December 2020.\n\n* Updated for Stardew Valley 1.5.\n\n## 1.0.2\nReleased 07 December 2020.\n\n* Assets on macOS are now unpacked into the game folder instead of resources, for consistency with other platforms.\n* Improved error if the game's content folder is missing.\n* Fixed duplicate tile index properties in some cases.\n* Fixed unpack error on macOS with Steam.\n\n## 1.0.1\nReleased 21 November 2020.\n\n* Fixed `.tmx` map files losing tile index properties.\n\n## 1.0\nReleased 04 October 2020.\n\n* Added compiled release.\n* Added icon/mascot (thanks to ParadigmNomad!).\n* Added support for running it from the game folder or another app.\n* Added file count and unpack time to log.\n* Improved compatibility on Linux/macOS.\n* Changed map format from `.tbin` to `.tmx` (thanks to Platonymous!).\n* Fixed unsupported XNB files not always copied into the export folder.\n* Fixed BMFont file extension set to `.xml` instead of `.fnt` (thanks to Platonymous!).\n\n## Prerelease\nIncludes changes between 16 June 2019 and 25 April 2020, which didn't have packaged releases.\n\n* Initial implementation with support for...\n  * unpacking data (`.json`);\n  * unpacking maps (`.tbin`);\n  * unpacking textures (`.png`);\n  * unpacking SpriteFont (`.png` and `.json`), and BMFont (`.png` and `.xml`) font files.\n  * Linux/macOS/Windows.\n* Fixed macOS build error (thanks to strobel1ght!).\n"
  }
]