Full Code of Pathoschild/StardewXnbHack for AI

develop e0b5d500bb8c cached
28 files
68.2 KB
16.7k tokens
74 symbols
1 requests
Download .txt
Repository: Pathoschild/StardewXnbHack
Branch: develop
Commit: e0b5d500bb8c
Files: 28
Total size: 68.2 KB

Directory structure:
gitextract_2qw3mmz3/

├── .editorconfig
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── StardewXnbHack/
│   ├── Framework/
│   │   ├── ConsoleProgressBar.cs
│   │   ├── DefaultConsoleLogger.cs
│   │   ├── PlatformContext.cs
│   │   ├── UnpackContext.cs
│   │   └── Writers/
│   │       ├── BaseAssetWriter.cs
│   │       ├── DataWriter.cs
│   │       ├── IAssetWriter.cs
│   │       ├── IgnoreDefaultOptionalPropertiesResolver.cs
│   │       ├── MapWriter.cs
│   │       ├── SpriteFontWriter.cs
│   │       ├── TextureWriter.cs
│   │       └── XmlSourceWriter.cs
│   ├── Program.cs
│   ├── ProgressHandling/
│   │   ├── IProgressLogger.cs
│   │   ├── IUnpackContext.cs
│   │   ├── ProgressStep.cs
│   │   └── UnpackFailedReason.cs
│   ├── Properties/
│   │   └── PublishProfiles/
│   │       └── FolderProfile.pubxml
│   └── StardewXnbHack.csproj
├── StardewXnbHack.sln
├── StardewXnbHack.sln.DotSettings
├── build-scripts/
│   └── prepare-release-packages.sh
└── release-notes.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# topmost editorconfig
root: true

##########
## General formatting
## documentation: http://editorconfig.org
##########
[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

[*.{csproj,nuspec,targets}]
indent_size = 2

[*.csproj]
charset = utf-8-bom
insert_final_newline = false

##########
## C# formatting
## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
##########
[*.cs]

#sort 'system' usings first
dotnet_sort_system_directives_first = true

# use 'this.' qualifier
dotnet_style_qualification_for_field = true:error
dotnet_style_qualification_for_property = true:error
dotnet_style_qualification_for_method = true:error
dotnet_style_qualification_for_event = true:error

# use language keywords (like int) instead of type (like Int32)
dotnet_style_predefined_type_for_locals_parameters_members = true:error
dotnet_style_predefined_type_for_member_access = true:error

# don't use 'var' for language keywords
csharp_style_var_for_built_in_types = false:error

# suggest modern C# features where simpler
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion

# prefer method block bodies
csharp_style_expression_bodied_methods = false:suggestion
csharp_style_expression_bodied_constructors = false:suggestion

# prefer property expression bodies
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion

# prefer inline out variables
csharp_style_inlined_variable_declaration = true:warning

# avoid superfluous braces
csharp_prefer_braces = false:suggestion


================================================
FILE: .gitattributes
================================================
# always normalise line endings
* text=auto


================================================
FILE: .gitignore
================================================
# user-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

# build results
[Dd]ebug/
[Rr]elease/
[Bb]in/
[Oo]bj/

# Visual Studio cache/options
.vs/

# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# Rider
.idea/

# NuGet packages
*.nupkg
**/packages/*
*.nuget.props
*.nuget.targets
.DS_Store


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright 2019 Pathoschild

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
**StardewXnbHack** is a one-way XNB unpacker for Stardew Valley. It supports every Stardew Valley
asset type, is very easy to update for game changes, and is quick at unpacking many files at once.

![](StardewXnbHack/assets/icon.png)

## Usage
To install it:

1. Install [Stardew Valley](https://www.stardewvalley.net/) and [SMAPI](https://smapi.io/).
2. From the [releases page](https://github.com/Pathoschild/StardewXnbHack/releases), download the
   `StardewXnbHack *.zip` file for your operating system under 'assets'.
3. Unzip it into [your Stardew Valley folder](https://stardewvalleywiki.com/Modding:Game_folder),
   so `StardewXnbHack.exe` (Windows) or `StardewXnbHack` (Linux/macOS) is in the same folder as
   `Stardew Valley.dll`.

To unpack the entire `Content` folder into `Content (unpacked)`, just double-click on
`StardewXnbHack.exe` (Windows) or `StardewXnbHack` (Linux/macOS).

## FAQs
### How does this compare to other XNB unpackers?
StardewXnbHack reads files through a temporary game instance, unlike other unpackers which read
them directly. That lets it support custom Stardew Valley formats, but it can't repack files (which
is [rarely needed anyway](https://stardewvalleywiki.com/Modding:Content_Patcher)) or support other
games.

The main differences at a glance:

                 | StardewXnbHack | [xnbcli](https://github.com/LeonBlade/xnbcli/) | [XNBExtract](https://community.playstarbound.com/threads/110976)
--------------------- | ---------------- | ------ | -----------
Supported 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
Export 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
Supported platforms | ✓ Windows<br />✓ Linux<br />✓ Mac | ✓ Windows<br />✓ Linux<br />✓ Mac | ✓ Windows<br />❑ Linux<br />❑ Mac
Supported operations | ✓ unpack<br />❑ pack | ✓ unpack<br />✓ pack  (uncompressed) | ✓ unpack<br />✓ pack
Maintainable | ✓ easy to update | ❑ complex | ❑ complex, closed-source
Sample unpack time<br />(full `Content` folder) | ≈0m 43s | ≈6m 5s | ≈2m 20s
License | MIT | GPL | n/a

<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>

### When I run StardewXnbHack, nothing happens or it quickly exits?
That means it crashed for some reason.

First, make sure you have the latest versions of SMAPI and Stardew Valley.

If it still happens, here's how to see what the error is:

<div style="margin-left:2em;">

1. Find [your game folder](https://stardewvalleywiki.com/Modding:Game_folder).
2. [Open a terminal in the game folder](https://www.groovypost.com/howto/open-command-window-terminal-window-specific-folder-windows-mac-linux/).
3. Type this command:
   * **Windows:** `StardewXnbHack.exe` (for Command Prompt) or `./StardewXnbHack.exe` (for PowerShell or Windows Terminal)
   * **Linux or macOS:** `./StardewXnbHack`
4. Press enter to run the command.

That should run StardewXnbHack in the same terminal, and the window will stay open if it crashes.
</div>

You can ask for help in [#making-mods on the Stardew Valley Discord](https://stardewvalleywiki.com/Modding:Community#Discord).
If you're sure it's a StardewXnbHack bug (and not a usage error), you can report it on the [issues
page](https://github.com/Pathoschild/StardewXnbHack/issues).

### Can I simplify the data files?
By default, unpacked data files include _all_ of the fields. This can be very noisy, and doesn't
really match how the data assets are formatted in the original code.

You can omit the default fields instead:

1. Open a terminal in [your game folder](https://stardewvalleywiki.com/Modding:Game_folder).
2. Run `StardewXnbHack.exe --clean` to omit the default fields.

This is still experimental, but it may become the default behavior in future versions.


## For StardewXnbHack developers
This section explains how to edit or compile StardewXnbHack from the source code. Most users should
[use the release version](#usage) instead.

### Compile from source
1. Install [Stardew Valley](https://www.stardewvalley.net/) and [SMAPI](https://smapi.io/).
2. Open the `.sln` solution file in [Visual Studio](https://visualstudio.microsoft.com/vs/).
3. Click _Build > Build Solution_. (If it doesn't find the Stardew Valley folder automatically, see
   [_custom game path_ in the mod build package readme](https://smapi.io/package/custom-game-path).)

### Debug a local build
Just launch the project via _Debug > Start Debugging_. It will run from your `bin` folder, but
should auto-detect your game folder and unpack its `Content` folder.

### Prepare a compiled release
To prepare a crossplatform SMAPI release:

1. Update the [semantic version](https://semver.org) in `StardewXnbHack.csproj`.
2. Run the `build-scripts/prepare-release-packages.sh` on Linux or macOS.  
   _See the [equivalent documentation for SMAPI](https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/smapi.md#preparing-a-release)
   for the first-time setup (including using WSL on Windows)._
3. Release the zip files created in the root `bin` folder.

## See also
* [Release notes](release-notes.md)


================================================
FILE: StardewXnbHack/Framework/ConsoleProgressBar.cs
================================================
using System;

namespace StardewXnbHack.Framework;

/// <summary>Manages a progress bar written to the console.</summary>
internal class ConsoleProgressBar
{
    /*********
    ** Fields
    *********/
    /// <summary>The total number of steps to perform.</summary>
    private readonly int TotalSteps;

    /// <summary>The current step being performed.</summary>
    private int CurrentStep;

    /// <summary>The last line to which the progress bar was output, if any.</summary>
    private int OutputLine = -1;


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="totalSteps">The total number of steps to perform.</param>
    public ConsoleProgressBar(int totalSteps)
    {
        this.TotalSteps = totalSteps;
    }

    /// <summary>Increment the current step.</summary>
    public void Increment()
    {
        this.CurrentStep++;
    }

    /// <summary>Print a progress bar to the console.</summary>
    /// <param name="message">The message to print.</param>
    /// <param name="removePrevious">Whether to remove the previously output progress bar.</param>
    public void Print(string message, bool removePrevious = true)
    {
        if (removePrevious)
            this.Erase();

        int percentage = (int)((this.CurrentStep / (this.TotalSteps * 1m)) * 100);

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"[{"".PadRight(percentage / 10, '#')}{"".PadRight(10 - percentage / 10, ' ')} {percentage}%]  {message}");
        Console.ResetColor();

        this.OutputLine = Console.CursorTop - 1;
    }

    /// <summary>Remove the last progress bar written to the console.</summary>
    /// <remarks>Derived from <a href="https://stackoverflow.com/a/8946847/262123" />.</remarks>
    public void Erase()
    {
        if (this.OutputLine == -1)
            return;

        bool isLastLine = this.OutputLine == Console.CursorTop - 1;
        int currentLine = isLastLine
            ? this.OutputLine
            : Console.CursorTop;

        Console.SetCursorPosition(0, this.OutputLine);
        Console.Write(new string(' ', Console.BufferWidth));
        Console.SetCursorPosition(0, currentLine);

        this.OutputLine = -1;
    }
}


