diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-05-03 18:11:31 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-05-03 18:11:31 -0400 |
commit | 5d3d919d490fd414fe9647e566e92c71d7f64509 (patch) | |
tree | e1eab3352287ef04b5de4cdc28b550e255d58c3f /src | |
parent | c48f6d78cc412c5f2e40a8b460b7b3c1c993c51a (diff) | |
parent | 3447e2f575c2c83af729777e4d37e93f4c2a6467 (diff) | |
download | SMAPI-5d3d919d490fd414fe9647e566e92c71d7f64509.tar.gz SMAPI-5d3d919d490fd414fe9647e566e92c71d7f64509.tar.bz2 SMAPI-5d3d919d490fd414fe9647e566e92c71d7f64509.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src')
56 files changed, 670 insertions, 330 deletions
diff --git a/src/SMAPI.Installer/Framework/InstallerContext.cs b/src/SMAPI.Installer/Framework/InstallerContext.cs index 7531eaee..88e57760 100644 --- a/src/SMAPI.Installer/Framework/InstallerContext.cs +++ b/src/SMAPI.Installer/Framework/InstallerContext.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI.Installer.Framework /// <summary>Whether the installer is running on Windows.</summary> public bool IsWindows => this.Platform == Platform.Windows; - /// <summary>Whether the installer is running on a Unix OS (including Linux or MacOS).</summary> + /// <summary>Whether the installer is running on a Unix OS (including Linux or macOS).</summary> public bool IsUnix => !this.IsWindows; diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs index ac6c3a8e..6ba5fa5f 100644 --- a/src/SMAPI.Installer/Framework/InstallerPaths.cs +++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs @@ -44,16 +44,16 @@ namespace StardewModdingAPI.Installer.Framework /// <summary>The full path to the user's config overrides file.</summary> public string ApiUserConfigPath { get; } - /// <summary>The full path to the installed SMAPI executable file.</summary> - public string ExecutablePath { get; } + /// <summary>The full path to the installed game executable file.</summary> + public string ExecutablePath { get; private set; } - /// <summary>The full path to the vanilla game launcher on Linux/Mac.</summary> + /// <summary>The full path to the vanilla game launcher on Linux/macOS.</summary> public string UnixLauncherPath { get; } - /// <summary>The full path to the installed SMAPI launcher on Linux/Mac before it's renamed.</summary> + /// <summary>The full path to the installed SMAPI launcher on Linux/macOS before it's renamed.</summary> public string UnixSmapiLauncherPath { get; } - /// <summary>The full path to the vanilla game launcher on Linux/Mac after SMAPI is installed.</summary> + /// <summary>The full path to the vanilla game launcher on Linux/macOS after SMAPI is installed.</summary> public string UnixBackupLauncherPath { get; } @@ -79,5 +79,12 @@ namespace StardewModdingAPI.Installer.Framework this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.json"); this.ApiUserConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.user.json"); } + + /// <summary>Override the filename for the <see cref="ExecutablePath"/>.</summary> + /// <param name="filename">the file name.</param> + public void SetExecutableFileName(string filename) + { + this.ExecutablePath = Path.Combine(this.GamePath, filename); + } } } diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 3d673719..83ecd257 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -10,9 +11,6 @@ using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Utilities; -#if !SMAPI_FOR_WINDOWS -using System.Diagnostics; -#endif namespace StardewModdingApi.Installer { @@ -40,11 +38,11 @@ namespace StardewModdingApi.Installer string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); // current files - yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only - yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only + yield return GetInstallPath("libgdiplus.dylib"); // Linux/macOS only + yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.exe.config"); - yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only + yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("smapi-internal"); @@ -106,13 +104,13 @@ namespace StardewModdingApi.Installer /// 2. Ask the user whether to install or uninstall. /// /// Uninstall logic: - /// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup. + /// 1. On Linux/macOS: if a backup of the launcher exists, delete the launcher and restore the backup. /// 2. Delete all files and folders in the game directory matching one of the values returned by <see cref="GetUninstallPaths"/>. /// /// Install flow: /// 1. Run the uninstall flow. /// 2. Copy the SMAPI files from package/Windows or package/Mono into the game directory. - /// 3. On Linux/Mac: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) + /// 3. On Linux/macOS: back up the game launcher and replace it with the SMAPI launcher. (This isn't possible on Windows, so the user needs to configure it manually.) /// 4. Create the 'Mods' directory. /// 5. Copy the bundled mods into the 'Mods' directory (deleting any existing versions). /// 6. Move any mods from app data into game's mods directory. @@ -143,7 +141,7 @@ namespace StardewModdingApi.Installer #else if (context.IsWindows) { - this.PrintError($"This is the installer for Linux/Mac. Run the 'install on Windows.exe' file instead."); + this.PrintError($"This is the installer for Linux/macOS. Run the 'install on Windows.exe' file instead."); Console.ReadLine(); return; } @@ -196,7 +194,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 2: choose a theme (can't auto-detect on Linux/Mac) + ** Step 2: choose a theme (can't auto-detect on Linux/macOS) *********/ MonitorColorScheme scheme = MonitorColorScheme.AutoDetect; if (context.IsUnix) @@ -260,7 +258,6 @@ namespace StardewModdingApi.Installer ** collect details ****/ // get game path - this.PrintInfo("Where is your game folder?"); DirectoryInfo installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg); if (installDir == null) { @@ -277,7 +274,20 @@ namespace StardewModdingApi.Installer /********* - ** Step 4: validate assumptions + ** Step 4: detect 64-bit Stardew Valley + *********/ + // detect 64-bit mode + bool isWindows64Bit = false; + if (context.Platform == Platform.Windows) + { + FileInfo linuxExecutable = new FileInfo(Path.Combine(paths.GamePath, "StardewValley.exe")); + isWindows64Bit = linuxExecutable.Exists && this.Is64Bit(linuxExecutable.FullName); + if (isWindows64Bit) + paths.SetExecutableFileName(linuxExecutable.Name); + } + + /********* + ** Step 5: validate assumptions *********/ // executable exists if (!File.Exists(paths.ExecutablePath)) @@ -300,7 +310,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 5: ask what to do + ** Step 6: ask what to do *********/ ScriptAction action; { @@ -308,7 +318,7 @@ namespace StardewModdingApi.Installer ** print header ****/ this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just one question first."); - this.PrintDebug($"Game path: {paths.GamePath}"); + this.PrintDebug($"Game path: {paths.GamePath}{(context.IsWindows ? $" [{(isWindows64Bit ? "64-bit" : "32-bit")}]" : "")}"); this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); @@ -346,14 +356,14 @@ namespace StardewModdingApi.Installer /********* - ** Step 6: apply + ** Step 7: apply *********/ { /**** ** print header ****/ this.PrintInfo($"That's all I need! I'll {action.ToString().ToLower()} SMAPI now."); - this.PrintDebug($"Game path: {paths.GamePath}"); + this.PrintDebug($"Game path: {paths.GamePath}{(context.IsWindows ? $" [{(isWindows64Bit ? "64-bit" : "32-bit")}]" : "")}"); this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); @@ -414,6 +424,27 @@ namespace StardewModdingApi.Installer this.RecursiveCopy(sourceEntry, paths.GameDir); } + if (isWindows64Bit) + { + this.PrintDebug("Making SMAPI 64-bit..."); + FileInfo x64Executable = new FileInfo(Path.Combine(paths.BundleDir.FullName, "StardewModdingAPI-x64.exe")); + if (x64Executable.Exists) + { + string targetName = "StardewModdingAPI.exe"; + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, targetName)); + this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, x64Executable.Name)); + + this.RecursiveCopy(x64Executable, paths.GameDir); + File.Move(Path.Combine(paths.GamePath, x64Executable.Name), Path.Combine(paths.GamePath, targetName)); + } + else + { + this.PrintError($"Oops! Could not find the required '{x64Executable.Name}' installer file. SMAPI was unable to install correctly."); + Console.ReadLine(); + return; + } + } + // replace mod launcher (if possible) if (context.IsUnix) { @@ -433,8 +464,6 @@ namespace StardewModdingApi.Installer // mark file executable // (MSBuild doesn't keep permission flags for files zipped in a build task.) - // (Note: exclude from Windows build because antivirus apps can flag the process start code as suspicious.) -#if !SMAPI_FOR_WINDOWS new Process { StartInfo = new ProcessStartInfo @@ -444,7 +473,6 @@ namespace StardewModdingApi.Installer CreateNoWindow = true } }.Start(); -#endif } // create mods directory (if needed) @@ -540,6 +568,13 @@ namespace StardewModdingApi.Installer /********* ** Private methods *********/ + /// <summary>Get whether an executable is 64-bit.</summary> + /// <param name="executablePath">The absolute path to the executable file.</param> + private bool Is64Bit(string executablePath) + { + return AssemblyName.GetAssemblyName(executablePath).ProcessorArchitecture != ProcessorArchitecture.X86; + } + /// <summary>Get the display text for a color scheme.</summary> /// <param name="scheme">The color scheme.</param> private string GetDisplayText(MonitorColorScheme scheme) @@ -676,56 +711,44 @@ namespace StardewModdingApi.Installer return dir; } - // use game folder which contains the installer, if any - { - 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)) - return curPath; - - curPath = curPath.Parent; - } - } - - // use an installed path - DirectoryInfo[] defaultPaths = toolkit.GetGameFolders().ToArray(); + // let user choose detected path + DirectoryInfo[] defaultPaths = this.DetectGameFolders(toolkit, context).ToArray(); if (defaultPaths.Any()) { - // only one path - if (defaultPaths.Length == 1) - return defaultPaths.First(); - - // let user choose path + this.PrintInfo("Where do you want to add or remove SMAPI?"); Console.WriteLine(); - this.PrintInfo("Found multiple copies of the game:"); for (int i = 0; i < defaultPaths.Length; i++) this.PrintInfo($"[{i + 1}] {defaultPaths[i].FullName}"); + this.PrintInfo($"[{defaultPaths.Length + 1}] Enter a custom game path."); Console.WriteLine(); - string[] validOptions = Enumerable.Range(1, defaultPaths.Length).Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - string choice = this.InteractivelyChoose("Where do you want to add/remove SMAPI? Type the number next to your choice, then press enter.", validOptions); + string[] validOptions = Enumerable.Range(1, defaultPaths.Length + 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; - return defaultPaths[index]; + + if (index < defaultPaths.Length) + return defaultPaths[index]; } + else + this.PrintInfo("Oops, couldn't find the game automatically."); - // ask user - this.PrintInfo("Oops, couldn't find the game automatically."); + // let user enter manual path while (true) { // get path from user + Console.WriteLine(); this.PrintInfo($"Type the file path to the game directory (the one containing '{context.ExecutableName}'), then press enter."); string path = Console.ReadLine()?.Trim(); if (string.IsNullOrWhiteSpace(path)) { - this.PrintInfo(" You must specify a directory path to continue."); + this.PrintWarning("You must specify a directory path to continue."); continue; } // normalize path path = context.IsWindows ? path.Replace("\"", "") // in Windows, quotes are used to escape spaces and aren't part of the file path - : path.Replace("\\ ", " "); // in Linux/Mac, spaces in paths may be escaped if copied from the command line + : path.Replace("\\ ", " "); // in Linux/macOS, spaces in paths may be escaped if copied from the command line if (path.StartsWith("~/")) { string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE"); @@ -740,12 +763,12 @@ namespace StardewModdingApi.Installer // validate path if (!directory.Exists) { - this.PrintInfo(" That directory doesn't seem to exist."); + this.PrintWarning("That directory doesn't seem to exist."); continue; } if (!context.LooksLikeGameFolder(directory)) { - this.PrintInfo(" That directory doesn't contain a Stardew Valley executable."); + this.PrintWarning("That directory doesn't contain a Stardew Valley executable."); continue; } @@ -755,6 +778,37 @@ 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) + { + HashSet<string> foundPaths = new HashSet<string>(); + + // game folder which contains the installer, if any + { + 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)) + { + foundPaths.Add(curPath.FullName); + yield return curPath; + break; + } + + curPath = curPath.Parent; + } + } + + // game paths detected by toolkit + foreach (DirectoryInfo dir in toolkit.GetGameFolders()) + { + if (foundPaths.Add(dir.FullName)) + yield return dir; + } + } + /// <summary>Interactively move mods out of the appdata directory.</summary> /// <param name="properModsDir">The directory which should contain all mods.</param> /// <param name="packagedModsDir">The installer directory containing packaged mods.</param> @@ -845,7 +899,7 @@ namespace StardewModdingApi.Installer switch (entry.Name) { case "mcs": - return false; // ignore Mac symlink + return false; // ignore macOS symlink case "Mods": return false; // Mods folder handled separately default: diff --git a/src/SMAPI.Installer/assets/README.txt b/src/SMAPI.Installer/assets/README.txt index 0da49a46..c3a7e271 100644 --- a/src/SMAPI.Installer/assets/README.txt +++ b/src/SMAPI.Installer/assets/README.txt @@ -24,19 +24,20 @@ Manual install THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. If you really want to install SMAPI manually, here's how. -1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on Linux/Mac). - You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent confusion. +1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on + Linux/macOS). You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent + confusion. 2. Copy the files from the folder you just unzipped into your game folder. The `StardewModdingAPI.exe` file should be right next to the game's executable. 3. - Windows only: if you use Steam, see the install guide above to enable achievements and overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. - - Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and + - Linux/macOS only: rename the "StardewValley" file (no extension) to "StardewValley-original", and "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to play with mods. -When installing on Linux or Mac: +When installing on Linux or macOS: - Make sure Mono is installed (normally the installer checks for you). While it's not required, many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or freeze the game.) diff --git a/src/SMAPI.Installer/assets/unix-install.sh b/src/SMAPI.Installer/assets/unix-install.sh index 6d0c86ce..311c5469 100644 --- a/src/SMAPI.Installer/assets/unix-install.sh +++ b/src/SMAPI.Installer/assets/unix-install.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Run the SMAPI installer through Mono on Linux or Mac. +# Run the SMAPI installer through Mono on Linux or macOS. # Move to script's directory cd "`dirname "$0"`" diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh index 1d97d487..a33c0d7f 100644 --- a/src/SMAPI.Installer/assets/unix-launcher.sh +++ b/src/SMAPI.Installer/assets/unix-launcher.sh @@ -37,73 +37,86 @@ if [ "$UNAME" == "Darwin" ]; then ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib fi + # create bin file + # Note: don't overwrite if it's identical, to avoid resetting permission flags + if [ ! -x StardewModdingAPI.bin.osx ] || ! cmp StardewValley.bin.osx StardewModdingAPI.bin.osx >/dev/null 2>&1; then + cp -p StardewValley.bin.osx StardewModdingAPI.bin.osx + fi + # launch SMAPI - cp StardewValley.bin.osx StardewModdingAPI.bin.osx open -a Terminal ./StardewModdingAPI.bin.osx "$@" else - # choose launcher - LAUNCHER="" + # choose binary file to launch + LAUNCH_FILE="" if [ "$ARCH" == "x86_64" ]; then ln -sf mcs.bin.x86_64 mcs cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64" + LAUNCH_FILE="./StardewModdingAPI.bin.x86_64" else ln -sf mcs.bin.x86 mcs cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86" - fi - export LAUNCHER - - # get cross-distro version of POSIX command - COMMAND="" - if command -v command 2>/dev/null; then - COMMAND="command -v" - elif type type 2>/dev/null; then - COMMAND="type -p" + LAUNCH_FILE="./StardewModdingAPI.bin.x86" fi + export LAUNCH_FILE # select terminal (prefer xterm for best compatibility, then known supported terminals) for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do - if $COMMAND "$terminal" 2>/dev/null; then - export LAUNCHTERM=$terminal + if command -v "$terminal" 2>/dev/null; then + export TERMINAL_NAME=$terminal break; fi done # find the true shell behind x-terminal-emulator - if [ "$LAUNCHTERM" = "x-terminal-emulator" ]; then - export LAUNCHTERM="$(basename "$(readlink -f $(COMMAND x-terminal-emulator))")" + if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then + export TERMINAL_NAME="$(basename "$(readlink -f $(command -v x-terminal-emulator))")" fi # run in selected terminal and account for quirks - case $LAUNCHTERM in - terminal|termite) - # LAUNCHTERM consumes only one argument after -e - # options containing space characters are unsupported - exec $LAUNCHTERM -e "env TERM=xterm $LAUNCHER $@" - ;; - xterm|konsole|alacritty) - # LAUNCHTERM consumes all arguments after -e - exec $LAUNCHTERM -e env TERM=xterm $LAUNCHER "$@" - ;; - terminator|xfce4-terminal|mate-terminal) - # LAUNCHTERM consumes all arguments after -x - exec $LAUNCHTERM -x env TERM=xterm $LAUNCHER "$@" - ;; - gnome-terminal) - # LAUNCHTERM consumes all arguments after -- - exec $LAUNCHTERM -- env TERM=xterm $LAUNCHER "$@" - ;; - kitty) - # LAUNCHTERM consumes all trailing arguments - exec $LAUNCHTERM env TERM=xterm $LAUNCHER "$@" - ;; - *) - # If we don't know the terminal, just try to run it in the current shell. - env TERM=xterm $LAUNCHER "$@" - # if THAT fails, launch with no output - if [ $? -eq 127 ]; then - exec $LAUNCHER --no-terminal "$@" - fi - esac + export TERMINAL_PATH="$(command -v $TERMINAL_NAME)" + if [ -x $TERMINAL_PATH ]; then + case $TERMINAL_NAME in + terminal|termite) + # consumes only one argument after -e + # options containing space characters are unsupported + exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@" + ;; + + xterm|konsole|alacritty) + # consumes all arguments after -e + exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@" + ;; + + terminator|xfce4-terminal|mate-terminal) + # consumes all arguments after -x + exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@" + ;; + + gnome-terminal) + # consumes all arguments after -- + exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@" + ;; + + kitty) + # consumes all trailing arguments + exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@" + ;; + + *) + # If we don't know the terminal, just try to run it in the current shell. + # If THAT fails, launch with no output. + env TERM=xterm $LAUNCH_FILE "$@" + if [ $? -eq 127 ]; then + exec $LAUNCH_FILE --no-terminal "$@" + fi + esac + + ## terminal isn't executable; fallback to current shell or no terminal + else + echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell." + env TERM=xterm $LAUNCH_FILE "$@" + if [ $? -eq 127 ]; then + exec $LAUNCH_FILE --no-terminal "$@" + fi + fi fi diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs index b5bd4600..bfe155e0 100644 --- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -129,7 +129,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting if (schemeID == MonitorColorScheme.AutoDetect) { schemeID = platform == Platform.Mac - ? MonitorColorScheme.LightBackground // MacOS doesn't provide console background color info, but it's usually white. + ? MonitorColorScheme.LightBackground // macOS doesn't provide console background color info, but it's usually white. : ColorfulConsoleWriter.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; } diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index d0123e93..8cc61f44 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -7,8 +7,8 @@ <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> - <PackageReference Include="NUnit" Version="3.12.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 4b0e45a0..4cfaf242 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using StardewValley; using StardewValley.Locations; @@ -224,18 +223,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { int removed = 0; - // get resource clumps - IList<ResourceClump> resourceClumps = - (location as Farm)?.resourceClumps - ?? (IList<ResourceClump>)(location as Woods)?.stumps - ?? new List<ResourceClump>(); + foreach (var clump in location.resourceClumps.Where(shouldRemove).ToArray()) + { + location.resourceClumps.Remove(clump); + removed++; + } - // remove matching clumps - foreach (var clump in resourceClumps.ToArray()) + if (location is Woods woods) { - if (shouldRemove(clump)) + foreach (ResourceClump clump in woods.stumps.Where(shouldRemove).ToArray()) { - resourceClumps.Remove(clump); + woods.stumps.Remove(clump); removed++; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index a187c1ff..432fdc35 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -19,7 +19,7 @@ <!-- Windows only --> <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" /> + <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" Condition="!$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))" /> </ItemGroup> <!-- Game framework --> diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 65c66d33..10e6733f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.9.5", + "Version": "3.10.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.9.5" + "MinimumApiVersion": "3.10.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs index 1edf2d6a..7a48133e 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs @@ -8,10 +8,11 @@ using Harmony; #endif using StardewModdingAPI.Framework.Patching; using StardewValley; +using xTile; namespace StardewModdingAPI.Mods.ErrorHandler.Patches { - /// <summary>A Harmony patch for <see cref="GameLocation.checkEventPrecondition"/> which intercepts invalid preconditions and logs an error instead of crashing.</summary> + /// <summary>Harmony patches for <see cref="GameLocation.checkEventPrecondition"/> and <see cref="GameLocation.updateSeasonalTileSheets"/> which intercept errors instead of crashing.</summary> /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks> [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] @@ -39,17 +40,25 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches public void Apply(Harmony harmony) { harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.checkEventPrecondition)), finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Finalize_GameLocation_CheckEventPrecondition)) ); +harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)), + finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_UpdateSeasonalTileSheets)) + ); } #else public void Apply(HarmonyInstance harmony) { harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.checkEventPrecondition)), prefix: new HarmonyMethod(this.GetType(), nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition)) ); + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), nameof(GameLocation.updateSeasonalTileSheets)), + prefix: new HarmonyMethod(this.GetType(), nameof(GameLocationPatches.Before_GameLocation_UpdateSeasonalTileSheets)) + ); } #endif @@ -74,7 +83,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches return null; } #else - /// <summary>The method to call instead of GameLocation.checkEventPrecondition.</summary> + /// <summary>The method to call instead of <see cref="GameLocation.checkEventPrecondition"/>.</summary> /// <param name="__instance">The instance being patched.</param> /// <param name="__result">The return value of the original method.</param> /// <param name="precondition">The precondition to be parsed.</param> @@ -103,5 +112,47 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches } } #endif + +#if HARMONY_2 + /// <summary>The method to call instead of <see cref="GameLocation.updateSeasonalTileSheets"/>.</summary> + /// <param name="__instance">The instance being patched.</param> + /// <param name="map">The map whose tilesheets to update.</param> + /// <param name="__exception">The exception thrown by the wrapped method, if any.</param> + /// <returns>Returns the exception to throw, if any.</returns> + private static Exception Before_GameLocation_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception __exception) + { + if (__exception != null) + GameLocationPatches.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}': \n{__exception}", LogLevel.Error); + + return null; + } +#else + /// <summary>The method to call instead of <see cref="GameLocation.updateSeasonalTileSheets"/>.</summary> + /// <param name="__instance">The instance being patched.</param> + /// <param name="map">The map whose tilesheets to update.</param> + /// <param name="__originalMethod">The method being wrapped.</param> + /// <returns>Returns whether to execute the original method.</returns> + private static bool Before_GameLocation_UpdateSeasonalTileSheets(GameLocation __instance, Map map, MethodInfo __originalMethod) + { + const string key = nameof(GameLocationPatches.Before_GameLocation_UpdateSeasonalTileSheets); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __originalMethod.Invoke(__instance, new object[] { map }); + return false; + } + catch (TargetInvocationException ex) + { + GameLocationPatches.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}'. Technical details:\n{ex.InnerException}", LogLevel.Error); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index 788f6f16..006a09ca 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -15,11 +15,12 @@ <ItemGroup> <Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" /> + <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="False" /> </ItemGroup> <!-- Windows only --> <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" /> + <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" Condition="!$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))" /> </ItemGroup> <!-- Game framework --> diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 1e810113..ab519781 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.9.5", + "Version": "3.10.0", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.9.5" + "MinimumApiVersion": "3.10.0" } diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index b8d3be1c..d6414e9c 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -145,7 +145,7 @@ namespace StardewModdingAPI.Mods.SaveBackup try { if (Constants.TargetPlatform == GamePlatform.Mac) - this.CompressUsingMacProcess(sourcePath, destination); // due to limitations with the bundled Mono on Mac, we can't reference System.IO.Compression + this.CompressUsingMacProcess(sourcePath, destination); // due to limitations with the bundled Mono on macOS, we can't reference System.IO.Compression else this.CompressUsingNetFramework(sourcePath, destination); @@ -185,7 +185,7 @@ namespace StardewModdingAPI.Mods.SaveBackup createFromDirectory.Invoke(null, new object[] { sourcePath, destination.FullName, CompressionLevel.Fastest, false }); } - /// <summary>Create a zip using a process command on MacOS.</summary> + /// <summary>Create a zip using a process command on macOS.</summary> /// <param name="sourcePath">The file or directory path to zip.</param> /// <param name="destination">The destination file to create.</param> private void CompressUsingMacProcess(string sourcePath, FileInfo destination) diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index ced7888a..0d421b8f 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.9.5", + "Version": "3.10.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.9.5" + "MinimumApiVersion": "3.10.0" } diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index a0e5b2df..27520baf 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -16,9 +16,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Moq" Version="4.15.1" /> + <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="NUnit" Version="3.12.0" /> + <PackageReference Include="NUnit" Version="3.13.2" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index b5494003..5a342974 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -251,7 +251,7 @@ namespace SMAPI.Tests.Utilities [TestCase( @"~/parent", @"~/PARENT/child", - ExpectedResult = @"child" // note: incorrect on Linux and sometimes MacOS, but not worth the complexity of detecting whether the filesystem is case-sensitive for SMAPI's purposes + ExpectedResult = @"child" // note: incorrect on Linux and sometimes macOS, but not worth the complexity of detecting whether the filesystem is case-sensitive for SMAPI's purposes )] #endif public string GetRelativePath(string sourceDir, string targetPath) diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 2fb6ed20..c2d906a0 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <param name="content">The body content to post.</param> private TResult Post<TBody, TResult>(string url, TBody content) { - // note: avoid HttpClient for Mac compatibility + // note: avoid HttpClient for macOS compatibility using WebClient client = new WebClient(); Uri fullUrl = new Uri(this.BaseUrl, url); diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 785daba3..c90fc1d3 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -20,9 +20,6 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// <summary>The current OS.</summary> private readonly Platform Platform; - /// <summary>The name of the Stardew Valley executable.</summary> - private readonly string ExecutableName; - /********* ** Public methods @@ -31,7 +28,6 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning public GameScanner() { this.Platform = EnvironmentUtility.DetectPlatform(); - this.ExecutableName = EnvironmentUtility.GetExecutableName(this.Platform); } /// <summary>Find all valid Stardew Valley install folders.</summary> @@ -58,7 +54,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// <param name="dir">The folder to check.</param> public bool LooksLikeGameFolder(DirectoryInfo dir) { - return dir.Exists && dir.EnumerateFiles(this.ExecutableName).Any(); + return + dir.Exists + && ( + dir.EnumerateFiles("StardewValley.exe").Any() + || dir.EnumerateFiles("Stardew Valley.exe").Any() + ); } @@ -82,7 +83,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning ? $"{home}/.steam/steam/steamapps/common/Stardew Valley" : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley"; - // Mac + // macOS yield return "/Applications/Stardew Valley.app/Contents/MacOS"; yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; } diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index e635725c..8cbd8e51 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -74,7 +74,7 @@ namespace StardewModdingAPI.Toolkit.Framework break; case nameof(Platform.Mac): - name = $"MacOS {name}"; + name = $"macOS {name}"; break; } return name; @@ -124,10 +124,10 @@ namespace StardewModdingAPI.Toolkit.Framework } } - /// <summary>Detect whether the code is running on Mac.</summary> + /// <summary>Detect whether the code is running on macOS.</summary> /// <remarks> - /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the - /// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS. + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects macOS by calling the + /// <c>uname</c> system command and checking the response, which is always 'Darwin' for macOS. /// </remarks> private static bool IsRunningMac() { diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs index 925e0b5c..afebba87 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>The mod patches the game in a way that may impact stability.</summary> PatchesGame = 4, - /// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary> + /// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary> UsesDynamic = 8, /// <summary>The mod references specialized 'unvalidated update tick' events which may impact stability.</summary> diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index fd206d9d..e6105f9c 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning private readonly HashSet<Regex> IgnoreFilesystemNames = new HashSet<Regex> { new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager - new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS + new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // macOS new Regex(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase) // Windows }; @@ -38,6 +38,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // images ".bmp", ".gif", + ".ico", ".jpeg", ".jpg", ".png", @@ -136,7 +137,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); // SMAPI installer - if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on Mac.command" || p.Name == "install on Windows.bat")) + if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on macOS.command" || p.Name == "install on Windows.bat")) return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file)."); // not a mod? diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index d8e32acf..8dc4d559 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -9,9 +9,9 @@ <Import Project="..\..\build\common.targets" /> <ItemGroup> - <PackageReference Include="HtmlAgilityPack" Version="1.11.28" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> <PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" /> </ItemGroup> diff --git a/src/SMAPI.Toolkit/Utilities/Platform.cs b/src/SMAPI.Toolkit/Utilities/Platform.cs index f780e812..563d3250 100644 --- a/src/SMAPI.Toolkit/Utilities/Platform.cs +++ b/src/SMAPI.Toolkit/Utilities/Platform.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>The Linux version of the game.</summary> Linux, - /// <summary>The Mac version of the game.</summary> + /// <summary>The macOS version of the game.</summary> Mac, /// <summary>The Windows version of the game.</summary> diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index ef3ef22e..4ba94f81 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -186,7 +186,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version, Url = this.GetModUrl(id), Downloads = files.Files - .Select(file => (IModDownload)new GenericModDownload(file.Name, null, file.FileVersion)) + .Select(file => (IModDownload)new GenericModDownload(file.Name, file.Description, file.FileVersion)) .ToArray() }; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index ce5ffdbd..e9d209ec 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -13,17 +13,17 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Azure.Storage.Blobs" Version="12.7.0" /> - <PackageReference Include="Hangfire.AspNetCore" Version="1.7.17" /> + <PackageReference Include="Azure.Storage.Blobs" Version="12.8.3" /> + <PackageReference Include="Hangfire.AspNetCore" Version="1.7.22" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" /> - <PackageReference Include="HtmlAgilityPack" Version="1.11.28" /> - <PackageReference Include="Humanizer.Core" Version="2.8.26" /> - <PackageReference Include="JetBrains.Annotations" Version="2020.1.0" /> - <PackageReference Include="Markdig" Version="0.22.0" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> + <PackageReference Include="Humanizer.Core" Version="2.9.9" /> + <PackageReference Include="JetBrains.Annotations" Version="2021.1.0" /> + <PackageReference Include="Markdig" Version="0.24.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.8" /> - <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" /> - <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.1" /> - <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" /> + <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> + <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.2" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" /> diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index bd1f8c9b..2556936c 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -231,7 +231,7 @@ namespace StardewModdingAPI.Web : null })) - // redirect to HTTPS (except API for Linux/Mac Mono compatibility) + // redirect to HTTPS (except API for Linux/macOS Mono compatibility) .Add( new RedirectToHttpsRule(except: req => req.Host.Host == "localhost" || req.Path.StartsWithSegments("/api")) ); diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index d78a155e..9d6e4bed 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -19,7 +19,7 @@ </h1> <div id="blurb"> <p>The mod loader for Stardew Valley.</p> - <p>Compatible with GOG/Steam achievements and Linux/Mac/Windows, uninstall anytime, and there's a friendly community if you need help.</p> + <p>Compatible with GOG/Steam achievements and Linux/macOS/Windows, uninstall anytime, and there's a friendly community if you need help.</p> </div> <div id="call-to-action"> diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index fd472673..06d46c9e 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -120,7 +120,7 @@ else if (Model.ParsedLog?.IsValid == true) </ol> </div> <div data-os="@Platform.Mac"> - On Mac: + On macOS: <ol> <li>Open the Finder app.</li> <li>Click <em>Go</em> at the top, then <em>Go to Folder</em>.</li> diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 8cc60a73..eeda13eb 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -109,7 +109,7 @@ "Rubydew": { "ID": "bwdy.rubydew", - "SuppressWarnings": "UsesDynamic", // mod explicitly loads DLLs for Linux/Mac compatibility + "SuppressWarnings": "UsesDynamic", // mod explicitly loads DLLs for Linux/macOS compatibility "Default | UpdateKey": "Nexus:3656" }, diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index f5056eb1..49900da6 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -11,9 +11,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", "type": "string", - "const": "1.21.0", + "const": "1.22.0", "@errorMessages": { - "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.21.0'." + "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.22.0'." } }, "ConfigSchema": { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 8b0c952d..3c21b205 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -38,6 +38,14 @@ namespace StardewModdingAPI /// <summary>The target game platform.</summary> internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform()); + /// <summary>Whether SMAPI is being compiled for Windows with a 64-bit Linux version of the game. This is highly specialized and shouldn't be used in most cases.</summary> + internal static bool IsWindows64BitHack { get; } = +#if SMAPI_FOR_WINDOWS_64BIT_HACK + true; +#else + false; +#endif + /// <summary>The game framework running the game.</summary> internal static GameFramework GameFramework { get; } = #if SMAPI_FOR_XNA @@ -47,10 +55,13 @@ namespace StardewModdingAPI #endif /// <summary>The game's assembly name.</summary> - internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley"; + internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows && !EarlyConstants.IsWindows64BitHack ? "Stardew Valley" : "StardewValley"; /// <summary>The <see cref="Context.ScreenId"/> value which should appear in the SMAPI log, if any.</summary> internal static int? LogScreenId { get; set; } + + /// <summary>SMAPI's current raw semantic version.</summary> + internal static string RawApiVersion = "3.10.0"; } /// <summary>Contains SMAPI's constants and assumptions.</summary> @@ -63,7 +74,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.5"); + 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.5.4"); @@ -231,33 +242,27 @@ namespace StardewModdingAPI targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly); // get changes for platform - switch (targetPlatform) + if (Constants.Platform != Platform.Windows || EarlyConstants.IsWindows64BitHack) { - case Platform.Linux: - case Platform.Mac: - removeAssemblyReferences.AddRange(new[] - { - "Netcode", - "Stardew Valley" - }); - targetAssemblies.Add( - typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/Mac - ); - break; - - case Platform.Windows: - removeAssemblyReferences.Add( - "StardewValley" - ); - targetAssemblies.AddRange(new[] - { - typeof(Netcode.NetBool).Assembly, - typeof(StardewValley.Game1).Assembly - }); - break; - - default: - throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); + removeAssemblyReferences.AddRange(new[] + { + "Netcode", + "Stardew Valley" + }); + targetAssemblies.Add( + typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/macOS + ); + } + else + { + removeAssemblyReferences.Add( + "StardewValley" + ); + targetAssemblies.AddRange(new[] + { + typeof(Netcode.NetBool).Assembly, + typeof(StardewValley.Game1).Assembly + }); } // get changes for game framework @@ -295,6 +300,21 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray()); } + /// <summary>Get whether the game assembly was patched by Stardew64Installer.</summary> + /// <param name="version">The version of Stardew64Installer which was applied to the game assembly, if any.</param> + internal static bool IsPatchedByStardew64Installer(out ISemanticVersion version) + { + PropertyInfo property = typeof(Game1).GetProperty("Stardew64InstallerVersion"); + if (property == null) + { + version = null; + return false; + } + + version = new SemanticVersion((string)property.GetValue(null)); + return true; + } + /********* ** Private methods diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index 5f70d0f7..a745592c 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -86,7 +86,7 @@ namespace StardewModdingAPI public static bool HasRemotePlayers => Context.IsMultiplayer && !Game1.hasLocalClientsOnly; /// <summary>Whether the current player is the main player. This is always true in single-player, and true when hosting in multiplayer.</summary> - public static bool IsMainPlayer => Game1.IsMasterGame && !(TitleMenu.subMenu is FarmhandMenu); + public static bool IsMainPlayer => Game1.IsMasterGame && Context.ScreenId == 0 && !(TitleMenu.subMenu is FarmhandMenu); /********* diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index 5f91610e..529fb93a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Framework.Content ** Fields *********/ /// <summary>The minimum value to consider non-transparent.</summary> - /// <remarks>On Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason.</remarks> + /// <remarks>On Linux/macOS, fully transparent pixels may have an alpha up to 4 for some reason.</remarks> private const byte MinOpacity = 5; @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.Content // premultiplied by the content pipeline. The formula is derived from // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. // Note: don't use named arguments here since they're different between - // Linux/Mac and Windows. + // Linux/macOS and Windows. float alphaBelow = 1 - (above.A / 255f); newData[i] = new Color( (int)(above.R + (below.R * alphaBelow)), // r diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 7edc9ab9..5c7ad778 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -57,6 +57,8 @@ namespace StardewModdingAPI.Framework.Content IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); this.NormalizeAssetNameForPlatform = path => method.Invoke<string>(path); } + else if (EarlyConstants.IsWindows64BitHack) + this.NormalizeAssetNameForPlatform = PathUtilities.NormalizePath; else this.NormalizeAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 2920e670..d0e759c2 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -132,7 +132,7 @@ namespace StardewModdingAPI.Framework ); this.ContentManagers.Add(contentManagerForAssetPropagation); this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, reflection, aggressiveMemoryOptimizations); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, aggressiveMemoryOptimizations); } /// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary> diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 9af14cb5..4f6aa775 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>The language code for language-agnostic mod assets.</summary> private readonly LanguageCode DefaultLanguage = Constants.DefaultLanguage; + /// <summary>If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder.</summary> + private readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" }; + /********* ** Public methods @@ -215,11 +218,17 @@ namespace StardewModdingAPI.Framework.ContentManagers FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") + if (!file.Exists && file.Extension == string.Empty) { - FileInfo result = new FileInfo(file.FullName + ".xnb"); - if (result.Exists) - file = result; + foreach (string extension in this.LocalTilesheetExtensions) + { + FileInfo result = new FileInfo(file.FullName + extension); + if (result.Exists) + { + file = result; + break; + } + } } return file; @@ -259,6 +268,7 @@ namespace StardewModdingAPI.Framework.ContentManagers string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets + this.Monitor.VerboseLog($"Fixing tilesheet paths for map '{relativeMapPath}' from mod '{this.ModName}'..."); foreach (TileSheet tilesheet in map.TileSheets) { // get image source @@ -280,6 +290,9 @@ namespace StardewModdingAPI.Framework.ContentManagers if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out string assetName, out string error)) throw new SContentLoadException($"{errorPrefix} {error}"); + if (assetName != tilesheet.ImageSource) + this.Monitor.VerboseLog($" Mapped tilesheet '{tilesheet.ImageSource}' to '{assetName}'."); + tilesheet.ImageSource = assetName; } catch (Exception ex) when (!(ex is SContentLoadException)) @@ -308,6 +321,15 @@ namespace StardewModdingAPI.Framework.ContentManagers return true; } + // special case: local filenames starting with a dot should be ignored + // For example, this lets mod authors have a '.spring_town.png' file in their map folder so it can be + // opened in Tiled, while still mapping it to the vanilla 'Maps/spring_town' asset at runtime. + { + string filename = Path.GetFileName(relativePath); + if (filename.StartsWith(".")) + relativePath = Path.Combine(Path.GetDirectoryName(relativePath) ?? "", filename.TrimStart('.')); + } + // get relative to map file { string localKey = Path.Combine(modRelativeMapFolder, relativePath); diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index a6835dbe..0660a367 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework /// <summary>Encapsulates SMAPI's JSON file parsing.</summary> private readonly JsonHelper JsonHelper; - /// <summary>A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/Mac.</summary> + /// <summary>A cache of case-insensitive => exact relative paths within the content pack, for case-insensitive file lookups on Linux/macOS.</summary> private readonly IDictionary<string, string> RelativePaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 5d2f352d..f5babafb 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -117,6 +117,11 @@ namespace StardewModdingAPI.Framework /// <param name="validOnly">Only return valid update keys.</param> IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = true); + /// <summary>Get whether the given mod ID must be installed to load this mod.</summary> + /// <param name="modId">The mod ID to check.</param> + /// <param name="includeOptional">Whether to include optional dependencies.</param> + bool HasRequiredModId(string modId, bool includeOptional); + /// <summary>Get the mod IDs that must be installed to load this mod.</summary> /// <param name="includeOptional">Whether to include optional dependencies.</param> IEnumerable<string> GetRequiredModIds(bool includeOptional = false); diff --git a/src/SMAPI/Framework/Logging/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs index 6b5babcd..6ab2bdfb 100644 --- a/src/SMAPI/Framework/Logging/LogFileManager.cs +++ b/src/SMAPI/Framework/Logging/LogFileManager.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Logging public void WriteLine(string message) { // always use Windows-style line endings for convenience - // (Linux/Mac editors are fine with them, Windows editors often require them) + // (Linux/macOS editors are fine with them, Windows editors often require them) this.Stream.Write(message + "\r\n"); } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 243ca3ae..a4df3c18 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -283,13 +283,16 @@ namespace StardewModdingAPI.Framework.Logging /// <param name="customSettings">The custom SMAPI settings.</param> public void LogIntro(string modsPath, IDictionary<string, object> customSettings) { - // get platform label - string platformLabel = EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform); - if ((Constants.GameFramework == GameFramework.Xna) != (Constants.Platform == Platform.Windows)) - platformLabel += $" with {Constants.GameFramework}"; + // log platform & patches + { + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + + string[] patchLabels = this.GetPatchLabels().ToArray(); + if (patchLabels.Any()) + this.Monitor.Log($"Detected custom version: {string.Join(", ", patchLabels)}", LogLevel.Info); + } - // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {platformLabel}", LogLevel.Info); + // log basic info this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); if (modsPath != Constants.DefaultModsPath) this.Monitor.Log("(Using custom --mods-path argument.)"); @@ -406,6 +409,20 @@ namespace StardewModdingAPI.Framework.Logging gameMonitor.Log(message, level); } + /// <summary>Get human-readable labels to log for detected SMAPI and Stardew Valley customizations.</summary> + private IEnumerable<string> GetPatchLabels() + { + // custom game framework + if (EarlyConstants.IsWindows64BitHack) + yield return $"running 64-bit SMAPI with {Constants.GameFramework}"; + else if ((Constants.GameFramework == GameFramework.Xna) != (Constants.Platform == Platform.Windows)) + yield return $"running {Constants.GameFramework}"; + + // patched by Stardew64Installer + if (Constants.IsPatchedByStardew64Installer(out ISemanticVersion patchedByVersion)) + yield return $"patched by Stardew64Installer {patchedByVersion}"; + } + /// <summary>Write a summary of mod warnings to the console and log.</summary> /// <param name="mods">The loaded mods.</param> /// <param name="skippedMods">The mods which could not be loaded.</param> @@ -426,67 +443,38 @@ namespace StardewModdingAPI.Framework.Logging // log skipped mods if (skippedMods.Any()) { - // get logging logic - HashSet<string> loggedDuplicateIds = new HashSet<string>(); - void LogSkippedMod(IModMetadata mod) - { - string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}"; + var loggedDuplicateIds = new HashSet<string>(); - // handle duplicate mods - // (log first duplicate only, don't show redundant version) - if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest()) + this.Monitor.Log(" Skipped mods", LogLevel.Error); + this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); + this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); + this.Monitor.Newline(); + foreach (var list in this.GroupFailedModsByPriority(skippedMods)) + { + if (list.Any()) { - if (!loggedDuplicateIds.Add(mod.Manifest.UniqueID)) - return; // already logged + foreach (IModMetadata mod in list.OrderBy(p => p.DisplayName)) + { + string message = $" - {mod.DisplayName}{(" " + mod.Manifest?.Version?.ToString()).TrimEnd()} because {mod.Error}"; - message = $" - {mod.DisplayName} because {mod.Error}"; - } + // duplicate mod: log first one only, don't show redundant version + if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest()) + { + if (loggedDuplicateIds.Add(mod.Manifest.UniqueID)) + continue; // already logged - // log message - this.Monitor.Log(message, LogLevel.Error); - if (mod.ErrorDetails != null) - this.Monitor.Log($" ({mod.ErrorDetails})"); - } + message = $" - {mod.DisplayName} because {mod.Error}"; + } - // group mods - List<IModMetadata> skippedDependencies = new List<IModMetadata>(); - List<IModMetadata> otherSkippedMods = new List<IModMetadata>(); - { - // track broken dependencies - HashSet<string> skippedDependencyIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); - HashSet<string> skippedModIds = new HashSet<string>(from mod in skippedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase); - foreach (IModMetadata mod in skippedMods) - { - foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds())) - skippedDependencyIds.Add(requiredId); - } + // log message + this.Monitor.Log(message, LogLevel.Error); + if (mod.ErrorDetails != null) + this.Monitor.Log($" ({mod.ErrorDetails})"); + } - // collect mod groups - foreach (IModMetadata mod in skippedMods) - { - if (mod.HasID() && skippedDependencyIds.Contains(mod.Manifest.UniqueID)) - skippedDependencies.Add(mod); - else - otherSkippedMods.Add(mod); + this.Monitor.Newline(); } } - - // log skipped mods - this.Monitor.Log(" Skipped mods", LogLevel.Error); - this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); - this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); - this.Monitor.Newline(); - - if (skippedDependencies.Any()) - { - foreach (IModMetadata mod in skippedDependencies.OrderBy(p => p.DisplayName)) - LogSkippedMod(mod); - this.Monitor.Newline(); - } - - foreach (IModMetadata mod in otherSkippedMods.OrderBy(p => p.DisplayName)) - LogSkippedMod(mod); - this.Monitor.Newline(); } // log warnings @@ -553,9 +541,95 @@ namespace StardewModdingAPI.Framework.Logging // not crossplatform this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", - "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." + "These mods use the 'dynamic' keyword, and won't work on Linux/macOS." + ); + } + } + + /// <summary>Group failed mods by the priority players should update them, where mods in earlier groups are more likely to fix multiple mods.</summary> + /// <param name="failedMods">The failed mods to group.</param> + private IEnumerable<IList<IModMetadata>> GroupFailedModsByPriority(IList<IModMetadata> failedMods) + { + var failedOthers = failedMods.ToList(); + var skippedModIds = new HashSet<string>(from mod in failedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase); + + // group B: dependencies which failed + var failedOtherDependencies = new List<IModMetadata>(); + { + // get failed dependency IDs + var skippedDependencyIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + foreach (IModMetadata mod in failedMods) + { + foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds())) + skippedDependencyIds.Add(requiredId); + } + + // group matching mods + this.FilterThrough( + fromList: failedOthers, + toList: failedOtherDependencies, + match: mod => mod.HasID() && skippedDependencyIds.Contains(mod.Manifest.UniqueID) ); } + + // group A: failed root dependencies which other dependencies need + var failedRootDependencies = new List<IModMetadata>(); + { + var skippedDependencyIds = new HashSet<string>(failedOtherDependencies.Select(p => p.Manifest.UniqueID)); + this.FilterThrough( + fromList: failedOtherDependencies, + toList: failedRootDependencies, + match: mod => + { + // has no failed dependency + foreach (string requiredId in mod.GetRequiredModIds()) + { + if (skippedDependencyIds.Contains(requiredId)) + return false; + } + + // another dependency depends on this mod + bool isDependedOn = false; + foreach (IModMetadata other in failedOtherDependencies) + { + if (other.HasRequiredModId(mod.Manifest.UniqueID, includeOptional: false)) + { + isDependedOn = true; + break; + } + } + + return isDependedOn; + } + ); + } + + // return groups + return new[] + { + failedRootDependencies, + failedOtherDependencies, + failedOthers + }; + } + + /// <summary>Filter matching items from one list and add them to the other.</summary> + /// <typeparam name="TItem">The list item type.</typeparam> + /// <param name="fromList">The list to filter.</param> + /// <param name="toList">The list to which to add filtered items.</param> + /// <param name="match">Matches items to filter through.</param> + private void FilterThrough<TItem>(IList<TItem> fromList, IList<TItem> toList, Func<TItem, bool> match) + { + for (int i = 0; i < fromList.Count; i++) + { + TItem item = fromList[i]; + if (match(item)) + { + toList.Add(item); + fromList.RemoveAt(i); + i--; + } + } } /// <summary>Write a mod warning group to the console and log.</summary> diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index a948213b..baffc50e 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod.</summary> DetectedSaveSerializer, - /// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary> + /// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary> DetectedDynamic, /// <summary>The instruction is compatible, but references <see cref="ISpecializedEvents.UnvalidatedUpdateTicking"/> or <see cref="ISpecializedEvents.UnvalidatedUpdateTicked"/> which may impact stability.</summary> diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index b4de3d6c..17e6d59a 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The non-error issues with the mod, including warnings suppressed by the data record.</summary> private ModWarning ActualWarnings = ModWarning.None; + /// <summary>The mod IDs which are listed as a requirement by this mod. The value for each pair indicates whether the dependency is required (i.e. not an optional dependency).</summary> + private readonly Lazy<IDictionary<string, bool>> Dependencies; + /********* ** Accessors @@ -100,6 +103,8 @@ namespace StardewModdingAPI.Framework.ModLoading this.Manifest = manifest; this.DataRecord = dataRecord; this.IsIgnored = isIgnored; + + this.Dependencies = new Lazy<IDictionary<string, bool>>(this.ExtractDependencies); } /// <inheritdoc /> @@ -199,23 +204,21 @@ namespace StardewModdingAPI.Framework.ModLoading } /// <inheritdoc /> - public IEnumerable<string> GetRequiredModIds(bool includeOptional = false) + public bool HasRequiredModId(string modId, bool includeOptional) { - HashSet<string> required = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + return + this.Dependencies.Value.TryGetValue(modId, out bool isRequired) + && (includeOptional || isRequired); + } - // yield dependencies - if (this.Manifest?.Dependencies != null) + /// <inheritdoc /> + public IEnumerable<string> GetRequiredModIds(bool includeOptional = false) + { + foreach (var pair in this.Dependencies.Value) { - foreach (var entry in this.Manifest?.Dependencies) - { - if ((entry.IsRequired || includeOptional) && required.Add(entry.UniqueID)) - yield return entry.UniqueID; - } + if (includeOptional || pair.Value) + yield return pair.Key; } - - // yield content pack parent - if (this.Manifest?.ContentPackFor?.UniqueID != null && required.Add(this.Manifest.ContentPackFor.UniqueID)) - yield return this.Manifest.ContentPackFor.UniqueID; } /// <inheritdoc /> @@ -237,5 +240,29 @@ namespace StardewModdingAPI.Framework.ModLoading string rootFolderName = Path.GetFileName(this.RootPath) ?? ""; return Path.Combine(rootFolderName, this.RelativeDirectoryPath); } + + + /********* + ** Private methods + *********/ + /// <summary>Extract mod IDs from the manifest that must be installed to load this mod.</summary> + /// <returns>Returns a dictionary of mod ID => is required (i.e. not an optional dependency).</returns> + public IDictionary<string, bool> ExtractDependencies() + { + var ids = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); + + // yield dependencies + if (this.Manifest?.Dependencies != null) + { + foreach (var entry in this.Manifest?.Dependencies) + ids[entry.UniqueID] = entry.IsRequired; + } + + // yield content pack parent + if (this.Manifest?.ContentPackFor?.UniqueID != null) + ids[this.Manifest.ContentPackFor.UniqueID] = true; + + return ids; + } } } diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs index cf71af77..aefd1c20 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs @@ -4,10 +4,10 @@ using Microsoft.Xna.Framework.Graphics; namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades { - /// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows.</summary> + /// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/macOS or Windows.</summary> /// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks> [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/macOS.")] [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] public class SpriteBatchFacade : SpriteBatch { diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 4a80e34c..a71bafd9 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -52,6 +52,9 @@ namespace StardewModdingAPI.Framework.Models /// <summary>SMAPI's GitHub project name, used to perform update checks.</summary> public string GitHubProjectName { get; set; } + /// <summary>Stardew64Installer's GitHub project name, used to perform update checks.</summary> + public string Stardew64InstallerGitHubProjectName { get; set; } + /// <summary>The base URL for SMAPI's web API, used to perform update checks.</summary> public string WebApiBaseUrl { get; set; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ebb21555..5862b112 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -187,7 +187,7 @@ namespace StardewModdingAPI.Framework #if SMAPI_FOR_WINDOWS if (Constants.Platform != Platform.Windows) { - this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or Mac. Please reinstall SMAPI to fix this.", LogLevel.Error); + this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or macOS. Please reinstall SMAPI to fix this.", LogLevel.Error); this.LogManager.PressAnyKeyToExit(); } #else @@ -263,10 +263,7 @@ namespace StardewModdingAPI.Framework }); // set window titles - this.SetWindowTitles( - game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}", - smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}" - ); + this.UpdateWindowTitles(); } catch (Exception ex) { @@ -280,10 +277,7 @@ namespace StardewModdingAPI.Framework this.LogManager.LogSettingsHeader(this.Settings); // set window titles - this.SetWindowTitles( - game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}", - smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}" - ); + this.UpdateWindowTitles(); // start game this.Monitor.Log("Starting game...", LogLevel.Debug); @@ -387,11 +381,7 @@ namespace StardewModdingAPI.Framework } // update window titles - int modsLoaded = this.ModRegistry.GetAll().Count(); - this.SetWindowTitles( - game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods", - smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods" - ); + this.UpdateWindowTitles(); } /// <summary>Raised after the game finishes initializing.</summary> @@ -419,7 +409,7 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); // log GPU info -#if SMAPI_FOR_WINDOWS +#if SMAPI_FOR_WINDOWS && !SMAPI_FOR_WINDOWS_64BIT_HACK this.Monitor.Log($"Running on GPU: {Game1.game1.GraphicsDevice?.Adapter?.Description ?? "<unknown>"}"); #endif } @@ -1238,13 +1228,23 @@ namespace StardewModdingAPI.Framework return !issuesFound; } - /// <summary>Set the window titles for the game and console windows.</summary> - /// <param name="game">The game window text.</param> - /// <param name="smapi">The SMAPI window text.</param> - private void SetWindowTitles(string game, string smapi) + /// <summary>Set the titles for the game and console windows.</summary> + private void UpdateWindowTitles() { - this.Game.Window.Title = game; - this.LogManager.SetConsoleTitle(smapi); + string smapiVersion = $"{Constants.ApiVersion}{(EarlyConstants.IsWindows64BitHack ? " [64-bit]" : "")}"; + + string consoleTitle = $"SMAPI {smapiVersion} - running Stardew Valley {Constants.GameVersion}"; + string gameTitle = $"Stardew Valley {Constants.GameVersion} - running SMAPI {smapiVersion}"; + + if (this.ModRegistry.AreAllModsLoaded) + { + int modsLoaded = this.ModRegistry.GetAll().Count(); + consoleTitle += $" with {modsLoaded} mods"; + gameTitle += $" with {modsLoaded} mods"; + } + + this.Game.Window.Title = gameTitle; + this.LogManager.SetConsoleTitle(consoleTitle); } /// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> @@ -1259,7 +1259,7 @@ namespace StardewModdingAPI.Framework // create client string url = this.Settings.WebApiBaseUrl; #if !SMAPI_FOR_WINDOWS - url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac + url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/macOS #endif WebApiClient client = new WebApiClient(url, Constants.ApiVersion); this.Monitor.Log("Checking for updates..."); @@ -1302,6 +1302,41 @@ namespace StardewModdingAPI.Framework this.LogManager.WriteUpdateMarker(updateFound.ToString(), updateUrl); } + // check Stardew64Installer version + if (Constants.IsPatchedByStardew64Installer(out ISemanticVersion patchedByVersion)) + { + ISemanticVersion updateFound = null; + string updateUrl = null; + try + { + // fetch update check + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Steviegt6.Stardew64Installer", patchedByVersion, new[] { $"GitHub:{this.Settings.Stardew64InstallerGitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value; + updateFound = response.SuggestedUpdate?.Version; + updateUrl = response.SuggestedUpdate?.Url ?? Constants.HomePageUrl; + + // log message + if (updateFound != null) + this.Monitor.Log($"You can update Stardew64Installer to {updateFound}: {updateUrl}", LogLevel.Alert); + else + this.Monitor.Log(" Stardew64Installer okay."); + + // show errors + if (response.Errors.Any()) + { + this.Monitor.Log("Couldn't check for a new version of Stardew64Installer. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); + } + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for a new version of Stardew64Installer. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? $"Error: {ex.Message}" + : $"Error: {ex.GetLogSummary()}" + ); + } + } + // check mod versions if (mods.Any()) { diff --git a/src/SMAPI/Framework/Serialization/ColorConverter.cs b/src/SMAPI/Framework/Serialization/ColorConverter.cs index 7315f1a5..3b3720b5 100644 --- a/src/SMAPI/Framework/Serialization/ColorConverter.cs +++ b/src/SMAPI/Framework/Serialization/ColorConverter.cs @@ -8,7 +8,7 @@ namespace StardewModdingAPI.Framework.Serialization { /// <summary>Handles deserialization of <see cref="Color"/> for crossplatform compatibility.</summary> /// <remarks> - /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } + /// - Linux/macOS format: { "B": 76, "G": 51, "R": 25, "A": 102 } /// - Windows format: "26, 51, 76, 102" /// </remarks> internal class ColorConverter : SimpleReadOnlyConverter<Color> diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs index 6cf795dc..21d1f845 100644 --- a/src/SMAPI/Framework/Serialization/PointConverter.cs +++ b/src/SMAPI/Framework/Serialization/PointConverter.cs @@ -8,7 +8,7 @@ namespace StardewModdingAPI.Framework.Serialization { /// <summary>Handles deserialization of <see cref="Point"/> for crossplatform compatibility.</summary> /// <remarks> - /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Linux/macOS format: { "X": 1, "Y": 2 } /// - Windows format: "1, 2" /// </remarks> internal class PointConverter : SimpleReadOnlyConverter<Point> diff --git a/src/SMAPI/Framework/Serialization/RectangleConverter.cs b/src/SMAPI/Framework/Serialization/RectangleConverter.cs index 8f7318b3..31f3ad77 100644 --- a/src/SMAPI/Framework/Serialization/RectangleConverter.cs +++ b/src/SMAPI/Framework/Serialization/RectangleConverter.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Framework.Serialization { /// <summary>Handles deserialization of <see cref="Rectangle"/> for crossplatform compatibility.</summary> /// <remarks> - /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } + /// - Linux/macOS format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" /// </remarks> internal class RectangleConverter : SimpleReadOnlyConverter<Rectangle> diff --git a/src/SMAPI/Framework/Serialization/Vector2Converter.cs b/src/SMAPI/Framework/Serialization/Vector2Converter.cs index 3e2ab776..589febf8 100644 --- a/src/SMAPI/Framework/Serialization/Vector2Converter.cs +++ b/src/SMAPI/Framework/Serialization/Vector2Converter.cs @@ -8,7 +8,7 @@ namespace StardewModdingAPI.Framework.Serialization { /// <summary>Handles deserialization of <see cref="Vector2"/> for crossplatform compatibility.</summary> /// <remarks> - /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Linux/macOS format: { "X": 1, "Y": 2 } /// - Windows format: "1, 2" /// </remarks> internal class Vector2Converter : SimpleReadOnlyConverter<Vector2> diff --git a/src/SMAPI/GamePlatform.cs b/src/SMAPI/GamePlatform.cs index b64595e4..cce5ed8d 100644 --- a/src/SMAPI/GamePlatform.cs +++ b/src/SMAPI/GamePlatform.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI /// <summary>The Linux version of the game.</summary> Linux = Platform.Linux, - /// <summary>The Mac version of the game.</summary> + /// <summary>The macOS version of the game.</summary> Mac = Platform.Mac, /// <summary>The Windows version of the game.</summary> diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 52da3946..623c65d5 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework.Graphics; using Netcode; +using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Utilities; @@ -36,15 +37,18 @@ namespace StardewModdingAPI.Metadata /// <summary>An internal content manager used only for asset propagation. See remarks on <see cref="GameContentManagerForAssetPropagation"/>.</summary> private readonly GameContentManagerForAssetPropagation DisposableContentManager; + /// <summary>Writes messages to the console.</summary> + private readonly IMonitor Monitor; + + /// <summary>Simplifies access to private game code.</summary> + private readonly Reflector Reflection; + /// <summary>Whether to enable more aggressive memory optimizations.</summary> private readonly bool AggressiveMemoryOptimizations; /// <summary>Normalizes an asset key to match the cache key and assert that it's valid.</summary> private readonly Func<string, string> AssertAndNormalizeAssetName; - /// <summary>Simplifies access to private game code.</summary> - private readonly Reflector Reflection; - /// <summary>Optimized bucket categories for batch reloading assets.</summary> private enum AssetBucket { @@ -65,12 +69,14 @@ namespace StardewModdingAPI.Metadata /// <summary>Initialize the core asset data.</summary> /// <param name="mainContent">The main content manager through which to reload assets.</param> /// <param name="disposableContent">An internal content manager used only for asset propagation.</param> + /// <param name="monitor">Writes messages to the console.</param> /// <param name="reflection">Simplifies access to private code.</param> /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> - public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, Reflector reflection, bool aggressiveMemoryOptimizations) + public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, bool aggressiveMemoryOptimizations) { this.MainContentManager = mainContent; this.DisposableContentManager = disposableContent; + this.Monitor = monitor; this.Reflection = reflection; this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; @@ -116,7 +122,17 @@ namespace StardewModdingAPI.Metadata default: foreach (var entry in bucket) { - bool changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out bool curChangedMapWarps); + bool changed = false; + bool curChangedMapWarps = false; + try + { + changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapWarps); + } + catch (Exception ex) + { + this.Monitor.Log($"An error occurred while propagating asset changes. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + propagatedAssets[entry.Key] = changed; updatedNpcWarps = updatedNpcWarps || curChangedMapWarps; } @@ -258,6 +274,7 @@ namespace StardewModdingAPI.Metadata return true; case "data\\bundles": // NetWorldState constructor + if (Context.IsMainPlayer && Game1.netWorldState != null) { var bundles = this.Reflection.GetField<NetBundles>(Game1.netWorldState.Value, "bundles").GetValue(); var rewards = this.Reflection.GetField<NetIntDictionary<bool, NetBool>>(Game1.netWorldState.Value, "bundleRewards").GetValue(); @@ -286,6 +303,10 @@ namespace StardewModdingAPI.Metadata Game1.clothingInformation = content.Load<Dictionary<int, string>>(key); return true; + case "data\\concessions": // MovieTheater.GetConcessions + MovieTheater.ClearCachedLocalizedData(); + return true; + case "data\\concessiontastes": // MovieTheater.GetConcessionTasteForCharacter this.Reflection .GetField<List<ConcessionTaste>>(typeof(MovieTheater), "_concessionTastes") @@ -306,16 +327,9 @@ namespace StardewModdingAPI.Metadata case "data\\hairdata": // Farmer.GetHairStyleMetadataFile return this.ReloadHairData(); - case "data\\moviesreactions": // MovieTheater.GetMovieReactions - this.Reflection - .GetField<List<MovieCharacterReaction>>(typeof(MovieTheater), "_genericReactions") - .SetValue(content.Load<List<MovieCharacterReaction>>(key)); - return true; - case "data\\movies": // MovieTheater.GetMovieData - this.Reflection - .GetField<Dictionary<string, MovieData>>(typeof(MovieTheater), "_movieData") - .SetValue(content.Load<Dictionary<string, MovieData>>(key)); + case "data\\moviesreactions": // MovieTheater.GetMovieReactions + MovieTheater.ClearCachedLocalizedData(); return true; case "data\\npcdispositions": // NPC constructor diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 986d2780..e830f799 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -24,6 +24,8 @@ namespace StardewModdingAPI /// <param name="args">The command-line arguments.</param> public static void Main(string[] args) { + Console.Title = $"SMAPI {EarlyConstants.RawApiVersion}{(EarlyConstants.IsWindows64BitHack ? " 64-bit" : "")} - {Console.Title}"; + try { AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; @@ -159,7 +161,7 @@ namespace StardewModdingAPI Console.ResetColor(); Console.WriteLine(); } - + Program.PressAnyKeyToExit(showMessage: true); } diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 034eceed..7b5625d6 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -70,8 +70,13 @@ copy all the settings, or you may cause bugs due to overridden changes in future "GitHubProjectName": "Pathoschild/SMAPI", /** + * Stardew64Installer's GitHub project name, used to perform update checks. + */ + "Stardew64InstallerGitHubProjectName": "Steviegt6/Stardew64Installer", + + /** * The base URL for SMAPI's web API, used to perform update checks. - * Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the + * Note: the protocol will be changed to http:// on Linux/macOS due to OpenSSL issues with the * game's bundled Mono. */ "WebApiBaseUrl": "https://smapi.io/api/", @@ -85,7 +90,7 @@ copy all the settings, or you may cause bugs due to overridden changes in future * The colors to use for text written to the SMAPI console. * * The possible values for 'UseScheme' are: - * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color + * - AutoDetect: SMAPI will assume a light background on macOS, and detect the background color * automatically on Linux or Windows. * - LightBackground: use darker text colors that look better on a white or light background. * - DarkBackground: use lighter text colors that look better on a black or dark background. diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index ceef33df..413d9f33 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,6 +14,10 @@ <Import Project="..\..\build\common.targets" /> + <PropertyGroup Condition="$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))"> + <PlatformTarget>x64</PlatformTarget> + </PropertyGroup> + <ItemGroup> <PackageReference Include="LargeAddressAware" Version="1.0.5" /> <PackageReference Include="Mono.Cecil" Version="0.11.3" /> @@ -34,7 +38,7 @@ <!-- Windows only --> <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" /> + <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="False" Condition="!$(DefineConstants.Contains(SMAPI_FOR_WINDOWS_64BIT_HACK))" /> <Reference Include="System.Windows.Forms" /> </ItemGroup> |