Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2020-12-26 11:22:45 -05:00
commit 48bb1581a6
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
17 changed files with 190 additions and 46 deletions

View File

@ -4,7 +4,7 @@
<!--set properties -->
<PropertyGroup>
<Version>3.8.0</Version>
<Version>3.8.1</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>

View File

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

View File

@ -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();
}
}
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -42,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
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);
@ -109,12 +109,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
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))

View File

@ -54,10 +54,10 @@ namespace StardewModdingAPI
** 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;

View File

@ -278,7 +278,7 @@ namespace StardewModdingAPI.Framework
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);

View File

@ -169,10 +169,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <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>
@ -315,7 +316,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <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()

View File

@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
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
@ -151,11 +151,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
*********/
/// <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);
@ -214,7 +215,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
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

View File

@ -58,7 +58,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <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();

View File

@ -211,7 +211,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
*********/
/// <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);
}

View File

@ -35,19 +35,17 @@ namespace StardewModdingAPI.Framework
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>
@ -58,7 +56,7 @@ namespace StardewModdingAPI.Framework
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
@ -111,21 +109,16 @@ namespace StardewModdingAPI.Framework
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.");

View File

@ -36,8 +36,16 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <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);
}
}

View File

@ -765,6 +765,9 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log(context);
// apply save fixes
this.ApplySaveFixes();
// raise events
this.OnLoadStageChanged(LoadStage.Ready);
events.SaveLoaded.RaiseEmpty();
@ -1054,6 +1057,40 @@ namespace StardewModdingAPI.Framework
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()
{

View File

@ -21,6 +21,9 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Tracks changes to the list of active mine locations.</summary>
private readonly ICollectionWatcher<MineShaft> MineLocationListWatcher;
/// <summary>Tracks changes to the list of active volcano locations.</summary>
private readonly ICollectionWatcher<GameLocation> VolcanoLocationListWatcher;
/// <summary>A lookup of the tracked locations.</summary>
private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>(new ObjectReferenceComparer<GameLocation>());
@ -53,10 +56,12 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Construct an instance.</summary>
/// <param name="locations">The game's list of locations.</param>
/// <param name="activeMineLocations">The game's list of active mine locations.</param>
public WorldLocationsTracker(ObservableCollection<GameLocation> locations, IList<MineShaft> activeMineLocations)
/// <param name="activeVolcanoLocations">The game's list of active volcano locations.</param>
public WorldLocationsTracker(ObservableCollection<GameLocation> locations, IList<MineShaft> activeMineLocations, IList<VolcanoDungeon> activeVolcanoLocations)
{
this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations);
this.VolcanoLocationListWatcher = WatcherFactory.ForReferenceList(activeVolcanoLocations);
}
/// <summary>Update the current value if needed.</summary>
@ -65,6 +70,7 @@ namespace StardewModdingAPI.Framework.StateTracking
// update watchers
this.LocationListWatcher.Update();
this.MineLocationListWatcher.Update();
this.VolcanoLocationListWatcher.Update();
foreach (LocationTracker watcher in this.Locations)
watcher.Update();
@ -79,6 +85,11 @@ namespace StardewModdingAPI.Framework.StateTracking
this.Remove(this.MineLocationListWatcher.Removed);
this.Add(this.MineLocationListWatcher.Added);
}
if (this.VolcanoLocationListWatcher.IsChanged)
{
this.Remove(this.VolcanoLocationListWatcher.Removed);
this.Add(this.VolcanoLocationListWatcher.Added);
}
// detect building changed
foreach (LocationTracker watcher in this.Locations.Where(p => p.BuildingsWatcher.IsChanged).ToArray())
@ -107,6 +118,7 @@ namespace StardewModdingAPI.Framework.StateTracking
this.Added.Clear();
this.LocationListWatcher.Reset();
this.MineLocationListWatcher.Reset();
this.VolcanoLocationListWatcher.Reset();
}
/// <summary>Set the current value as the baseline.</summary>
@ -243,6 +255,7 @@ namespace StardewModdingAPI.Framework.StateTracking
{
yield return this.LocationListWatcher;
yield return this.MineLocationListWatcher;
yield return this.VolcanoLocationListWatcher;
foreach (LocationTracker watcher in this.Locations)
yield return watcher;
}

View File

@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework
this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
this.LocationsWatcher = new WorldLocationsTracker(gameLocations, MineShaft.activeMines);
this.LocationsWatcher = new WorldLocationsTracker(gameLocations, MineShaft.activeMines, VolcanoDungeon.activeLevels);
this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode);
this.Watchers.AddRange(new IWatcher[]
{