================================================
FILE: StardewXnbHack/Framework/DefaultConsoleLogger.cs
================================================
using System;
using System.Linq;
using StardewXnbHack.ProgressHandling;

namespace StardewXnbHack.Framework;

/// <summary>Report updates to the console while the unpacker is running.</summary>
internal class DefaultConsoleLogger : IProgressLogger
{
    /*********
    ** Fields
    *********/
    /// <summary>The context info for the current unpack run.</summary>
    private readonly IUnpackContext Context;

    /// <summary>Whether to show a 'press any key to exit' prompt on end.</summary>
    private readonly bool ShowPressAnyKeyToExit;

    /// <summary>The current progress bar written to the console.</summary>
    private ConsoleProgressBar ProgressBar;


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="context">The context info for the current unpack run.</param>
    /// <param name="showPressAnyKeyToExit">Whether to show a 'press any key to exit' prompt on end.</param>
    public DefaultConsoleLogger(IUnpackContext context, bool showPressAnyKeyToExit)
    {
        this.Context = context;
        this.ShowPressAnyKeyToExit = showPressAnyKeyToExit;
    }

    /// <inheritdoc />
    public void OnFatalError(string error)
    {
        this.PrintColor(error, ConsoleColor.Red);
    }

    /// <inheritdoc />
    public void OnStepChanged(ProgressStep step, string message)
    {
        this.ProgressBar?.Erase();

        if (step == ProgressStep.Done)
            Console.WriteLine();

        Console.WriteLine(message);
    }

    /// <inheritdoc />
    public void OnFileUnpacking(string relativePath)
    {
        if (this.ProgressBar == null)
            this.ProgressBar = new ConsoleProgressBar(this.Context.Files.Count());

        this.ProgressBar.Increment();
        this.ProgressBar.Print(relativePath);
    }

    /// <inheritdoc />
    public void OnFileUnpackFailed(string relativePath, UnpackFailedReason errorCode, string errorMessage)
    {
        ConsoleColor color = errorCode == UnpackFailedReason.UnsupportedFileType
            ? ConsoleColor.DarkYellow
            : ConsoleColor.Red;

        this.ProgressBar.Erase();
        this.PrintColor($"{relativePath} => {errorMessage}", color);
    }

    /// <inheritdoc />
    public void OnEnded()
    {
        if (this.ShowPressAnyKeyToExit)
            DefaultConsoleLogger.PressAnyKeyToExit();
    }

    /// <summary>Show a 'press any key to exit' message and wait for a key press.</summary>
    public static void PressAnyKeyToExit()
    {
        Console.WriteLine();
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Print a message to the console with a foreground color.</summary>
    /// <param name="message">The message to print.</param>
    /// <param name="color">The foreground color to use.</param>
    private void PrintColor(string message, ConsoleColor color)
    {
        Console.ForegroundColor = color;
        Console.WriteLine(message);
        Console.ResetColor();
    }
}


================================================
FILE: StardewXnbHack/Framework/PlatformContext.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework;

/// <summary>Provides platform-specific information.</summary>
internal class PlatformContext
{
    /*********
    ** Accessors
    *********/
    /// <summary>The current platform.</summary>
    public Platform Platform { get; } = EnvironmentUtility.DetectPlatform();


    /*********
    ** Public methods
    *********/
    /// <summary>Get whether any of the listed platforms is the current one.</summary>
    /// <param name="platforms">The platforms to match.</param>
    public bool Is(params Platform[] platforms)
    {
        return platforms.Contains(this.Platform);
    }

    /// <summary>Get the absolute paths to the game and content folders, if found.</summary>
    /// <param name="specifiedPath">The game path specified by the user, if any.</param>
    /// <param name="gamePath">The absolute path to the game folder, if found.</param>
    /// <param name="contentPath">The absolute path to the content folder, if found.</param>
    /// <returns>Returns whether both the game and content folders were found.</returns>
    public bool TryDetectGamePaths(string specifiedPath, out string gamePath, out string contentPath)
    {
        gamePath = null;
        contentPath = null;

        // check possible game paths
        foreach (string candidate in this.GetCandidateGamePaths(specifiedPath))
        {
            // detect paths
            string curGamePath = this.TryGamePath(candidate);
            string curContentPath = this.FindContentPath(curGamePath);

            // valid game install found
            if (curGamePath != null && curContentPath != null)
            {
                gamePath = curGamePath;
                contentPath = curContentPath;
                return true;
            }

            // if game folder exists without a content folder, track the first found game path (i.e. the highest-priority one)
            gamePath ??= curGamePath;
        }

        return false;
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Get the possible game paths.</summary>
    /// <param name="specifiedPath">The game path specified by the user, if any.</param>
    private IEnumerable<string> GetCandidateGamePaths(string specifiedPath = null)
    {
        // specified path
        if (!string.IsNullOrWhiteSpace(specifiedPath))
            yield return specifiedPath;

        // current working directory
        yield return AppDomain.CurrentDomain.BaseDirectory;

        // detected game path
        string detectedPath = new ModToolkit().GetGameFolders().FirstOrDefault()?.FullName;
        if (detectedPath != null)
            yield return detectedPath;
    }

    /// <summary>Get the absolute path to the game folder, if it's valid.</summary>
    /// <param name="path">The path to check for a game install.</param>
    private string TryGamePath(string path)
    {
        // game path exists
        if (path == null)
            return null;
        DirectoryInfo gameDir = new DirectoryInfo(path);
        if (!gameDir.Exists)
            return null;

        // has game files
        bool hasGameDll = File.Exists(Path.Combine(gameDir.FullName, "Stardew Valley.dll"));
        if (!hasGameDll)
            return null;

        // isn't the build folder when compiled directly
        bool isCompileFolder = File.Exists(Path.Combine(gameDir.FullName, "StardewXnbHack.exe.config"));
        if (isCompileFolder)
            return null;

        return gameDir.FullName;
    }

    /// <summary>Get the absolute path to the content folder for a given game, if found.</summary>
    /// <param name="gamePath">The absolute path to the game folder.</param>
    private string FindContentPath(string gamePath)
    {
        if (gamePath == null)
            return null;

        foreach (string relativePath in this.GetPossibleRelativeContentPaths())
        {
            DirectoryInfo folder = new DirectoryInfo(Path.Combine(gamePath, relativePath));
            if (folder.Exists)
                return folder.FullName;
        }

        return null;
    }

    /// <summary>Get the possible relative paths for the current platform.</summary>
    private IEnumerable<string> GetPossibleRelativeContentPaths()
    {
        // under game folder on most platforms
        if (this.Platform != Platform.Mac)
            yield return "Content";

        // macOS
        else
        {
            // Steam paths
            // - game path: StardewValley/Contents/MacOS
            // - content:   StardewValley/Contents/Resources/Content
            yield return "../Resources/Content";

            // GOG paths
            // - game path: Stardew Valley.app/Contents/MacOS
            // - content:   Stardew Valley.app/Resources/Content
            yield return "../../Resources/Content";
        }
    }
}


================================================
FILE: StardewXnbHack/Framework/UnpackContext.cs
================================================
using System.Collections.Generic;
using System.IO;
using StardewXnbHack.ProgressHandling;

namespace StardewXnbHack.Framework;

/// <summary>The context info for the current unpack run.</summary>
internal class UnpackContext : IUnpackContext
{
    /*********
    ** Accessors
    *********/
    /// <inheritdoc />
    public string GamePath { get; set; }

