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.

## 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 ✓ maps ✓ dictionary data ✓ font texture ✓ font XML data ✓ structured data | ✓ images ✓ maps ✓ dictionary data ✓ font textures ✓ font XML data ❑ structured data | ✓ images ✓ maps ✓ dictionary data ✓ font textures ❑ font XML data ❑ structured data
Export formats | ✓ `.png` for images ✓ `.tmx` for maps ✓ `.json` for data ([CP](https://stardewvalleywiki.com/Modding:Content_Patcher)-compatible) | ✓ `.png` for images ✓ `.tbin` for maps¹ ❑ `.json` for data (custom format) | ✓ `.png` for images ✓ `.tbin` for maps¹ ❑ `.yaml` for data
Supported platforms | ✓ Windows ✓ Linux ✓ Mac | ✓ Windows ✓ Linux ✓ Mac | ✓ Windows ❑ Linux ❑ Mac
Supported operations | ✓ unpack ❑ pack | ✓ unpack ✓ pack (uncompressed) | ✓ unpack ✓ pack
Maintainable | ✓ easy to update | ❑ complex | ❑ complex, closed-source
Sample unpack time (full `Content` folder) | ≈0m 43s | ≈6m 5s | ≈2m 20s
License | MIT | GPL | n/a
¹ `.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`.
### 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:
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.
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;
/// Manages a progress bar written to the console.
internal class ConsoleProgressBar
{
/*********
** Fields
*********/
/// The total number of steps to perform.
private readonly int TotalSteps;
/// The current step being performed.
private int CurrentStep;
/// The last line to which the progress bar was output, if any.
private int OutputLine = -1;
/*********
** Public methods
*********/
/// Construct an instance.
/// The total number of steps to perform.
public ConsoleProgressBar(int totalSteps)
{
this.TotalSteps = totalSteps;
}
/// Increment the current step.
public void Increment()
{
this.CurrentStep++;
}
/// Print a progress bar to the console.
/// The message to print.
/// Whether to remove the previously output progress bar.
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;
}
/// Remove the last progress bar written to the console.
/// Derived from .
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;
/// Report updates to the console while the unpacker is running.
internal class DefaultConsoleLogger : IProgressLogger
{
/*********
** Fields
*********/
/// The context info for the current unpack run.
private readonly IUnpackContext Context;
/// Whether to show a 'press any key to exit' prompt on end.
private readonly bool ShowPressAnyKeyToExit;
/// The current progress bar written to the console.
private ConsoleProgressBar ProgressBar;
/*********
** Public methods
*********/
/// Construct an instance.
/// The context info for the current unpack run.
/// Whether to show a 'press any key to exit' prompt on end.
public DefaultConsoleLogger(IUnpackContext context, bool showPressAnyKeyToExit)
{
this.Context = context;
this.ShowPressAnyKeyToExit = showPressAnyKeyToExit;
}
///
public void OnFatalError(string error)
{
this.PrintColor(error, ConsoleColor.Red);
}
///
public void OnStepChanged(ProgressStep step, string message)
{
this.ProgressBar?.Erase();
if (step == ProgressStep.Done)
Console.WriteLine();
Console.WriteLine(message);
}
///
public void OnFileUnpacking(string relativePath)
{
if (this.ProgressBar == null)
this.ProgressBar = new ConsoleProgressBar(this.Context.Files.Count());
this.ProgressBar.Increment();
this.ProgressBar.Print(relativePath);
}
///
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);
}
///
public void OnEnded()
{
if (this.ShowPressAnyKeyToExit)
DefaultConsoleLogger.PressAnyKeyToExit();
}
/// Show a 'press any key to exit' message and wait for a key press.
public static void PressAnyKeyToExit()
{
Console.WriteLine();
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
/*********
** Private methods
*********/
/// Print a message to the console with a foreground color.
/// The message to print.
/// The foreground color to use.
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;
/// Provides platform-specific information.
internal class PlatformContext
{
/*********
** Accessors
*********/
/// The current platform.
public Platform Platform { get; } = EnvironmentUtility.DetectPlatform();
/*********
** Public methods
*********/
/// Get whether any of the listed platforms is the current one.
/// The platforms to match.
public bool Is(params Platform[] platforms)
{
return platforms.Contains(this.Platform);
}
/// Get the absolute paths to the game and content folders, if found.
/// The game path specified by the user, if any.
/// The absolute path to the game folder, if found.
/// The absolute path to the content folder, if found.
/// Returns whether both the game and content folders were found.
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
*********/
/// Get the possible game paths.
/// The game path specified by the user, if any.
private IEnumerable 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;
}
/// Get the absolute path to the game folder, if it's valid.
/// The path to check for a game install.
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;
}
/// Get the absolute path to the content folder for a given game, if found.
/// The absolute path to the game folder.
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;
}
/// Get the possible relative paths for the current platform.
private IEnumerable 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;
/// The context info for the current unpack run.
internal class UnpackContext : IUnpackContext
{
/*********
** Accessors
*********/
///
public string GamePath { get; set; }
///
public string ContentPath { get; set; }
///
public string ExportPath { get; set; }
///
public IEnumerable 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;
/// The base class for an asset writer.
internal abstract class BaseAssetWriter : IAssetWriter
{
/*********
** Private methods
*********/
/// The settings to use when serializing JSON.
private readonly Lazy JsonSettings;
/*********
** Public methods
*********/
/// Whether the writer can handle a given asset.
/// The asset value.
public abstract bool CanWrite(object asset);
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
public abstract bool TryWriteFile(object asset, string toPathWithoutExtension, string relativePath, Platform platform, out string error);
/*********
** Protected methods
*********/
/// Construct an instance.
/// Whether to ignore members marked which match the default value.
protected BaseAssetWriter(bool omitDefaultFields = false)
{
this.JsonSettings = new(() => BaseAssetWriter.GetJsonSerializerSettings(omitDefaultFields));
}
/// Get a text representation for the given asset.
/// The asset to serialize.
protected string FormatData(object asset)
{
return JsonConvert.SerializeObject(asset, this.JsonSettings.Value);
}
/// Get the recommended file extension for a data file formatted with .
protected string GetDataExtension()
{
return "json";
}
/// Get the serializer settings to apply when writing JSON.
/// Whether to ignore members marked which match the default value.
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;
/// Writes and assets to disk.
internal class DataWriter : BaseAssetWriter
{
/*********
** Public methods
*********/
///
public DataWriter(bool omitDefaultFields)
: base(omitDefaultFields) { }
/// Whether the writer can handle a given asset.
/// The asset value.
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;
}
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
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;
/// Writes assets to disk.
internal interface IAssetWriter
{
/*********
** Methods
*********/
/// Whether the writer can handle a given asset.
/// The asset value.
bool CanWrite(object asset);
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
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;
/// A Json.NET contract resolver which ignores properties marked with , or (optionally) marked with the default value.
internal class IgnoreDefaultOptionalPropertiesResolver : DefaultContractResolver
{
/*********
** Fields
*********/
/// Whether to ignore members marked which match the default value.
private readonly bool OmitDefaultValues;
/// The default values for fields and properties marked .
private readonly Dictionary> DefaultValues = new();
/*********
** Public methods
*********/
/// Construct an instance.
/// Whether to ignore members marked which match the default value.
public IgnoreDefaultOptionalPropertiesResolver(bool omitDefaultValues)
{
this.OmitDefaultValues = omitDefaultValues;
}
/*********
** Protected methods
*********/
///
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
// property marked ignore
if (member.GetCustomAttribute() != null)
property.ShouldSerialize = _ => false;
// property marked optional which matches default value
else if (this.OmitDefaultValues)
{
Dictionary? 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;
}
/// The default values for a type's fields and properties marked , if any.
/// The type whose fields and properties to get default values for.
/// Returns a dictionary of default values by member name if any were found, else null.
private Dictionary? 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 defaults))
return defaults;
// get members
MemberInfo[] optionalMembers =
(type.GetFields().OfType())
.Concat(type.GetProperties())
.Where(member => member.GetCustomAttribute()?.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();
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;
/// Writes assets to disk.
internal class MapWriter : BaseAssetWriter
{
/*********
** Fields
*********/
/// The actual size of a tile in the tilesheet.
const int TileSize = Game1.tileSize / Game1.pixelZoom;
/// The underlying map format handler.
private readonly TMXFormat Format;
/*********
** Public methods
*********/
/// Construct an instance.
public MapWriter()
{
// init TMX support
this.Format = new TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom);
}
/// Whether the writer can handle a given asset.
/// The asset value.
public override bool CanWrite(object asset)
{
return asset is Map;
}
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
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 tileSizes = new Dictionary();
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 imageSources = new Dictionary();
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", "\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
*********/
/// Get the image source for a map tilesheet without the game's automatic path changes.
/// The relative path to the map file within the content folder.
/// The tilesheet image source.
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;
/// Writes assets to disk.
internal class SpriteFontWriter : BaseAssetWriter
{
/*********
** Public methods
*********/
/// Whether the writer can handle a given asset.
/// The asset value.
public override bool CanWrite(object asset)
{
return asset is SpriteFont;
}
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
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
*********/
/// Draw a texture to a GPU render target.
/// The texture to draw.
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;
/// Writes assets to disk.
internal class TextureWriter : BaseAssetWriter
{
/*********
** Public methods
*********/
/// Whether the writer can handle a given asset.
/// The asset value.
public override bool CanWrite(object asset)
{
return asset is Texture2D;
}
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
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
*********/
/// Reverse premultiplication applied to an image asset by the XNA content pipeline.
/// The texture to adjust.
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;
/// Writes assets to disk.
internal class XmlSourceWriter : BaseAssetWriter
{
/*********
** Public methods
*********/
/// Whether the writer can handle a given asset.
/// The asset value.
public override bool CanWrite(object asset)
{
return asset is XmlSource;
}
/// Write an asset instance to disk.
/// The asset value.
/// The absolute path to the export file, without the file extension.
/// The relative path within the content folder.
/// The operating system running the unpacker.
/// An error phrase indicating why writing to disk failed (if applicable).
/// Returns whether writing to disk completed successfully.
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;
/// The console app entry point.
public static class Program
{
/*********
** Fields
*********/
/// The relative paths to search for unresolved assembly files.
private static readonly string[] RelativeAssemblyProbePaths =
{
"", // app directory
"smapi-internal"
};
/*********
** Public methods
*********/
/// The console app entry method.
/// The command-line arguments.
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();
}
}
/// Unpack all assets in the content folder and store them in the output folder.
/// The command-line arguments.
/// The game instance through which to unpack files, or null to launch a temporary internal instance.
/// The absolute path to the game folder, or null to auto-detect it.
/// Get a custom progress update logger, or null to use the default console logging. Receives the unpack context and default logger as arguments.
/// Whether the default logger should show a 'press any key to exit' prompt when it finishes.
public static void Run(string[] args, GameRunner game = null, string gamePath = null, Func 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