Skip to content

Commit

Permalink
Merge branch 'develop' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
Pathoschild committed Apr 8, 2024
2 parents 06fdd81 + f560278 commit 7205035
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 11 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ You can ask for help in [#making-mods on the Stardew Valley Discord](https://sta
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.
Expand Down
17 changes: 13 additions & 4 deletions StardewXnbHack/Framework/Writers/BaseAssetWriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Force.DeepCloner;
using Microsoft.Xna.Framework.Content;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
Expand All @@ -13,7 +14,7 @@ internal abstract class BaseAssetWriter : IAssetWriter
** Private methods
*********/
/// <summary>The settings to use when serializing JSON.</summary>
private static readonly Lazy<JsonSerializerSettings> JsonSettings = new(BaseAssetWriter.GetJsonSerializerSettings);
private readonly Lazy<JsonSerializerSettings> JsonSettings;


/*********
Expand All @@ -36,11 +37,18 @@ internal abstract class BaseAssetWriter : IAssetWriter
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="omitDefaultFields">Whether to ignore members marked <see cref="ContentSerializerAttribute.Optional"/> which match the default value.</param>
protected BaseAssetWriter(bool omitDefaultFields = false)
{
this.JsonSettings = new(() => BaseAssetWriter.GetJsonSerializerSettings(omitDefaultFields));
}

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

/// <summary>Get the recommended file extension for a data file formatted with <see cref="FormatData"/>.</summary>
Expand All @@ -50,12 +58,13 @@ protected string GetDataExtension()
}

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

settings.ContractResolver = new IgnoreDefaultOptionalPropertiesResolver();
settings.ContractResolver = new IgnoreDefaultOptionalPropertiesResolver(omitDefaultFields);

return settings;
}
Expand Down
4 changes: 4 additions & 0 deletions StardewXnbHack/Framework/Writers/DataWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ internal class DataWriter : BaseAssetWriter
/*********
** Public methods
*********/
/// <inheritdoc />
public DataWriter(bool omitDefaultFields)
: base(omitDefaultFields) { }

/// <summary>Whether the writer can handle a given asset.</summary>
/// <param name="asset">The asset value.</param>
public override bool CanWrite(object asset)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework.Content;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Sickhead.Engine.Util;

namespace StardewXnbHack.Framework.Writers
{
/// <summary>A Json.NET contract resolver which ignores properties marked with <see cref="ContentSerializerIgnoreAttribute"/>.</summary>
/// <summary>A Json.NET contract resolver which ignores properties marked with <see cref="ContentSerializerIgnoreAttribute"/>, or (optionally) marked <see cref="ContentSerializerAttribute.Optional"/> with the default value.</summary>
internal class IgnoreDefaultOptionalPropertiesResolver : DefaultContractResolver
{
/*********
** Fields
*********/
/// <summary>Whether to ignore members marked <see cref="ContentSerializerAttribute.Optional"/> which match the default value.</summary>
private readonly bool OmitDefaultValues;

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


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


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

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

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

return property;
}

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

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

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

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

// get default values
defaults = new Dictionary<string, object>();
foreach (MemberInfo member in optionalMembers)
defaults[member.Name] = member.GetValue(defaultInstance);
return this.DefaultValues[type.FullName] = defaults;
}
}
}
17 changes: 12 additions & 5 deletions StardewXnbHack/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public static class Program
** Public methods
*********/
/// <summary>The console app entry method.</summary>
internal static void Main()
/// <param name="args">The command-line arguments.</param>
internal static void Main(string[] args)
{
// set window title
Console.Title = $"StardewXnbHack {Program.GetUnpackerVersion()}";
Expand All @@ -49,7 +50,7 @@ internal static void Main()
// launch app
try
{
Program.Run();
Program.Run(args);
}
catch (Exception ex)
{
Expand All @@ -73,23 +74,29 @@ internal static void Main()
}

/// <summary>Unpack all assets in the content folder and store them in the output folder.</summary>
/// <param name="args">The command-line arguments.</param>
/// <param name="game">The game instance through which to unpack files, or <c>null</c> to launch a temporary internal instance.</param>
/// <param name="gamePath">The absolute path to the game folder, or <c>null</c> to auto-detect it.</param>
/// <param name="getLogger">Get a custom progress update logger, or <c>null</c> to use the default console logging. Receives the unpack context and default logger as arguments.</param>
/// <param name="showPressAnyKeyToExit">Whether the default logger should show a 'press any key to exit' prompt when it finishes.</param>
public static void Run(GameRunner game = null, string gamePath = null, Func<IUnpackContext, IProgressLogger, IProgressLogger> getLogger = null, bool showPressAnyKeyToExit = true)
public static void Run(string[] args, GameRunner game = null, string gamePath = null, Func<IUnpackContext, IProgressLogger, IProgressLogger> getLogger = null, bool showPressAnyKeyToExit = true)
{
// init logging
UnpackContext context = new UnpackContext();
IProgressLogger logger = new DefaultConsoleLogger(context, showPressAnyKeyToExit);
logger.OnStepChanged(ProgressStep.Started, $"Running StardewXnbHack {Program.GetUnpackerVersion()}.");

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();
Expand All @@ -101,7 +108,7 @@ public static void Run(GameRunner game = null, string gamePath = null, Func<IUnp
new SpriteFontWriter(),
new TextureWriter(),
new XmlSourceWriter(),
new DataWriter() // check last due to more expensive CanWrite
new DataWriter(omitDefaultFields) // check last due to more expensive CanWrite
};

// get paths
Expand Down
5 changes: 4 additions & 1 deletion StardewXnbHack/StardewXnbHack.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/Pathoschild/StardewXnbHack</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Version>1.0.8</Version>
<Version>1.1.0</Version>

<!--build-->
<TargetFramework>net6.0</TargetFramework>
Expand All @@ -14,6 +14,9 @@
<ApplicationIcon>assets/icon.ico</ApplicationIcon>
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);IS_FOR_WINDOWS</DefineConstants>

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

<!--mod build package-->
<CopyModReferencesToBuildOutput>True</CopyModReferencesToBuildOutput>
<EnableGameDebugging>False</EnableGameDebugging>
Expand Down
7 changes: 7 additions & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
[← back to readme](README.md)

# Release notes
## 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.

Expand Down

0 comments on commit 7205035

Please sign in to comment.