    /// <inheritdoc />
    public string ContentPath { get; set; }

    /// <inheritdoc />
    public string ExportPath { get; set; }

    /// <inheritdoc />
    public IEnumerable<FileInfo> Files { get; set; }
}


================================================
FILE: StardewXnbHack/Framework/Writers/BaseAssetWriter.cs
================================================
using System;
using Force.DeepCloner;
using Microsoft.Xna.Framework.Content;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework.Writers;

/// <summary>The base class for an asset writer.</summary>
internal abstract class BaseAssetWriter : IAssetWriter
{
    /*********
    ** Private methods
    *********/
    /// <summary>The settings to use when serializing JSON.</summary>
    private readonly Lazy<JsonSerializerSettings> JsonSettings;


    /*********
    ** Public methods
    *********/
    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    public abstract bool CanWrite(object asset);

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    public abstract bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error);


    /*********
    ** Protected methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="omitDefaultFields">Whether to ignore members marked <see cref="ContentSerializerAttribute.Optional"/> which match the default value.</param>
    protected BaseAssetWriter(bool omitDefaultFields = false)
    {
        this.JsonSettings = new(() => BaseAssetWriter.GetJsonSerializerSettings(omitDefaultFields));
    }

    /// <summary>Get a text representation for the given asset.</summary>
    /// <param name="asset">The asset to serialize.</param>
    protected string FormatData(object asset)
    {
        return JsonConvert.SerializeObject(asset, this.JsonSettings.Value);
    }

    /// <summary>Get the recommended file extension for a data file formatted with <see cref="FormatData"/>.</summary>
    protected string GetDataExtension()
    {
        return "json";
    }

    /// <summary>Get the serializer settings to apply when writing JSON.</summary>
    /// <param name="omitDefaultFields">Whether to ignore members marked <see cref="ContentSerializerAttribute.Optional"/> which match the default value.</param>
    private static JsonSerializerSettings GetJsonSerializerSettings(bool omitDefaultFields = false)
    {
        JsonHelper jsonHelper = new();
        JsonSerializerSettings settings = jsonHelper.JsonSettings.DeepClone();

        settings.ContractResolver = new IgnoreDefaultOptionalPropertiesResolver(omitDefaultFields);

        return settings;
    }
}


================================================
FILE: StardewXnbHack/Framework/Writers/DataWriter.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework.Writers;

/// <summary>Writes <see cref="Dictionary{TKey,TValue}"/> and <see cref="List{T}"/> assets to disk.</summary>
internal class DataWriter : BaseAssetWriter
{
    /*********
    ** Public methods
    *********/
    /// <inheritdoc />
    public DataWriter(bool omitDefaultFields)
        : base(omitDefaultFields) { }

    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    public override bool CanWrite(object asset)
    {
        Type type = asset.GetType();
        type = type.IsGenericType ? type.GetGenericTypeDefinition() : type;

        return
            type == typeof(Dictionary<,>)
            || type == typeof(List<>)
            || type.FullName?.StartsWith("StardewValley.GameData.") == true;
    }

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)
    {
        File.WriteAllText($"{toPathWithoutExtension}.{this.GetDataExtension()}", this.FormatData(asset));

        error = null;
        return true;
    }
}


================================================
FILE: StardewXnbHack/Framework/Writers/IAssetWriter.cs
================================================
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework.Writers;

/// <summary>Writes assets to disk.</summary>
internal interface IAssetWriter
{
    /*********
    ** Methods
    *********/
    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    bool CanWrite(object asset);

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error);
}


================================================
FILE: StardewXnbHack/Framework/Writers/IgnoreDefaultOptionalPropertiesResolver.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework.Content;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Sickhead.Engine.Util;

namespace StardewXnbHack.Framework.Writers;

/// <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>
internal class IgnoreDefaultOptionalPropertiesResolver : DefaultContractResolver
{
    /*********
    ** Fields
    *********/
    /// <summary>Whether to ignore members marked <see cref="ContentSerializerAttribute.Optional"/> which match the default value.</summary>
    private readonly bool OmitDefaultValues;

    /// <summary>The default values for fields and properties marked <see cref="ContentSerializerAttribute.Optional"/>.</summary>
    private readonly Dictionary<string, Dictionary<string, object>> DefaultValues = new();


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="omitDefaultValues">Whether to ignore members marked <see cref="ContentSerializerAttribute.Optional"/> which match the default value.</param>
    public IgnoreDefaultOptionalPropertiesResolver(bool omitDefaultValues)
    {
        this.OmitDefaultValues = omitDefaultValues;
    }


    /*********
    ** Protected methods
    *********/
    /// <inheritdoc />
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        // property marked ignore
        if (member.GetCustomAttribute<ContentSerializerIgnoreAttribute>() != null)
            property.ShouldSerialize = _ => false;

        // property marked optional which matches default value
        else if (this.OmitDefaultValues)
        {
            Dictionary<string, object>? optionalMembers = this.GetDefaultValues(member.DeclaringType);
            if (optionalMembers != null && optionalMembers.TryGetValue(member.Name, out object defaultValue))
            {
                property.ShouldSerialize = instance =>
                {
                    object value = member.GetValue(instance);
                    return !defaultValue?.Equals(value) ?? value is not null;
                };
            }
        }

        return property;
    }

    /// <summary>The default values for a type's fields and properties marked <see cref="ContentSerializerAttribute.Optional"/>, if any.</summary>
    /// <param name="type">The type whose fields and properties to get default values for.</param>
    /// <returns>Returns a dictionary of default values by member name if any were found, else <c>null</c>.</returns>
    private Dictionary<string, object>? GetDefaultValues(Type type)
    {
        // skip invalid
        if (!type.IsClass || type.FullName is null || type.Namespace?.StartsWith("StardewValley") != true)
            return null;

        // skip if already cached
        if (this.DefaultValues.TryGetValue(type.FullName, out Dictionary<string, object> defaults))
            return defaults;

        // get members
        MemberInfo[] optionalMembers =
            (type.GetFields().OfType<MemberInfo>())
            .Concat(type.GetProperties())
            .Where(member => member.GetCustomAttribute<ContentSerializerAttribute>()?.Optional is true)
            .ToArray();
        if (optionalMembers.Length == 0)
            return this.DefaultValues[type.FullName] = null;

        // get default instance
        object defaultInstance;
        try
        {
            defaultInstance = Activator.CreateInstance(type);
        }
        catch
        {
            return this.DefaultValues[type.FullName] = null;
        }

        // get default values
        defaults = new Dictionary<string, object>();
        foreach (MemberInfo member in optionalMembers)
            defaults[member.Name] = member.GetValue(defaultInstance);
        return this.DefaultValues[type.FullName] = defaults;
    }
}


================================================
FILE: StardewXnbHack/Framework/Writers/MapWriter.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using TMXTile;
using xTile;
using xTile.Dimensions;
using xTile.Layers;
using xTile.Tiles;

namespace StardewXnbHack.Framework.Writers;

/// <summary>Writes <see cref="Map"/> assets to disk.</summary>
internal class MapWriter : BaseAssetWriter
{
    /*********
    ** Fields
    *********/
    /// <summary>The actual size of a tile in the tilesheet.</summary>
    const int TileSize = Game1.tileSize / Game1.pixelZoom;

    /// <summary>The underlying map format handler.</summary>
    private readonly TMXFormat Format;


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    public MapWriter()
    {
        // init TMX support
        this.Format = new TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom);
    }

    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    public override bool CanWrite(object asset)
    {
        return asset is Map;
    }

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)
    {
        Map map = (Map)asset;

        // fix tile sizes (game overrides them in-memory)
        IDictionary<Layer, Size> tileSizes = new Dictionary<Layer, Size>();
        foreach (var layer in map.Layers)
        {
            tileSizes[layer] = layer.TileSize;
            layer.TileSize = new Size(MapWriter.TileSize, MapWriter.TileSize);
        }

        // fix image sources (game overrides them in-memory)
        IDictionary<TileSheet, string> imageSources = new Dictionary<TileSheet, string>();
        foreach (var sheet in map.TileSheets)
        {
            imageSources[sheet] = sheet.ImageSource;
            sheet.ImageSource = this.GetOriginalImageSource(relativePath, sheet.ImageSource);
        }

        // save file
        using (Stream stream = new MemoryStream())
        {
            // serialize to stream
            this.Format.Store(map, stream, DataEncodingType.CSV);

            // workaround: TMXTile doesn't indent the XML in newer .NET versions
            stream.Position = 0;
            var doc = XDocument.Load(stream);
            File.WriteAllText($"{toPathWithoutExtension}.tmx", "<?xml version=\"1.0\"?>\n" + doc.ToString());
        }

        // undo changes
        foreach (var layer in map.Layers)
            layer.TileSize = tileSizes[layer];
        foreach (var sheet in map.TileSheets)
            sheet.ImageSource = imageSources[sheet];

        error = null;
        return true;
    }


    /*********
    ** Public methods
    *********/
    /// <summary>Get the image source for a map tilesheet without the game's automatic path changes.</summary>
    /// <param name="relativeMapPath">The relative path to the map file within the content folder.</param>
    /// <param name="imageSource">The tilesheet image source.</param>
    private string GetOriginalImageSource(string relativeMapPath, string imageSource)
    {
        string mapDirPath = PathUtilities.NormalizePath(Path.GetDirectoryName(relativeMapPath));
        string normalizedImageSource = PathUtilities.NormalizePath(imageSource);

        return normalizedImageSource.StartsWith($"{mapDirPath}{PathUtilities.PreferredPathSeparator}", StringComparison.OrdinalIgnoreCase)
            ? imageSource.Substring(mapDirPath.Length + 1)
            : imageSource;
    }
}


