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 Dec 26, 2020
2 parents 71284e7 + 5ac46b7 commit 48bb158
Show file tree
Hide file tree
Showing 17 changed files with 190 additions and 46 deletions.
2 changes: 1 addition & 1 deletion build/common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<!--set properties -->
<PropertyGroup>
<Version>3.8.0</Version>
<Version>3.8.1</Version>
<Product>SMAPI</Product>

<LangVersion>latest</LangVersion>
Expand Down
16 changes: 15 additions & 1 deletion docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
-->

## 3.8.1
Released 26 December 2020 for Stardew Valley 1.5.1 or later.

* For players:
* Fixed broken community center bundles for non-English saves created in Stardew Valley 1.5. Affected saves will be fixed automatically on load.

* For modders:
* World events are now raised for volcano dungeon levels.
* Added `apply_save_fix` command to reapply a save migration in exceptional cases. This should be used very carefully. Type `help apply_save_fix` for details.
* **Deprecation notice:** the `Helper.ConsoleCommands.Trigger` method is now deprecated and should no longer be used. See [integration APIs](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations) for better mod integration options. It will eventually be removed in SMAPI 4.0.

For the web UI:
* Fixed edge cases in SMAPI log parsing.

## 3.8
Released 21 December 2020 for Stardew Valley 1.5 or later.
Released 21 December 2020 for Stardew Valley 1.5 or later. See [release highlights](https://www.patreon.com/posts/45294737).

* For players:
* Updated for Stardew Valley 1.5, including split-screen support.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using StardewValley;

namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
{
/// <summary>A command which runs one of the game's save migrations.</summary>
internal class ApplySaveFixCommand : TrainerCommand
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public ApplySaveFixCommand()
: base("apply_save_fix", "Apply one of the game's save migrations to the currently loaded save. WARNING: This may corrupt or make permanent changes to your save. DO NOT USE THIS unless you're absolutely sure.\n\nUsage: apply_save_fix list\nList all valid save IDs.\n\nUsage: apply_save_fix <fix ID>\nApply the named save fix.") { }

/// <summary>Handle the command.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="command">The command name.</param>
/// <param name="args">The command arguments.</param>
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// get fix ID
if (!args.TryGet(0, "fix_id", out string rawFixId, required: false))
{
monitor.Log("Invalid usage. Type 'help apply_save_fix' for details.", LogLevel.Error);
return;
}
rawFixId = rawFixId.Trim();


// list mode
if (rawFixId == "list")
{
monitor.Log("Valid save fix IDs:\n - " + string.Join("\n - ", this.GetSaveIds()), LogLevel.Info);
return;
}

// validate fix ID
if (!Enum.TryParse(rawFixId, ignoreCase: true, out SaveGame.SaveFixes fixId))
{
monitor.Log($"Invalid save ID '{rawFixId}'. Type 'help apply_save_fix' for details.", LogLevel.Error);
return;
}

// apply
monitor.Log("THIS MAY CAUSE PERMANENT CHANGES TO YOUR SAVE FILE. If you're not sure, exit your game without saving to avoid issues.", LogLevel.Warn);
monitor.Log($"Trying to apply save fix ID: '{fixId}'.", LogLevel.Warn);
try
{
Game1.applySaveFix(fixId);
monitor.Log("Save fix applied.", LogLevel.Info);
}
catch (Exception ex)
{
monitor.Log("Applying save fix failed. The save may be in an invalid state; you should exit your game now without saving to avoid issues.", LogLevel.Error);
monitor.Log($"Technical details: {ex}", LogLevel.Debug);
}
}


/*********
** Private methods
*********/
/// <summary>Get the valid save fix IDs.</summary>
private IEnumerable<string> GetSaveIds()
{
foreach (SaveGame.SaveFixes id in Enum.GetValues(typeof(SaveGame.SaveFixes)))
{
if (id == SaveGame.SaveFixes.MAX)
continue;

yield return id.ToString();
}
}
}
}
4 changes: 2 additions & 2 deletions src/SMAPI.Mods.ConsoleCommands/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
"Version": "3.8.0",
"Version": "3.8.1",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
"MinimumApiVersion": "3.8.0"
"MinimumApiVersion": "3.8.1"
}
4 changes: 2 additions & 2 deletions src/SMAPI.Mods.SaveBackup/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
"Version": "3.8.0",
"Version": "3.8.1",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
"MinimumApiVersion": "3.8.0"
"MinimumApiVersion": "3.8.1"
}
11 changes: 4 additions & 7 deletions src/SMAPI.Web/Framework/LogParsing/LogParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class LogParser
private readonly Regex ModUpdateListStartPattern = new Regex(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>A regex pattern matching an entry in SMAPI's mod update list.</summary>
private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>A regex pattern matching SMAPI's update line.</summary>
private readonly Regex SmapiUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Expand Down Expand Up @@ -109,12 +109,9 @@ public ParsedLog Parse(string logText)
if (message.Mod == "SMAPI")
{
// update flags
if (inModList && !this.ModListEntryPattern.IsMatch(message.Text))
inModList = false;
if (inContentPackList && !this.ContentPackListEntryPattern.IsMatch(message.Text))
inContentPackList = false;
if (inModUpdateList && !this.ModUpdateListEntryPattern.IsMatch(message.Text))
inModUpdateList = false;
inModList = inModList && message.Level == LogLevel.Info && this.ModListEntryPattern.IsMatch(message.Text);
inContentPackList = inContentPackList && message.Level == LogLevel.Info && this.ContentPackListEntryPattern.IsMatch(message.Text);
inModUpdateList = inModUpdateList && message.Level == LogLevel.Alert && this.ModUpdateListEntryPattern.IsMatch(message.Text);

// mod list
if (!inModList && message.Level == LogLevel.Info && this.ModListStartPattern.IsMatch(message.Text))
Expand Down
4 changes: 2 additions & 2 deletions src/SMAPI/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ public static class Constants
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.0");
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.1");

/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.0");
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.1");

/// <summary>The maximum supported version of Stardew Valley.</summary>
public static ISemanticVersion MaximumGameVersion { get; } = null;
Expand Down
2 changes: 1 addition & 1 deletion src/SMAPI/Framework/ContentCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public IEnumerable<object> GetLoadedValues(string assetName)
return this.ContentManagerLock.InReadLock(() =>
{
List<object> values = new List<object>();
foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName)))
foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName, p.Language)))
{
object value = content.Load<object>(assetName, this.Language, useCache: true);
values.Add(value);
Expand Down
8 changes: 5 additions & 3 deletions src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,11 @@ public string GetLocale(LanguageCode language)

/// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
public bool IsLoaded(string assetName)
/// <param name="language">The language.</param>
public bool IsLoaded(string assetName, LanguageCode language)
{
assetName = this.Cache.NormalizeKey(assetName);
return this.IsNormalizedKeyLoaded(assetName);
return this.IsNormalizedKeyLoaded(assetName, language);
}

/// <summary>Get the cached asset keys.</summary>
Expand Down Expand Up @@ -315,7 +316,8 @@ protected void ParseCacheKey(string cacheKey, out string assetName, out string l

/// <summary>Get whether an asset has already been loaded.</summary>
/// <param name="normalizedAssetName">The normalized asset name.</param>
protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName);
/// <param name="language">The language to check.</param>
protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language);

/// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
private IDictionary<LanguageCode, string> GetKeyLocales()
Expand Down
9 changes: 5 additions & 4 deletions src/SMAPI/Framework/ContentManagers/GameContentManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public override T Load<T>(string assetName, LocalizedContentManager.LanguageCode
return this.Load<T>(newAssetName, newLanguage, useCache);

// get from cache
if (useCache && this.IsLoaded(assetName))
if (useCache && this.IsLoaded(assetName, language))
return this.RawLoad<T>(assetName, language, useCache: true);

// get managed asset
Expand Down Expand Up @@ -151,11 +151,12 @@ public override LocalizedContentManager CreateTemporary()
*********/
/// <summary>Get whether an asset has already been loaded.</summary>
/// <param name="normalizedAssetName">The normalized asset name.</param>
protected override bool IsNormalizedKeyLoaded(string normalizedAssetName)
/// <param name="language">The language to check.</param>
protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language)
{
string cachedKey = null;
bool localized =
this.Language != LocalizedContentManager.LanguageCode.en
language != LocalizedContentManager.LanguageCode.en
&& !this.Coordinator.IsManagedAssetKey(normalizedAssetName)
&& this.LocalizedAssetNames.TryGetValue(normalizedAssetName, out cachedKey);

Expand Down Expand Up @@ -214,7 +215,7 @@ protected override void TrackAsset<T>(string assetName, T value, LanguageCode la
private T RawLoad<T>(string assetName, LanguageCode language, bool useCache)
{
// use cached key
if (this.LocalizedAssetNames.TryGetValue(assetName, out string cachedKey))
if (language == this.Language && this.LocalizedAssetNames.TryGetValue(assetName, out string cachedKey))
return base.RawLoad<T>(cachedKey, useCache);

// try translated key
Expand Down
3 changes: 2 additions & 1 deletion src/SMAPI/Framework/ContentManagers/IContentManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ internal interface IContentManager : IDisposable

/// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
bool IsLoaded(string assetName);
/// <param name="language">The language.</param>
bool IsLoaded(string assetName, LocalizedContentManager.LanguageCode language);

/// <summary>Get the cached asset keys.</summary>
IEnumerable<string> GetAssetKeys();
Expand Down
3 changes: 2 additions & 1 deletion src/SMAPI/Framework/ContentManagers/ModContentManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ public string GetInternalAssetKey(string key)
*********/
/// <summary>Get whether an asset has already been loaded.</summary>
/// <param name="normalizedAssetName">The normalized asset name.</param>
protected override bool IsNormalizedKeyLoaded(string normalizedAssetName)
/// <param name="language">The language to check.</param>
protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language)
{
return this.Cache.ContainsKey(normalizedAssetName);
}
Expand Down
31 changes: 12 additions & 19 deletions src/SMAPI/Framework/DeprecationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,17 @@ public DeprecationManager(IMonitor monitor, ModRegistry modRegistry)
this.ModRegistry = modRegistry;
}

/// <summary>Log a deprecation warning for the old-style events.</summary>
public void WarnForOldEvents()
/// <summary>Get the source name for a mod from its unique ID.</summary>
public string GetSourceNameFromStack()
{
this.Warn("legacy events", "2.9", DeprecationLevel.PendingRemoval);
return this.ModRegistry.GetFromStack()?.DisplayName;
}

/// <summary>Log a deprecation warning.</summary>
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <param name="severity">How deprecated the code is.</param>
public void Warn(string nounPhrase, string version, DeprecationLevel severity)
/// <summary>Get the source name for a mod from its unique ID.</summary>
/// <param name="modId">The mod's unique ID.</param>
public string GetSourceName(string modId)
{
this.Warn(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version, severity);
return this.ModRegistry.Get(modId)?.DisplayName;
}

/// <summary>Log a deprecation warning.</summary>
Expand All @@ -58,7 +56,7 @@ public void Warn(string nounPhrase, string version, DeprecationLevel severity)
public void Warn(string source, string nounPhrase, string version, DeprecationLevel severity)
{
// ignore if already warned
if (!this.MarkWarned(source ?? "<unknown>", nounPhrase, version))
if (!this.MarkWarned(source ?? this.GetSourceNameFromStack() ?? "<unknown>", nounPhrase, version))
return;

// queue warning
Expand Down Expand Up @@ -111,21 +109,16 @@ public void PrintQueued()
this.QueuedWarnings.Clear();
}

/// <summary>Mark a deprecation warning as already logged.</summary>
/// <param name="nounPhrase">A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method").</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
public bool MarkWarned(string nounPhrase, string version)
{
return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version);
}

