Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard
2024-04-18 13:50:45 -04:00
14 changed files with 171 additions and 144 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
<Version>4.0.6</Version>
<Version>4.0.7</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
+17 -2
View File
@@ -1,10 +1,25 @@
← [README](README.md)
# Release notes
## 4.0.7
Released 18 April 2024 for Stardew Valley 1.6.4 or later.
* For players:
* Updated for Stardew Valley 1.6.4. **This drops compatibility with Stardew Valley 1.6.0–1.6.3.**
* The installer now lists detected game folders with an incompatible version to simplify troubleshooting.
* When the installer asks for a game folder path, entering an incorrect path to a file inside it will now still select the folder.
* Fixed installer not detecting 1.6 compatibility branch.
* For the web UI:
* Updated `manifest.json` JSON schema for the new `MinimumGameVersion` field (thanks to KhloeLeclair!).
* For external tool authors:
* In the SMAPI toolkit, added a new `GetGameFoldersIncludingInvalid()` method to get all detected game folders and their validity type.
## 4.0.6
Released 07 April 2024 for Stardew Valley 1.6.0 or later.
* For player:
* For players:
* The SMAPI log file now includes installed mod IDs, to help with troubleshooting (thanks to DecidedlyHuman!).
* For mod authors:
@@ -14,7 +29,7 @@ Released 07 April 2024 for Stardew Valley 1.6.0 or later.
Released 06 April 2024 for Stardew Valley 1.6.0 or later.
* For players:
* The installer now deletes obsolete files from very old SMAPI versions again.
* The installer now deletes obsolete files from very old SMAPI versions again. (This was removed in SMAPI 4.0, but many players still had very old versions.)
* The installer now deletes Error Handler automatically if it's at the default path.
* Fixed mods sometimes not applying logic inside new buildings.
* Minor optimizations.
@@ -48,13 +48,6 @@ namespace StardewModdingAPI.Installer.Framework
return new SemanticVersion(raw);
}
/// <summary>Get whether a folder seems to contain the game files.</summary>
/// <param name="dir">The folder to check.</param>
public bool LooksLikeGameFolder(DirectoryInfo dir)
{
return this.GameScanner.LooksLikeGameFolder(dir);
}
/// <summary>Get whether a folder seems to contain the game, and which version it contains if so.</summary>
/// <param name="dir">The folder to check.</param>
public GameFolderType GetGameFolderType(DirectoryInfo dir)
+66 -47
View File
@@ -695,48 +695,50 @@ namespace StardewModdingApi.Installer
return null;
}
switch (context.GetGameFolderType(dir))
GameFolderType type = context.GetGameFolderType(dir);
switch (type)
{
case GameFolderType.Valid:
return dir;
case GameFolderType.Legacy154OrEarlier:
this.PrintWarning($"{errorPrefix} that directory seems to have Stardew Valley 1.5.4 or earlier.");
this.PrintWarning("Please update your game to the latest version to use SMAPI.");
return null;
case GameFolderType.LegacyCompatibilityBranch:
this.PrintWarning($"{errorPrefix} that directory seems to have the Stardew Valley legacy 'compatibility' branch.");
this.PrintWarning("Unfortunately SMAPI is only compatible with the modern version of the game.");
this.PrintWarning("Please update your game to the main branch to use SMAPI.");
return null;
case GameFolderType.NoGameFound:
this.PrintWarning($"{errorPrefix} that directory doesn't contain a Stardew Valley executable.");
return null;
default:
this.PrintWarning($"{errorPrefix} that directory doesn't seem to contain a valid game install.");
foreach (string message in this.GetInvalidFolderWarning(type))
this.PrintWarning(message);
return null;
}
}
// get valid install paths & log invalid ones
List<DirectoryInfo> defaultPaths = new();
foreach ((DirectoryInfo dir, GameFolderType type) in this.DetectGameFolders(toolkit, context))
{
if (type is GameFolderType.Valid)
{
defaultPaths.Add(dir);
continue;
}
this.PrintDebug($"Ignored game folder: {dir.FullName}");
foreach (string message in this.GetInvalidFolderWarning(type))
this.PrintDebug(message);
this.PrintDebug("\n");
}
// let user choose detected path
DirectoryInfo[] defaultPaths = this.DetectGameFolders(toolkit, context).ToArray();
if (defaultPaths.Any())
{
this.PrintInfo("Where do you want to add or remove SMAPI?");
Console.WriteLine();
for (int i = 0; i < defaultPaths.Length; i++)
for (int i = 0; i < defaultPaths.Count; i++)
this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}");
this.PrintInfo($"[{defaultPaths.Length + 1}] Enter a custom game path.");
this.PrintInfo($"[{defaultPaths.Count + 1}] Enter a custom game path.");
Console.WriteLine();
string[] validOptions = Enumerable.Range(1, defaultPaths.Length + 1).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray();
string[] validOptions = Enumerable.Range(1, defaultPaths.Count + 1).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray();
string choice = this.InteractivelyChoose("Type the number next to your choice, then press enter.", validOptions);
int index = int.Parse(choice, CultureInfo.InvariantCulture) - 1;
if (index < defaultPaths.Length)
if (index < defaultPaths.Count)
return defaultPaths[index];
}
else
@@ -766,9 +768,9 @@ namespace StardewModdingApi.Installer
}
// get directory
if (File.Exists(path))
path = Path.GetDirectoryName(path)!;
DirectoryInfo directory = new(path);
if (!directory.Exists && (path.EndsWith(".dll") || path.EndsWith(".exe") || File.Exists(path)) && directory.Parent is { Exists: true })
directory = directory.Parent;
// validate path
if (!directory.Exists)
@@ -777,29 +779,16 @@ namespace StardewModdingApi.Installer
continue;
}
switch (context.GetGameFolderType(directory))
GameFolderType type = context.GetGameFolderType(directory);
switch (type)
{
case GameFolderType.Valid:
this.PrintInfo(" OK!");
return directory;
case GameFolderType.Legacy154OrEarlier:
this.PrintWarning("That directory seems to have Stardew Valley 1.5.4 or earlier.");
this.PrintWarning("Please update your game to the latest version to use SMAPI.");
continue;
case GameFolderType.LegacyCompatibilityBranch:
this.PrintWarning("That directory seems to have the Stardew Valley legacy 'compatibility' branch.");
this.PrintWarning("Unfortunately SMAPI is only compatible with the modern version of the game.");
this.PrintWarning("Please update your game to the main branch to use SMAPI.");
continue;
case GameFolderType.NoGameFound:
this.PrintWarning("That directory doesn't contain a Stardew Valley executable.");
continue;
default:
this.PrintWarning("That directory doesn't seem to contain a valid game install.");
foreach (string message in this.GetInvalidFolderWarning(type))
this.PrintWarning(message);
continue;
}
}
@@ -808,7 +797,7 @@ namespace StardewModdingApi.Installer
/// <summary>Get the possible game paths to update.</summary>
/// <param name="toolkit">The mod toolkit.</param>
/// <param name="context">The installer context.</param>
private IEnumerable<DirectoryInfo> DetectGameFolders(ModToolkit toolkit, InstallerContext context)
private IEnumerable<(DirectoryInfo, GameFolderType)> DetectGameFolders(ModToolkit toolkit, InstallerContext context)
{
HashSet<string> foundPaths = new HashSet<string>();
@@ -817,10 +806,10 @@ namespace StardewModdingApi.Installer
DirectoryInfo? curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
while (curPath?.Parent != null) // must be in a folder (not at the root)
{
if (context.LooksLikeGameFolder(curPath))
if (context.GetGameFolderType(curPath) == GameFolderType.Valid)
{
foundPaths.Add(curPath.FullName);
yield return curPath;
yield return (curPath, GameFolderType.Valid);
break;
}
@@ -829,10 +818,40 @@ namespace StardewModdingApi.Installer
}
// game paths detected by toolkit
foreach (DirectoryInfo dir in toolkit.GetGameFolders())
foreach ((DirectoryInfo, GameFolderType) pair in toolkit.GetGameFoldersIncludingInvalid())
{
if (foundPaths.Add(dir.FullName))
yield return dir;
if (foundPaths.Add(pair.Item1.FullName))
yield return pair;
}
}
private string[] GetInvalidFolderWarning(GameFolderType type)
{
switch (type)
{
case GameFolderType.Valid:
return new[] { "OK!" }; // should never happen
case GameFolderType.LegacyVersion:
return new[]
{
"That directory seems to have Stardew Valley 1.5.6 or earlier.",
"Please update your game to the latest version to use SMAPI."
};
case GameFolderType.LegacyCompatibilityBranch:
return new[]
{
"That directory seems to have the Stardew Valley legacy 'compatibility' branch.",
"Unfortunately SMAPI is only compatible with the modern version of the game.",
"Please update your game to the main branch to use SMAPI."
};
case GameFolderType.NoGameFound:
return new[] { "That directory doesn't contain a Stardew Valley executable." };
default:
return new[] { "That directory doesn't seem to contain a valid game install." };
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
"Version": "4.0.6",
"Version": "4.0.7",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
"MinimumApiVersion": "4.0.6"
"MinimumApiVersion": "4.0.7"
}
+2 -2
View File
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
"Version": "4.0.6",
"Version": "4.0.7",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
"MinimumApiVersion": "4.0.6"
"MinimumApiVersion": "4.0.7"
}
@@ -9,10 +9,10 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
/// <summary>The folder doesn't contain Stardew Valley.</summary>
NoGameFound,
/// <summary>The folder contains Stardew Valley 1.5.4 or earlier. This version uses XNA Framework and 32-bit .NET Framework 4.5.2 on Windows and Mono on Linux/macOS, and isn't compatible with current versions of SMAPI.</summary>
Legacy154OrEarlier,
/// <summary>The folder contains Stardew Valley 1.5.6 or earlier, which isn't compatible with current versions of SMAPI.</summary>
LegacyVersion,
/// <summary>The folder contains Stardew Valley from the game's legacy compatibility branch, which backports newer changes to the <see cref="Legacy154OrEarlier"/> format.</summary>
/// <summary>The folder contains Stardew Valley from the game's legacy compatibility branch, which backports newer changes to the <see cref="LegacyVersion"/> format.</summary>
LegacyCompatibilityBranch,
/// <summary>The folder seems to contain Stardew Valley files, but they failed to load for unknown reasons (e.g. corrupted executable).</summary>
@@ -40,6 +40,17 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
/// <summary>Find all valid Stardew Valley install folders.</summary>
/// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS.</remarks>
public IEnumerable<DirectoryInfo> Scan()
{
foreach ((DirectoryInfo folder, GameFolderType type) in this.ScanIncludingInvalid())
{
if (type is GameFolderType.Valid)
yield return folder;
}
}
/// <summary>Find all valid Stardew Valley install folders.</summary>
/// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS.</remarks>
public IEnumerable<(DirectoryInfo, GameFolderType)> ScanIncludingInvalid()
{
// get install paths
IEnumerable<string> paths = this
@@ -52,18 +63,11 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
foreach (string path in paths)
{
DirectoryInfo folder = new(path);
if (this.LooksLikeGameFolder(folder))
yield return folder;
if (folder.Exists)
yield return (folder, this.GetGameFolderType(folder));
}
}
/// <summary>Get whether a folder seems to contain the game.</summary>
/// <param name="dir">The folder to check.</param>
public bool LooksLikeGameFolder(DirectoryInfo dir)
{
return this.GetGameFolderType(dir) == GameFolderType.Valid;
}
/// <summary>Detect the validity of a game folder based on file structure heuristics.</summary>
/// <param name="dir">The folder to check.</param>
public GameFolderType GetGameFolderType(DirectoryInfo dir)
@@ -72,49 +76,42 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
if (!dir.Exists)
return GameFolderType.NoGameFound;
// invalid folder
if (!File.Exists(Path.Combine(dir.FullName, "Stardew Valley.dll")))
{
// get executable
FileInfo executable = new(Path.Combine(dir.FullName, "Stardew Valley.exe"));
if (!executable.Exists)
executable = new(Path.Combine(dir.FullName, "StardewValley.exe")); // pre-1.5.5 Linux/macOS executable
if (!executable.Exists)
return GameFolderType.NoGameFound;
// get assembly version
Version? version;
try
{
version = AssemblyName.GetAssemblyName(executable.FullName).Version;
if (version == null)
return GameFolderType.InvalidUnknown;
}
catch
{
return GameFolderType.InvalidUnknown; // executable exists, but it doesn't seem to be a valid assembly
}
// legacy version that's no longer supported
if (version.Major < 1 || version is { Major: 1, Minor: < 6 })
return GameFolderType.LegacyVersion;
// compatibility branch
if (!File.Exists(Path.Combine(dir.FullName, "MonoGame.Framework.dll")))
return GameFolderType.LegacyCompatibilityBranch;
return GameFolderType.InvalidUnknown;
}
// apparently valid
if (File.Exists(Path.Combine(dir.FullName, "Stardew Valley.dll")))
return GameFolderType.Valid;
// doesn't contain any version of Stardew Valley
FileInfo executable = new(Path.Combine(dir.FullName, "Stardew Valley.exe"));
if (!executable.Exists)
executable = new(Path.Combine(dir.FullName, "StardewValley.exe")); // pre-1.5.5 Linux/macOS executable
if (!executable.Exists)
return GameFolderType.NoGameFound;
// get assembly version
Version? version;
try
{
version = AssemblyName.GetAssemblyName(executable.FullName).Version;
if (version == null)
return GameFolderType.InvalidUnknown;
}
catch
{
// The executable exists but it doesn't seem to be a valid assembly. This would
// happen with Stardew Valley 1.5.5+, but that should have been flagged as a valid
// folder before this point.
return GameFolderType.InvalidUnknown;
}
// ignore Stardew Valley 1.5.5+ at this point
if (version.Major == 1 && version.Minor == 3 && version.Build == 37)
return GameFolderType.InvalidUnknown;
// incompatible version
if (version.Major == 1 && version.Minor < 4)
{
// Stardew Valley 1.5.4 and earlier have assembly versions <= 1.3.7853.31734
if (version.Minor < 3 || version.Build <= 7853)
return GameFolderType.Legacy154OrEarlier;
// Stardew Valley 1.5.5+ legacy compatibility branch
return GameFolderType.LegacyCompatibilityBranch;
}
return GameFolderType.InvalidUnknown;
return GameFolderType.Valid;
}
/*********
+7
View File
@@ -54,6 +54,13 @@ namespace StardewModdingAPI.Toolkit
return new GameScanner().Scan();
}
/// <summary>Find all default Stardew Valley install folders which exist, regardless of whether they're valid.</summary>
/// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data.</remarks>
public IEnumerable<(DirectoryInfo, GameFolderType)> GetGameFoldersIncludingInvalid()
{
return new GameScanner().ScanIncludingInvalid();
}
/// <summary>Extract mod metadata from the wiki compatibility list.</summary>
public async Task<WikiModList> GetWikiCompatibilityListAsync()
{
@@ -71,6 +71,11 @@
"title": "Minimum API version",
"description": "The minimum SMAPI version needed to use this mod. If a player tries to use the mod with an older SMAPI version, they'll see a friendly message saying they need to update SMAPI. This also serves as a proxy for the minimum game version, since SMAPI itself enforces a minimum game version.",
"$ref": "#/definitions/SemanticVersion"
},
"MinimumGameVersion": {
"title": "Minimum Game version",
"description": "The minimum Stardew Valley version needed to use this mod. If a player tries to use the mod with an older Stardew Valley version, they'll see a friendly message saying they need to update Stardew Valley.",
"$ref": "#/definitions/SemanticVersion"
},
"Dependencies": {
"title": "Mod dependencies",
+2 -2
View File
@@ -49,7 +49,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
internal static string RawApiVersion = "4.0.6";
internal static string RawApiVersion = "4.0.7";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -65,7 +65,7 @@ namespace StardewModdingAPI
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion);
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.6.0");
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.6.4");
/// <summary>The maximum supported version of Stardew Valley, if any.</summary>
public static ISemanticVersion? MaximumGameVersion { get; } = null;
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
@@ -116,11 +117,26 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6
Game1.drawObjectQuestionDialogue(dialogue, choices?.ToArray());
}
public static NPC getCharacterFromName(string name, bool mustBeVillager = true, bool useLocationsListOnly = false)
public new static NPC? getCharacterFromName(string name, bool mustBeVillager = true)
{
return Game1.getCharacterFromName(name, mustBeVillager);
}
public static T? getCharacterFromName<T>(string name, bool mustBeVillager = true) where T : NPC
{
return Game1.getCharacterFromName<T>(name, mustBeVillager);
}
public static T? GetCharacterOfType<T>() where T : NPC
{
return Game1.GetCharacterOfType<T>();
}
public static T? GetCharacterWhere<T>(Func<T, bool> check) where T : NPC
{
return Game1.GetCharacterWhere(check);
}
public static int getModeratelyDarkTime()
{
return Game1.getModeratelyDarkTime(Game1.currentLocation);
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework;
using StardewModdingAPI.Framework.ModLoading.Framework;
using StardewValley;
@@ -47,30 +46,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6
});
}
public new static void ForEachCharacter(Func<NPC, bool> action, bool includeEventActors = false)
{
// Stardew Valley 1.6.3 added an optional parameter to ForEachCharacter, so mods compiled in 1.6.3 won't
// run in 1.6.2 and earlier. We can't just call the method directly either since SMAPI itself is compiled
// for 1.6.3+ too.
MethodInfo method =
typeof(Utility).GetMethod(nameof(Utility.ForEachCharacter))
?? throw new InvalidOperationException($"Can't find method {nameof(Utility)}.{nameof(Utility.ForEachCharacter)} for compatibility rewrite.");
method.Invoke(null, new object[] { action });
}
public new static void ForEachVillager(Func<NPC, bool> action, bool includeEventActors = false)
{
// Stardew Valley 1.6.3 added an optional parameter to ForEachVillager, so mods compiled in 1.6.3 won't
// run in 1.6.2 and earlier. We can't just call the method directly either since SMAPI itself is compiled
// for 1.6.3+ too.
MethodInfo method =
typeof(Utility).GetMethod(nameof(Utility.ForEachVillager))
?? throw new InvalidOperationException($"Can't find method {nameof(Utility)}.{nameof(Utility.ForEachVillager)} for compatibility rewrite.");
method.Invoke(null, new object[] { action });
}
public new static DisposableList<NPC> getAllCharacters()
{
return new DisposableList<NPC>(Utility.getAllCharacters());
+1 -1
View File
@@ -6,7 +6,7 @@ namespace StardewModdingAPI
/// <summary>Erase the original content within the area before drawing the new content.</summary>
Replace,
/// <summary>Draw the new content over the original content, so the original content shows through any transparent pixels.</summary>
/// <summary>Draw the new content over the original content, so the original content shows through any transparent or semi-transparent pixels.</summary>
Overlay
}
}