================================================
FILE: StardewXnbHack/Framework/Writers/SpriteFontWriter.cs
================================================
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework.Writers;

/// <summary>Writes <see cref="SpriteFont"/> assets to disk.</summary>
internal class SpriteFontWriter : BaseAssetWriter
{
    /*********
    ** Public methods
    *********/
    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    public override bool CanWrite(object asset)
    {
        return asset is SpriteFont;
    }

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)
    {
        SpriteFont font = (SpriteFont)asset;

        // get texture
        Texture2D texture = font.Texture;

        // save texture
        using (Stream stream = File.Create($"{toPathWithoutExtension}.png"))
        {
            if (texture.Format == SurfaceFormat.Dxt3) // MonoGame can't read DXT3 textures directly, need to export through GPU
            {
                using RenderTarget2D renderTarget = this.RenderWithGpu(texture);
                renderTarget.SaveAsPng(stream, texture.Width, texture.Height);
            }
            else
                texture.SaveAsPng(stream, texture.Width, texture.Height);
        }

        // save font data
        var data = new
        {
            font.LineSpacing,
            font.Spacing,
            font.DefaultCharacter,
            font.Characters,
            Glyphs = font.GetGlyphs()
        };
        File.WriteAllText($"{toPathWithoutExtension}.{this.GetDataExtension()}", this.FormatData(data));

        error = null;
        return true;
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Draw a texture to a GPU render target.</summary>
    /// <param name="texture">The texture to draw.</param>
    private RenderTarget2D RenderWithGpu(Texture2D texture)
    {
        // set render target
        var gpu = texture.GraphicsDevice;
        RenderTarget2D target = new RenderTarget2D(gpu, texture.Width, texture.Height);
        gpu.SetRenderTarget(target);

        // render
        try
        {
            gpu.Clear(Color.Transparent); // set transparent background

            using SpriteBatch batch = new SpriteBatch(gpu);
            batch.Begin();
            batch.Draw(texture, Vector2.Zero, Color.White);
            batch.End();
        }
        finally
        {
            gpu.SetRenderTarget(null);
        }

        return target;
    }
}


================================================
FILE: StardewXnbHack/Framework/Writers/TextureWriter.cs
================================================
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework.Writers;

/// <summary>Writes <see cref="Texture2D"/> assets to disk.</summary>
internal class TextureWriter : BaseAssetWriter
{
    /*********
    ** Public methods
    *********/
    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    public override bool CanWrite(object asset)
    {
        return asset is Texture2D;
    }

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)
    {
        Texture2D texture = (Texture2D)asset;

        this.UnpremultiplyTransparency(texture);
        using (Stream stream = File.Create($"{toPathWithoutExtension}.png"))
            texture.SaveAsPng(stream, texture.Width, texture.Height);

        error = null;
        return true;
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Reverse premultiplication applied to an image asset by the XNA content pipeline.</summary>
    /// <param name="texture">The texture to adjust.</param>
    private void UnpremultiplyTransparency(Texture2D texture)
    {
        Color[] data = new Color[texture.Width * texture.Height];
        texture.GetData(data);

        for (int i = 0; i < data.Length; i++)
        {
            Color pixel = data[i];
            if (pixel.A == 0)
                continue;

            data[i] = new Color(
                (byte)((pixel.R * 255) / pixel.A),
                (byte)((pixel.G * 255) / pixel.A),
                (byte)((pixel.B * 255) / pixel.A),
                pixel.A
            ); // don't use named parameters, which are inconsistent between MonoGame (e.g. 'alpha') and XNA (e.g. 'a')
        }

        texture.SetData(data);
    }
}


================================================
FILE: StardewXnbHack/Framework/Writers/XmlSourceWriter.cs
================================================
using System.IO;
using BmFont;
using StardewModdingAPI.Toolkit.Utilities;

namespace StardewXnbHack.Framework.Writers;

/// <summary>Writes <see cref="XmlSource"/> assets to disk.</summary>
internal class XmlSourceWriter : BaseAssetWriter
{
    /*********
    ** Public methods
    *********/
    /// <summary>Whether the writer can handle a given asset.</summary>
    /// <param name="asset">The asset value.</param>
    public override bool CanWrite(object asset)
    {
        return asset is XmlSource;
    }

    /// <summary>Write an asset instance to disk.</summary>
    /// <param name="asset">The asset value.</param>
    /// <param name="toPathWithoutExtension">The absolute path to the export file, without the file extension.</param>
    /// <param name="relativePath">The relative path within the content folder.</param>
    /// <param name="platform">The operating system running the unpacker.</param>
    /// <param name="error">An error phrase indicating why writing to disk failed (if applicable).</param>
    /// <returns>Returns whether writing to disk completed successfully.</returns>
    public override bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error)
    {
        XmlSource value = (XmlSource)asset;
        File.WriteAllText($"{toPathWithoutExtension}.fnt", value.Source);

        error = null;
        return true;
    }
}


================================================
FILE: StardewXnbHack/Program.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using StardewXnbHack.Framework;
using StardewXnbHack.Framework.Writers;
using StardewXnbHack.ProgressHandling;

namespace StardewXnbHack;

/// <summary>The console app entry point.</summary>
public static class Program
{
    /*********
    ** Fields
    *********/
    /// <summary>The relative paths to search for unresolved assembly files.</summary>
    private static readonly string[] RelativeAssemblyProbePaths =
    {
        "", // app directory
        "smapi-internal"
    };


    /*********
    ** Public methods
    *********/
    /// <summary>The console app entry method.</summary>
    /// <param name="args">The command-line arguments.</param>
    internal static void Main(string[] args)
    {
        // set window title
        Console.Title = $"StardewXnbHack {Program.GetUnpackerVersion()}";

        // check platform
        Program.AssertPlatform();

        // Add fallback assembly resolution that loads DLLs from a 'smapi-internal' subfolder,
        // so it can be run from the game folder. This must be set before any references to
        // game or toolkit types (including IAssetWriter which references the toolkit's
        // Platform enum).
        AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve;

        // launch app
        try
        {
            Program.Run(args);
        }
        catch (Exception ex)
        {
            // not in game folder
            if (ex is FileNotFoundException fileNotFoundEx)
            {
                AssemblyName assemblyName = new AssemblyName(fileNotFoundEx.FileName);
                if (assemblyName.Name == "Stardew Valley")
                {
                    Console.WriteLine("Oops! StardewXnbHack must be placed in the Stardew Valley game folder.\nSee instructions: https://github.com/Pathoschild/StardewXnbHack#readme.");
                    DefaultConsoleLogger.PressAnyKeyToExit();
                    return;
                }
            }

            // generic unhandled exception
            Console.WriteLine("Oops! Something went wrong running the unpacker:");
            Console.WriteLine(ex.ToString());
            DefaultConsoleLogger.PressAnyKeyToExit();
        }
    }

