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
✓ 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(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 *********/ /// Assert that the current platform matches the one StardewXnbHack was compiled for. 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 } /// Method called when assembly resolution fails, which may return a manually resolved assembly. /// The event sender. /// The event arguments. 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; } /// Create a temporary game instance for the unpacker. /// The platform-specific context. /// The absolute path to the content folder to import. 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(); } } /// Get a human-readable representation of elapsed time. /// The elapsed time. private static string GetHumanTime(TimeSpan time) { List parts = new List(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); } /// Get the version string for StardewXnbHack. private static string GetUnpackerVersion() { var attribute = typeof(Program).Assembly.GetCustomAttribute(); return attribute?.InformationalVersion ?? ""; } /// Get the version string for Stardew Valley. 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; /// Logs updates while the unpacker is running. public interface IProgressLogger { /********* ** Methods *********/ /// Log an error which halts the unpack process (e.g. game folder missing). /// The error message indicating why the unpacker halted. void OnFatalError(string error); /// Log a step transition in the overall unpack process. /// The new step. /// The default log message for the step transition. void OnStepChanged(ProgressStep step, string message); /// Log a file being unpacked. /// The relative path of the file within the content folder. void OnFileUnpacking(string relativePath); /// Log a file which couldn't be unpacked. /// The relative path of the file within the content folder. /// An error code indicating why unpacking failed. /// An error message indicating why unpacking failed. void OnFileUnpackFailed(string relativePath, UnpackFailedReason errorCode, string errorMessage); /// The unpacker is done and exiting. void OnEnded(); } ================================================ FILE: StardewXnbHack/ProgressHandling/IUnpackContext.cs ================================================ using System.Collections.Generic; using System.IO; namespace StardewXnbHack.ProgressHandling; /// The context info for the current unpack run. public interface IUnpackContext { /// The absolute path to the game folder. string GamePath { get; } /// The absolute path to the content folder. string ContentPath { get; } /// The absolute path to the folder containing exported assets. string ExportPath { get; } /// The files found to unpack. IEnumerable Files { get; } } ================================================ FILE: StardewXnbHack/ProgressHandling/ProgressStep.cs ================================================ namespace StardewXnbHack.ProgressHandling; /// A step in the overall unpack process. public enum ProgressStep { /// The unpacker has started. Started, /// The game folder was located, but unpacking hasn't started yet. GameFound, /// The temporary game instance is being loaded. LoadingGameInstance, /// The files are being unpacked. UnpackingFiles, /// The overall unpack process completed successfully. Done } ================================================ FILE: StardewXnbHack/ProgressHandling/UnpackFailedReason.cs ================================================ namespace StardewXnbHack.ProgressHandling; /// An error code indicating why unpacking failed for a file. public enum UnpackFailedReason { /// An error occurred trying to read the raw XNB asset. ReadError, /// The XNB asset was successfully loaded, but its file format can't be unpacked. UnsupportedFileType, /// The XNB asset was successfully loaded, but an error occurred trying to save the unpacked file. WriteError, /// An unhandled error occurred. UnknownError } ================================================ FILE: StardewXnbHack/Properties/PublishProfiles/FolderProfile.pubxml ================================================ Release x64 bin\x64\Release\net6.0\publish\win-x64\ FileSystem net6.0 win-x64 true True False False ================================================ FILE: StardewXnbHack/StardewXnbHack.csproj ================================================  Pathoschild MIT https://github.com/Pathoschild/StardewXnbHack git 1.1.2 net6.0 x64 Exe assets/icon.ico $(DefineConstants);IS_FOR_WINDOWS false True False False False ================================================ 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 ================================================  DO_NOT_SHOW <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> True ================================================ 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!).