/*********
** Private methods
*********/
/// <summary>Mark a deprecation warning as already logged.</summary>
/// <param name="source">The friendly name of the assembly which used the deprecated code.</param>
/// <param name="nounPhrase">A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method").</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
public bool MarkWarned(string source, string nounPhrase, string version)
private bool MarkWarned(string source, string nounPhrase, string version)
{
if (string.IsNullOrWhiteSpace(source))
throw new InvalidOperationException("The deprecation source cannot be empty.");
Expand Down
8 changes: 8 additions & 0 deletions src/SMAPI/Framework/ModHelpers/CommandHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ public ICommandHelper Add(string name, string documentation, Action<string, stri
}

/// <inheritdoc />
[Obsolete]
public bool Trigger(string name, string[] arguments)
{
SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetSourceName(this.ModID),
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
version: "3.8.1",
severity: DeprecationLevel.Notice
);

return this.CommandManager.Trigger(name, arguments);
}
}
Expand Down
37 changes: 37 additions & 0 deletions src/SMAPI/Framework/SCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,9 @@ private void OnPlayerInstanceUpdating(SGame instance, GameTime gameTime, Action

this.Monitor.Log(context);

// apply save fixes
this.ApplySaveFixes();

// raise events
this.OnLoadStageChanged(LoadStage.Ready);
events.SaveLoaded.RaiseEmpty();
Expand Down Expand Up @@ -1054,6 +1057,40 @@ internal void OnLoadStageChanged(LoadStage newStage)
this.EventManager.ReturnedToTitle.RaiseEmpty();
}

/// <summary>Apply fixes to the save after it's loaded.</summary>
private void ApplySaveFixes()
{
// get last SMAPI version used with this save
const string migrationKey = "Pathoschild.SMAPI/api-version";
if (!Game1.CustomData.TryGetValue(migrationKey, out string rawVersion) || !SemanticVersion.TryParse(rawVersion, out ISemanticVersion lastVersion))
lastVersion = new SemanticVersion(3, 8, 0);

// fix bundle corruption in SMAPI 3.8.0
// For non-English players who created a new save in SMAPI 3.8.0, bundle data was
// incorrectly translated which caused the code to crash whenever the game tried to
// read it.
if (lastVersion.IsOlderThan(new SemanticVersion(3, 8, 1)) && Game1.netWorldState?.Value?.BundleData != null)
{
var oldData = new Dictionary<string, string>(Game1.netWorldState.Value.BundleData);

try
{
Game1.applySaveFix(SaveGame.SaveFixes.FixBotchedBundleData);
bool changed = Game1.netWorldState.Value.BundleData.Any(p => oldData.TryGetValue(p.Key, out string oldValue) && oldValue != p.Value);
if (changed)
this.Monitor.Log("Found broken community center bundles and fixed them automatically.", LogLevel.Info);
}
catch (Exception ex)
{
this.Monitor.Log("Failed to verify community center data.", LogLevel.Error); // should never happen
this.Monitor.Log($"Technical details: {ex}");
}
}

// update last run
Game1.CustomData[migrationKey] = Constants.ApiVersion.ToString();
}

/// <summary>Raised after custom content is removed from the save data to avoid a crash.</summary>
internal void OnSaveContentRemoved()
{
Expand Down
Loading

0 comments on commit 48bb158

Please sign in to comment.