    /// <summary>Unpack all assets in the content folder and store them in the output folder.</summary>
    /// <param name="args">The command-line arguments.</param>
    /// <param name="game">The game instance through which to unpack files, or <c>null</c> to launch a temporary internal instance.</param>
    /// <param name="gamePath">The absolute path to the game folder, or <c>null</c> to auto-detect it.</param>
    /// <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>
    /// <param name="showPressAnyKeyToExit">Whether the default logger should show a 'press any key to exit' prompt when it finishes.</param>
    public static void Run(string[] args, GameRunner game = null, string gamePath = null, Func<IUnpackContext, IProgressLogger, IProgressLogger> getLogger = null, bool showPressAnyKeyToExit = true)
    {
        // init logging
        UnpackContext context = new UnpackContext();
        IProgressLogger logger = new DefaultConsoleLogger(context, showPressAnyKeyToExit);

        try
        {
            // get override logger
            if (getLogger != null)
                logger = getLogger(context, logger);

            // read command-line arguments
            bool omitDefaultFields = args.Contains("--clean");

            // start log
            logger.OnStepChanged(ProgressStep.Started, $"Running StardewXnbHack {Program.GetUnpackerVersion()}.{(omitDefaultFields ? " Special options: omit default fields." : "")}");

            // start timer
            Stopwatch timer = new Stopwatch();
            timer.Start();

            // get asset writers
            var assetWriters = new IAssetWriter[]
            {
                new MapWriter(),
                new SpriteFontWriter(),
                new TextureWriter(),
                new XmlSourceWriter(),
                new DataWriter(omitDefaultFields) // check last due to more expensive CanWrite
            };

            // get paths
            var platform = new PlatformContext();
            {
                if (platform.TryDetectGamePaths(gamePath, out gamePath, out string contentPath))
                {
                    context.GamePath = gamePath;
                    context.ContentPath = contentPath;
                }
                else
                {
                    logger.OnFatalError(gamePath == null
                        ? "Can't find the Stardew Valley folder. Try running StardewXnbHack from the game folder instead."
                        : $"Can't find the content folder for the game at {gamePath}. Is the game installed correctly?"
                    );
                    return;
                }
            }
            context.ExportPath = Path.Combine(context.GamePath, "Content (unpacked)");
            logger.OnStepChanged(ProgressStep.GameFound, $"Found game folder: {context.GamePath}.");

            // symlink files on Linux/Mac
            if (platform.Is(Platform.Linux, Platform.Mac))
            {
                foreach (string dirName in new[] { "lib", "lib64" })
                {
                    string fullPath = Path.Combine(context.GamePath, dirName);
                    if (!Directory.Exists(dirName))
                        Process.Start("ln", $"-sf \"{fullPath}\"");
                }
            }

            // load game instance
            bool disposeGame = false;
            if (game == null)
            {
                logger.OnStepChanged(ProgressStep.LoadingGameInstance, "Loading game instance...");
                game = Program.CreateTemporaryGameInstance(platform, context.ContentPath);
                disposeGame = true;
            }

            // unpack files
            try
            {
                logger.OnStepChanged(ProgressStep.UnpackingFiles, $"Unpacking files for Stardew Valley {Program.GetGameVersion()}...");

                // collect files
                DirectoryInfo contentDir = new DirectoryInfo(context.ContentPath);
                FileInfo[] files = contentDir.EnumerateFiles("*.xnb", SearchOption.AllDirectories).ToArray();
                context.Files = files;

                // write assets
                foreach (FileInfo file in files)
                {
                    // prepare paths
                    string assetName = file.FullName.Substring(context.ContentPath.Length + 1, file.FullName.Length - context.ContentPath.Length - 5); // remove root path + .xnb extension
                    string relativePath = $"{assetName}.xnb";
                    string fileExportPath = Path.Combine(context.ExportPath, assetName);
                    Directory.CreateDirectory(Path.GetDirectoryName(fileExportPath));

                    // fallback logic
                    void ExportRawXnb()
                    {
                        File.Copy(file.FullName, $"{fileExportPath}.xnb", overwrite: true);
                    }

                    // show progress bar
                    logger.OnFileUnpacking(assetName);

                    // read asset
                    object asset;
                    try
                    {
                        asset = game.Content.Load<object>(assetName);
                    }
                    catch (Exception ex)
                    {
                        if (ex.Message == "This does not appear to be a MonoGame MGFX file!")
                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.UnsupportedFileType, $"{nameof(Effect)} isn't a supported asset type.");
                        else
                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.ReadError, $"read error: {ex.Message}");
                        ExportRawXnb();
                        continue;
                    }

                    // write asset
                    try
                    {
                        // get writer
                        IAssetWriter writer = assetWriters.FirstOrDefault(p => p.CanWrite(asset));

                        // write file
                        if (writer == null)
                        {
                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.UnsupportedFileType, $"{asset.GetType().Name} isn't a supported asset type.");
                            ExportRawXnb();
                        }
                        else if (!writer.TryWriteFile(asset, fileExportPath, assetName, platform.Platform, out string writeError))
                        {
                            logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.WriteError, $"{asset.GetType().Name} file could not be saved: {writeError}.");
                            ExportRawXnb();
                        }
                    }
                    catch (Exception ex)
                    {
                        logger.OnFileUnpackFailed(relativePath, UnpackFailedReason.UnknownError, $"unhandled export error: {ex.Message}");
                    }
                    finally
                    {
                        game.Content.Unload();
                    }
                }
            }
            finally
            {
                if (disposeGame)
                    game.Dispose();
            }

            logger.OnStepChanged(ProgressStep.Done, $"Done! Unpacked {context.Files.Count()} files in {Program.GetHumanTime(timer.Elapsed)}.\nUnpacked into {context.ExportPath}.");
        }
        catch (Exception ex)
        {
            logger.OnFatalError($"Unhandled exception: {ex}");
        }
        finally
        {
            logger.OnEnded();
        }
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Assert that the current platform matches the one StardewXnbHack was compiled for.</summary>
    private static void AssertPlatform()
    {
        bool isWindows = Environment.OSVersion.Platform != PlatformID.MacOSX && Environment.OSVersion.Platform != PlatformID.Unix;

#if IS_FOR_WINDOWS
        if (!isWindows)
        {
            Console.WriteLine("Oops! This is the Windows version of StardewXnbHack. Make sure to install the Windows version instead.");
            DefaultConsoleLogger.PressAnyKeyToExit();
        }
#else
            if (isWindows)
            {
                Console.WriteLine("Oops! This is the Linux/macOS version of StardewXnbHack. Make sure to install the version for your OS type instead.");
                DefaultConsoleLogger.PressAnyKeyToExit();
            }
#endif
    }

    /// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary>
    /// <param name="sender">The event sender.</param>
    /// <param name="e">The event arguments.</param>
    private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
    {
        // get assembly name
        AssemblyName name = new AssemblyName(e.Name);
        if (name.Name == null)
            return null;

        // check search folders
        foreach (string relativePath in Program.RelativeAssemblyProbePaths)
        {
            // get absolute path of search folder
            string searchPath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), relativePath);
            if (!Directory.Exists(searchPath))
                continue;

            // try to resolve DLL
            try
            {
                foreach (FileInfo dll in new DirectoryInfo(searchPath).EnumerateFiles("*.dll"))
                {
                    // get assembly name
                    string dllAssemblyName;
                    try
                    {
                        dllAssemblyName = AssemblyName.GetAssemblyName(dll.FullName).Name;
                    }
                    catch
                    {
                        continue;
                    }

                    // check for match
                    if (name.Name.Equals(dllAssemblyName, StringComparison.OrdinalIgnoreCase))
                        return Assembly.LoadFrom(dll.FullName);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error resolving assembly: {ex}");
                return null;
            }
        }

        return null;
    }

    /// <summary>Create a temporary game instance for the unpacker.</summary>
    /// <param name="platform">The platform-specific context.</param>
    /// <param name="contentPath">The absolute path to the content folder to import.</param>
    private static GameRunner CreateTemporaryGameInstance(PlatformContext platform, string contentPath)
    {
        var foregroundColor = Console.ForegroundColor;
        Console.ForegroundColor = ConsoleColor.DarkGray;

        try
        {
            GameRunner game = new GameRunner();
            GameRunner.instance = game;

            Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef;

            game.RunOneFrame();

            return game;
        }
        finally
        {
            Console.ForegroundColor = foregroundColor;
            Console.WriteLine();
        }
    }

    /// <summary>Get a human-readable representation of elapsed time.</summary>
    /// <param name="time">The elapsed time.</param>
    private static string GetHumanTime(TimeSpan time)
    {
        List<string> parts = new List<string>(2);

        if (time.TotalMinutes >= 1)
            parts.Add($"{time.TotalMinutes:0} minute{(time.TotalMinutes >= 2 ? "s" : "")}");
        if (time.Seconds > 0)
            parts.Add($"{time.Seconds:0} second{(time.Seconds > 1 ? "s" : "")}");

        return string.Join(" ", parts);
    }

    /// <summary>Get the version string for StardewXnbHack.</summary>
    private static string GetUnpackerVersion()
    {
        var attribute = typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
        return attribute?.InformationalVersion ?? "<unknown version>";
    }

    /// <summary>Get the version string for Stardew Valley.</summary>
    private static string GetGameVersion()
    {
        StringBuilder version = new();

        // base version
        version.Append(Game1.version);
        version.Append(" (");

        // build number
        {
            string buildVersion = typeof(Game1).Assembly.GetName().Version?.ToString() ?? "unknown";
            if (buildVersion.StartsWith($"{Game1.version}."))
                buildVersion = buildVersion.Substring(Game1.version.Length + 1);
            version.Append(buildVersion);
        }

        // version label
        if (!string.IsNullOrWhiteSpace(Game1.versionLabel))
        {
            version.Append(" \"");
            version.Append(Game1.versionLabel);
            version.Append('"');
        }

        version.Append(')');

        return version.ToString();
    }
}


================================================
FILE: StardewXnbHack/ProgressHandling/IProgressLogger.cs
================================================
namespace StardewXnbHack.ProgressHandling;

/// <summary>Logs updates while the unpacker is running.</summary>
public interface IProgressLogger
{
    /*********
    ** Methods
    *********/
    /// <summary>Log an error which halts the unpack process (e.g. game folder missing).</summary>
    /// <param name="error">The error message indicating why the unpacker halted.</param>
    void OnFatalError(string error);

    /// <summary>Log a step transition in the overall unpack process.</summary>
    /// <param name="step">The new step.</param>
    /// <param name="message">The default log message for the step transition.</param>
    void OnStepChanged(ProgressStep step, string message);

    /// <summary>Log a file being unpacked.</summary>
    /// <param name="relativePath">The relative path of the file within the content folder.</param>
    void OnFileUnpacking(string relativePath);

    /// <summary>Log a file which couldn't be unpacked.</summary>
    /// <param name="relativePath">The relative path of the file within the content folder.</param>
    /// <param name="errorCode">An error code indicating why unpacking failed.</param>
    /// <param name="errorMessage">An error message indicating why unpacking failed.</param>
    void OnFileUnpackFailed(string relativePath, UnpackFailedReason errorCode, string errorMessage);

    /// <summary>The unpacker is done and exiting.</summary>
    void OnEnded();
}


================================================
FILE: StardewXnbHack/ProgressHandling/IUnpackContext.cs
================================================
using System.Collections.Generic;
using System.IO;

namespace StardewXnbHack.ProgressHandling;

/// <summary>The context info for the current unpack run.</summary>
public interface IUnpackContext
{
    /// <summary>The absolute path to the game folder.</summary>
    string GamePath { get; }

    /// <summary>The absolute path to the content folder.</summary>
    string ContentPath { get; }

    /// <summary>The absolute path to the folder containing exported assets.</summary>
    string ExportPath { get; }

    /// <summary>The files found to unpack.</summary>
    IEnumerable<FileInfo> Files { get; }
}


================================================
FILE: StardewXnbHack/ProgressHandling/ProgressStep.cs
================================================
namespace StardewXnbHack.ProgressHandling;

/// <summary>A step in the overall unpack process.</summary>
public enum ProgressStep
{
    /// <summary>The unpacker has started.</summary>
    Started,

    /// <summary>The game folder was located, but unpacking hasn't started yet.</summary>
    GameFound,

    /// <summary>The temporary game instance is being loaded.</summary>
    LoadingGameInstance,

    /// <summary>The files are being unpacked.</summary>
    UnpackingFiles,

    /// <summary>The overall unpack process completed successfully.</summary>
    Done
}


================================================
FILE: StardewXnbHack/ProgressHandling/UnpackFailedReason.cs
================================================
namespace StardewXnbHack.ProgressHandling;

/// <summary>An error code indicating why unpacking failed for a file.</summary>
public enum UnpackFailedReason
{
    /// <summary>An error occurred trying to read the raw XNB asset.</summary>
    ReadError,

    /// <summary>The XNB asset was successfully loaded, but its file format can't be unpacked.</summary>
    UnsupportedFileType,

    /// <summary>The XNB asset was successfully loaded, but an error occurred trying to save the unpacked file.</summary>
    WriteError,

    /// <summary>An unhandled error occurred.</summary>
    UnknownError
}


================================================
FILE: StardewXnbHack/Properties/PublishProfiles/FolderProfile.pubxml
================================================
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <Configuration>Release</Configuration>
        <Platform>x64</Platform>
        <PublishDir>bin\x64\Release\net6.0\publish\win-x64\</PublishDir>
        <PublishProtocol>FileSystem</PublishProtocol>
        <TargetFramework>net6.0</TargetFramework>
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>
        <SelfContained>true</SelfContained>
        <PublishSingleFile>True</PublishSingleFile>
        <PublishReadyToRun>False</PublishReadyToRun>
        <PublishTrimmed>False</PublishTrimmed>
    </PropertyGroup>
</Project>


================================================
FILE: StardewXnbHack/StardewXnbHack.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!--metadata-->
    <Authors>Pathoschild</Authors>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <RepositoryUrl>https://github.com/Pathoschild/StardewXnbHack</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <Version>1.1.2</Version>

    <!--build-->
    <TargetFramework>net6.0</TargetFramework>
    <Platforms>x64</Platforms>
    <OutputType>Exe</OutputType>
    <ApplicationIcon>assets/icon.ico</ApplicationIcon>
    <DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);IS_FOR_WINDOWS</DefineConstants>

    <!--don't append Git commit SHA to version, since it's shown in the app-->
    <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>

    <!--mod build package-->
    <CopyModReferencesToBuildOutput>True</CopyModReferencesToBuildOutput>
    <EnableGameDebugging>False</EnableGameDebugging>
    <EnableModDeploy>False</EnableModDeploy>
    <EnableModZip>False</EnableModZip>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
    <PackageReference Include="Pathoschild.Stardew.ModBuildConfig" Version="4.3.1" />
    <PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="BmFont" HintPath="$(GamePath)\BmFont.dll" />
    <Reference Include="SMAPI.Toolkit" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.dll" />
  </ItemGroup>
</Project>


================================================
FILE: StardewXnbHack.sln
================================================

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31912.275
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewXnbHack", "StardewXnbHack\StardewXnbHack.csproj", "{F1512B6A-75A9-4425-B4EE-26D50E7075C8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{5EDB5467-BFEE-4122-8764-FA41B4E59D7F}"
	ProjectSection(SolutionItems) = preProject
		.editorconfig = .editorconfig
		.gitattributes = .gitattributes
		.gitignore = .gitignore
		LICENSE = LICENSE
		README.md = README.md
		release-notes.md = release-notes.md
	EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build-scripts", "build-scripts", "{26C61354-013D-4E87-ADD8-951C04FFB185}"
	ProjectSection(SolutionItems) = preProject
		build-scripts\prepare-release-packages.sh = build-scripts\prepare-release-packages.sh
	EndProjectSection
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|x64 = Debug|x64
		Release|x64 = Release|x64
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Debug|x64.ActiveCfg = Debug|x64
		{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Debug|x64.Build.0 = Debug|x64
		{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Release|x64.ActiveCfg = Release|x64
		{F1512B6A-75A9-4425-B4EE-26D50E7075C8}.Release|x64.Build.0 = Release|x64
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(NestedProjects) = preSolution
		{26C61354-013D-4E87-ADD8-951C04FFB185} = {5EDB5467-BFEE-4122-8764-FA41B4E59D7F}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {ED3F1CBF-3651-49A3-9652-9A8BCD952184}
	EndGlobalSection
EndGlobal


================================================
FILE: StardewXnbHack.sln.DotSettings
================================================
<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">
	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeThisQualifier/@EntryIndexedValue">DO_NOT_SHOW</s:String>
	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
	<s:Boolean x:Key="/Default/UserDictionary/Words/=Stardew/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

================================================
FILE: build-scripts/prepare-release-packages.sh
================================================
#!/bin/bash


##########
## Constants
##########
gamePath="/home/pathoschild/Stardew Valley"
buildConfig="Release"
platforms=("Linux" "macOS" "Windows")
declare -A runtimes=(["Linux"]="linux-x64" ["macOS"]="osx-x64" ["Windows"]="win-x64")
declare -A msBuildPlatformNames=(["Linux"]="Unix" ["macOS"]="OSX" ["Windows"]="Windows_NT")


##########
## Move to solution root
##########
cd "`dirname "$0"`/.."


##########
## Clear old build files
##########
echo "Clearing old builds..."
echo "-----------------------"
for path in **/bin **/obj; do
    echo "$path"
    rm -rf "$path"
done
rm -rf "bin"
echo ""


##########
## Compile files
##########
version="$1"
if [ $# -eq 0 ]; then
    echo "StardewXnbHack release version (like '2.0.0'):"
    read version
fi


##########
## Compile files
##########
for platform in ${platforms[@]}; do
    # constants
    runtime=${runtimes[$platform]}
    msbuildPlatformName=${msBuildPlatformNames[$platform]}
    binPath="StardewXnbHack/bin/Release/net6.0/$runtime/publish"
    folderName="StardewXnbHack $version for $platform"
    bundlePath="bin/$folderName"

    # compile
    echo "Compiling for $platform..."
    echo "--------------------------"
    dotnet publish StardewXnbHack --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:PublishSingleFile=True --self-contained true
    echo ""
    echo ""

    # build package folder
    echo "Preparing package for $platform..."
    echo "----------------------------------"
    mkdir "$bundlePath" --parents
    cp "$binPath/StardewXnbHack"* "$bundlePath"

    if [ -t "$bundlePath/StardewXnbHack" ]; then
        chmod 755 "$bundlePath/StardewXnbHack"
    fi

    # zip package
    pushd bin > /dev/null
    zip -9 "$folderName.zip" "$folderName" --recurse-paths --quiet
    popd > /dev/null
    echo "Package created at $(pwd)/bin/$folderName.zip"
    echo ""
    echo ""
done
exit

echo "Done!"


================================================
FILE: release-notes.md
================================================
[← back to readme](README.md)

# Release notes
## 1.1.2
Released 05 November 2024.

* Updated for SMAPI 4.1.4.

## 1.1.1
Released 20 August 2024.

* Updated for Stardew Valley 1.6.9 and SMAPI 4.1.0.

## 1.1.0
Released 08 April 2024.

* You can now omit data fields which match their default value by calling `StardewXnbHack.exe --clean`.
* Updated for SMAPI 4.0.6.
* Fixed StardewXnbHack version shown in the console including a Git commit SHA in recent versions.

## 1.0.8
Released 19 March 2024.

* Updated for Stardew Valley 1.6.
* Added StardewXnbHack & game versions to console output.
* Data model properties marked with `[ContentSerializerIgnore]` are now omitted from output `.json` files.

## 1.0.7
Released 28 December 2021.

* Fixed exported `.tmx` files no longer indented.

## 1.0.6
Released 04 December 2021.

* StardewXnbHack no longer needs .NET to be installed.
* Updated to .NET 6.
* Fixed launch errors on macOS and Windows.

## 1.0.5
Released 15 September 2021.

* Updated for Stardew Valley 1.5.5 and .NET 5.
* Fixed some textures not unpacked correctly on Linux/macOS.
* Fixed _cannot be loaded into a Reach GraphicsDevice_ error for some users.

## 1.0.4
Released 23 March 2021.

* Added a descriptive error if you install the wrong version (e.g. the Windows version on macOS).
* Updated for SMAPI 3.9.5 (thanks to nyrdosh!).

## 1.0.3
Released 21 December 2020.

* Updated for Stardew Valley 1.5.

## 1.0.2
Released 07 December 2020.

* Assets on macOS are now unpacked into the game folder instead of resources, for consistency with other platforms.
* Improved error if the game's content folder is missing.
* Fixed duplicate tile index properties in some cases.
* Fixed unpack error on macOS with Steam.

## 1.0.1
Released 21 November 2020.

* Fixed `.tmx` map files losing tile index properties.

## 1.0
Released 04 October 2020.

* Added compiled release.
* Added icon/mascot (thanks to ParadigmNomad!).
* Added support for running it from the game folder or another app.
* Added file count and unpack time to log.
* Improved compatibility on Linux/macOS.
* Changed map format from `.tbin` to `.tmx` (thanks to Platonymous!).
* Fixed unsupported XNB files not always copied into the export folder.
* Fixed BMFont file extension set to `.xml` instead of `.fnt` (thanks to Platonymous!).

## Prerelease
Includes changes between 16 June 2019 and 25 April 2020, which didn't have packaged releases.

* Initial implementation with support for...
  * unpacking data (`.json`);
  * unpacking maps (`.tbin`);
  * unpacking textures (`.png`);
  * unpacking SpriteFont (`.png` and `.json`), and BMFont (`.png` and `.xml`) font files.
  * Linux/macOS/Windows.
* Fixed macOS build error (thanks to strobel1ght!).
Download .txt
gitextract_2qw3mmz3/

├── .editorconfig
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── StardewXnbHack/
│   ├── Framework/
│   │   ├── ConsoleProgressBar.cs
│   │   ├── DefaultConsoleLogger.cs
│   │   ├── PlatformContext.cs
│   │   ├── UnpackContext.cs
│   │   └── Writers/
│   │       ├── BaseAssetWriter.cs
│   │       ├── DataWriter.cs
│   │       ├── IAssetWriter.cs
│   │       ├── IgnoreDefaultOptionalPropertiesResolver.cs
│   │       ├── MapWriter.cs
│   │       ├── SpriteFontWriter.cs
│   │       ├── TextureWriter.cs
│   │       └── XmlSourceWriter.cs
│   ├── Program.cs
│   ├── ProgressHandling/
│   │   ├── IProgressLogger.cs
│   │   ├── IUnpackContext.cs
│   │   ├── ProgressStep.cs
│   │   └── UnpackFailedReason.cs
│   ├── Properties/
│   │   └── PublishProfiles/
│   │       └── FolderProfile.pubxml
│   └── StardewXnbHack.csproj
├── StardewXnbHack.sln
├── StardewXnbHack.sln.DotSettings
├── build-scripts/
│   └── prepare-release-packages.sh
└── release-notes.md
Download .txt
SYMBOL INDEX (74 symbols across 17 files)

FILE: StardewXnbHack/Framework/ConsoleProgressBar.cs
  class ConsoleProgressBar (line 6) | internal class ConsoleProgressBar
    method ConsoleProgressBar (line 26) | public ConsoleProgressBar(int totalSteps)
    method Increment (line 32) | public void Increment()
    method Print (line 40) | public void Print(string message, bool removePrevious = true)
    method Erase (line 56) | public void Erase()

FILE: StardewXnbHack/Framework/DefaultConsoleLogger.cs
  class DefaultConsoleLogger (line 8) | internal class DefaultConsoleLogger : IProgressLogger
    method DefaultConsoleLogger (line 29) | public DefaultConsoleLogger(IUnpackContext context, bool showPressAnyK...
    method OnFatalError (line 36) | public void OnFatalError(string error)
    method OnStepChanged (line 42) | public void OnStepChanged(ProgressStep step, string message)
    method OnFileUnpacking (line 53) | public void OnFileUnpacking(string relativePath)
    method OnFileUnpackFailed (line 63) | public void OnFileUnpackFailed(string relativePath, UnpackFailedReason...
    method OnEnded (line 74) | public void OnEnded()
    method PressAnyKeyToExit (line 81) | public static void PressAnyKeyToExit()
    method PrintColor (line 95) | private void PrintColor(string message, ConsoleColor color)

FILE: StardewXnbHack/Framework/PlatformContext.cs
  class PlatformContext (line 11) | internal class PlatformContext
    method Is (line 25) | public bool Is(params Platform[] platforms)
    method TryDetectGamePaths (line 35) | public bool TryDetectGamePaths(string specifiedPath, out string gamePa...
    method GetCandidateGamePaths (line 68) | private IEnumerable<string> GetCandidateGamePaths(string specifiedPath...
    method TryGamePath (line 85) | private string TryGamePath(string path)
    method FindContentPath (line 109) | private string FindContentPath(string gamePath)
    method GetPossibleRelativeContentPaths (line 125) | private IEnumerable<string> GetPossibleRelativeContentPaths()

FILE: StardewXnbHack/Framework/UnpackContext.cs
  class UnpackContext (line 8) | internal class UnpackContext : IUnpackContext

FILE: StardewXnbHack/Framework/Writers/BaseAssetWriter.cs
  class BaseAssetWriter (line 11) | internal abstract class BaseAssetWriter : IAssetWriter
    method CanWrite (line 25) | public abstract bool CanWrite(object asset);
    method TryWriteFile (line 34) | public abstract bool TryWriteFile(object asset, string toPathWithoutEx...
    method BaseAssetWriter (line 42) | protected BaseAssetWriter(bool omitDefaultFields = false)
    method FormatData (line 49) | protected string FormatData(object asset)
    method GetDataExtension (line 55) | protected string GetDataExtension()
    method GetJsonSerializerSettings (line 62) | private static JsonSerializerSettings GetJsonSerializerSettings(bool o...

FILE: StardewXnbHack/Framework/Writers/DataWriter.cs
  class DataWriter (line 9) | internal class DataWriter : BaseAssetWriter
    method DataWriter (line 15) | public DataWriter(bool omitDefaultFields)
    method CanWrite (line 20) | public override bool CanWrite(object asset)
    method TryWriteFile (line 38) | public override bool TryWriteFile(object asset, string toPathWithoutEx...

FILE: StardewXnbHack/Framework/Writers/IAssetWriter.cs
  type IAssetWriter (line 6) | internal interface IAssetWriter
    method CanWrite (line 13) | bool CanWrite(object asset);
    method TryWriteFile (line 22) | bool TryWriteFile(object asset, string toPathWithoutExtension, string ...

FILE: StardewXnbHack/Framework/Writers/IgnoreDefaultOptionalPropertiesResolver.cs
  class IgnoreDefaultOptionalPropertiesResolver (line 13) | internal class IgnoreDefaultOptionalPropertiesResolver : DefaultContract...
    method IgnoreDefaultOptionalPropertiesResolver (line 30) | public IgnoreDefaultOptionalPropertiesResolver(bool omitDefaultValues)
    method CreateProperty (line 40) | protected override JsonProperty CreateProperty(MemberInfo member, Memb...
    method GetDefaultValues (line 68) | private Dictionary<string, object>? GetDefaultValues(Type type)

FILE: StardewXnbHack/Framework/Writers/MapWriter.cs
  class MapWriter (line 16) | internal class MapWriter : BaseAssetWriter
    method MapWriter (line 32) | public MapWriter()
    method CanWrite (line 40) | public override bool CanWrite(object asset)
    method TryWriteFile (line 52) | public override bool TryWriteFile(object asset, string toPathWithoutEx...
    method GetOriginalImageSource (line 101) | private string GetOriginalImageSource(string relativeMapPath, string i...

FILE: StardewXnbHack/Framework/Writers/SpriteFontWriter.cs
  class SpriteFontWriter (line 9) | internal class SpriteFontWriter : BaseAssetWriter
    method CanWrite (line 16) | public override bool CanWrite(object asset)
    method TryWriteFile (line 28) | public override bool TryWriteFile(object asset, string toPathWithoutEx...
    method RenderWithGpu (line 68) | private RenderTarget2D RenderWithGpu(Texture2D texture)

FILE: StardewXnbHack/Framework/Writers/TextureWriter.cs
  class TextureWriter (line 9) | internal class TextureWriter : BaseAssetWriter
    method CanWrite (line 16) | public override bool CanWrite(object asset)
    method TryWriteFile (line 28) | public override bool TryWriteFile(object asset, string toPathWithoutEx...
    method UnpremultiplyTransparency (line 46) | private void UnpremultiplyTransparency(Texture2D texture)

FILE: StardewXnbHack/Framework/Writers/XmlSourceWriter.cs
  class XmlSourceWriter (line 8) | internal class XmlSourceWriter : BaseAssetWriter
    method CanWrite (line 15) | public override bool CanWrite(object asset)
    method TryWriteFile (line 27) | public override bool TryWriteFile(object asset, string toPathWithoutEx...

FILE: StardewXnbHack/Program.cs
  class Program (line 18) | public static class Program
    method Main (line 36) | internal static void Main(string[] args)
    method Run (line 82) | public static void Run(string[] args, GameRunner game = null, string g...
    method AssertPlatform (line 249) | private static void AssertPlatform()
    method CurrentDomain_AssemblyResolve (line 271) | private static Assembly CurrentDomain_AssemblyResolve(object sender, R...
    method CreateTemporaryGameInstance (line 320) | private static GameRunner CreateTemporaryGameInstance(PlatformContext ...
    method GetHumanTime (line 345) | private static string GetHumanTime(TimeSpan time)
    method GetUnpackerVersion (line 358) | private static string GetUnpackerVersion()
    method GetGameVersion (line 365) | private static string GetGameVersion()

FILE: StardewXnbHack/ProgressHandling/IProgressLogger.cs
  type IProgressLogger (line 4) | public interface IProgressLogger
    method OnFatalError (line 11) | void OnFatalError(string error);
    method OnStepChanged (line 16) | void OnStepChanged(ProgressStep step, string message);
    method OnFileUnpacking (line 20) | void OnFileUnpacking(string relativePath);
    method OnFileUnpackFailed (line 26) | void OnFileUnpackFailed(string relativePath, UnpackFailedReason errorC...
    method OnEnded (line 29) | void OnEnded();

FILE: StardewXnbHack/ProgressHandling/IUnpackContext.cs
  type IUnpackContext (line 7) | public interface IUnpackContext

FILE: StardewXnbHack/ProgressHandling/ProgressStep.cs
  type ProgressStep (line 4) | public enum ProgressStep

FILE: StardewXnbHack/ProgressHandling/UnpackFailedReason.cs
  type UnpackFailedReason (line 4) | public enum UnpackFailedReason
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (74K chars).
[
  {
    "path": ".editorconfig",
    "chars": 2170,
    "preview": "# topmost editorconfig\nroot: true\n\n##########\n## General formatting\n## documentation: http://editorconfig.org\n##########"
  },
  {
    "path": ".gitattributes",
    "chars": 44,
    "preview": "# always normalise line endings\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "chars": 316,
    "preview": "# 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/"
  },
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "The MIT License (MIT)\n\nCopyright 2019 Pathoschild\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 5698,
    "preview": "**StardewXnbHack** is a one-way XNB unpacker for Stardew Valley. It supports every Stardew Valley\nasset type, is very ea"
  },
  {
    "path": "StardewXnbHack/Framework/ConsoleProgressBar.cs",
    "chars": 2267,
    "preview": "using System;\n\nnamespace StardewXnbHack.Framework;\n\n/// <summary>Manages a progress bar written to the console.</summary"
  },
  {
    "path": "StardewXnbHack/Framework/DefaultConsoleLogger.cs",
    "chars": 3079,
    "preview": "using System;\nusing System.Linq;\nusing StardewXnbHack.ProgressHandling;\n\nnamespace StardewXnbHack.Framework;\n\n/// <summa"
  },
  {
    "path": "StardewXnbHack/Framework/PlatformContext.cs",
    "chars": 5003,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing StardewModdingAPI.Toolkit;\nusi"
  },
  {
    "path": "StardewXnbHack/Framework/UnpackContext.cs",
    "chars": 569,
    "preview": "using System.Collections.Generic;\nusing System.IO;\nusing StardewXnbHack.ProgressHandling;\n\nnamespace StardewXnbHack.Fram"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/BaseAssetWriter.cs",
    "chars": 3015,
    "preview": "using System;\nusing Force.DeepCloner;\nusing Microsoft.Xna.Framework.Content;\nusing Newtonsoft.Json;\nusing StardewModding"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/DataWriter.cs",
    "chars": 1839,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace S"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/IAssetWriter.cs",
    "chars": 1093,
    "preview": "using StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n/// <summary>Writes assets to d"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/IgnoreDefaultOptionalPropertiesResolver.cs",
    "chars": 4196,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing Microsoft.Xna.Framewor"
  },
  {
    "path": "StardewXnbHack/Framework/Writers/MapWriter.cs",
    "chars": 4228,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Xml.Linq;\nusing StardewModdingAPI.Toolkit."
  },
  {
    "path": "StardewXnbHack/Framework/Writers/SpriteFontWriter.cs",
    "chars": 3182,
    "preview": "using System.IO;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing StardewModdingAPI.Toolkit."
  },
  {
    "path": "StardewXnbHack/Framework/Writers/TextureWriter.cs",
    "chars": 2519,
    "preview": "using System.IO;\nusing Microsoft.Xna.Framework;\nusing Microsoft.Xna.Framework.Graphics;\nusing StardewModdingAPI.Toolkit."
  },
  {
    "path": "StardewXnbHack/Framework/Writers/XmlSourceWriter.cs",
    "chars": 1423,
    "preview": "using System.IO;\nusing BmFont;\nusing StardewModdingAPI.Toolkit.Utilities;\n\nnamespace StardewXnbHack.Framework.Writers;\n\n"
  },
  {
    "path": "StardewXnbHack/Program.cs",
    "chars": 15427,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing Syst"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/IProgressLogger.cs",
    "chars": 1427,
    "preview": "namespace StardewXnbHack.ProgressHandling;\n\n/// <summary>Logs updates while the unpacker is running.</summary>\npublic in"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/IUnpackContext.cs",
    "chars": 610,
    "preview": "using System.Collections.Generic;\nusing System.IO;\n\nnamespace StardewXnbHack.ProgressHandling;\n\n/// <summary>The context"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/ProgressStep.cs",
    "chars": 570,
    "preview": "namespace StardewXnbHack.ProgressHandling;\n\n/// <summary>A step in the overall unpack process.</summary>\npublic enum Pro"
  },
  {
    "path": "StardewXnbHack/ProgressHandling/UnpackFailedReason.cs",
    "chars": 598,
    "preview": "namespace StardewXnbHack.ProgressHandling;\n\n/// <summary>An error code indicating why unpacking failed for a file.</summ"
  },
  {
    "path": "StardewXnbHack/Properties/PublishProfiles/FolderProfile.pubxml",
    "chars": 688,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild"
  },
  {
    "path": "StardewXnbHack/StardewXnbHack.csproj",
    "chars": 1514,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <!--metadata-->\n    <Authors>Pathoschild</Authors>\n    <Package"
  },
  {
    "path": "StardewXnbHack.sln",
    "chars": 1864,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.0.3191"
  },
  {
    "path": "StardewXnbHack.sln.DotSettings",
    "chars": 737,
    "preview": "<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namesp"
  },
  {
    "path": "build-scripts/prepare-release-packages.sh",
    "chars": 1954,
    "preview": "#!/bin/bash\n\n\n##########\n## Constants\n##########\ngamePath=\"/home/pathoschild/Stardew Valley\"\nbuildConfig=\"Release\"\nplatf"
  },
  {
    "path": "release-notes.md",
    "chars": 2729,
    "preview": "[← 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"
  }
]

About this extraction

This page contains the full source code of the Pathoschild/StardewXnbHack GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (68.2 KB), approximately 16.7k tokens, and a symbol index with 74 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!