diff options
Diffstat (limited to 'src')
74 files changed, 1145 insertions, 965 deletions
diff --git a/src/SMAPI.Installer/Framework/InstallerContext.cs b/src/SMAPI.Installer/Framework/InstallerContext.cs index 88e57760..95df32ca 100644 --- a/src/SMAPI.Installer/Framework/InstallerContext.cs +++ b/src/SMAPI.Installer/Framework/InstallerContext.cs @@ -1,6 +1,4 @@ -using System; using System.IO; -using Microsoft.Win32; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.GameScanning; using StardewModdingAPI.Toolkit.Utilities; @@ -13,9 +11,6 @@ namespace StardewModdingAPI.Installer.Framework /********* ** Fields *********/ - /// <summary>The <see cref="Environment.OSVersion"/> value that represents Windows 7.</summary> - private readonly Version Windows7Version = new Version(6, 1); - /// <summary>The underlying toolkit game scanner.</summary> private readonly GameScanner GameScanner = new GameScanner(); @@ -29,9 +24,6 @@ namespace StardewModdingAPI.Installer.Framework /// <summary>The human-readable OS name and version.</summary> public string PlatformName { get; } - /// <summary>The name of the Stardew Valley executable.</summary> - public string ExecutableName { get; } - /// <summary>Whether the installer is running on Windows.</summary> public bool IsWindows => this.Platform == Platform.Windows; @@ -47,7 +39,6 @@ namespace StardewModdingAPI.Installer.Framework { this.Platform = EnvironmentUtility.DetectPlatform(); this.PlatformName = EnvironmentUtility.GetFriendlyPlatformName(this.Platform); - this.ExecutableName = EnvironmentUtility.GetExecutableName(this.Platform); } /// <summary>Get the installer's version number.</summary> @@ -57,42 +48,6 @@ namespace StardewModdingAPI.Installer.Framework return new SemanticVersion(raw); } - /// <summary>Get whether the current system has .NET Framework 4.5 or later installed. This only applies on Windows.</summary> - /// <exception cref="NotSupportedException">The current platform is not Windows.</exception> - public bool HasNetFramework45() - { - switch (this.Platform) - { - case Platform.Windows: - using (RegistryKey versionKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full")) - return versionKey?.GetValue("Release") != null; // .NET Framework 4.5+ - - default: - throw new NotSupportedException("The installed .NET Framework version can only be checked on Windows."); - } - } - - /// <summary>Get whether the current system has XNA Framework installed. This only applies on Windows.</summary> - /// <exception cref="NotSupportedException">The current platform is not Windows.</exception> - public bool HasXna() - { - switch (this.Platform) - { - case Platform.Windows: - using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\XNA\Framework")) - return key != null; // XNA Framework 4.0+ - - default: - throw new NotSupportedException("The installed XNA Framework version can only be checked on Windows."); - } - } - - /// <summary>Whether the current OS supports newer versions of .NET Framework.</summary> - public bool CanInstallLatestNetFramework() - { - return Environment.OSVersion.Version >= this.Windows7Version; // Windows 7+ - } - /// <summary>Get whether a folder seems to contain the game files.</summary> /// <param name="dir">The folder to check.</param> public bool LooksLikeGameFolder(DirectoryInfo dir) diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs index 6ba5fa5f..0976eceb 100644 --- a/src/SMAPI.Installer/Framework/InstallerPaths.cs +++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs @@ -1,4 +1,5 @@ using System.IO; +using StardewModdingAPI.Toolkit.Framework; namespace StardewModdingAPI.Installer.Framework { @@ -44,17 +45,20 @@ 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 game executable file.</summary> - public string ExecutablePath { get; private set; } + /// <summary>The full path to the installed game DLL.</summary> + public string GameDllPath { get; } - /// <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 executable file.</summary> + public string UnixSmapiExecutablePath { get; } - /// <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 launch script on Linux/macOS.</summary> + public string VanillaLaunchScriptPath { get; } - /// <summary>The full path to the vanilla game launcher on Linux/macOS after SMAPI is installed.</summary> - public string UnixBackupLauncherPath { get; } + /// <summary>The full path to the installed SMAPI launch script on Linux/macOS before it's renamed.</summary> + public string NewLaunchScriptPath { get; } + + /// <summary>The full path to the backed up game launch script on Linux/macOS after SMAPI is installed.</summary> + public string BackupLaunchScriptPath { get; } /********* @@ -63,28 +67,24 @@ namespace StardewModdingAPI.Installer.Framework /// <summary>Construct an instance.</summary> /// <param name="bundleDir">The directory path containing the files to copy into the game folder.</param> /// <param name="gameDir">The directory path for the installed game.</param> - /// <param name="gameExecutableName">The name of the game's executable file for the current platform.</param> - public InstallerPaths(DirectoryInfo bundleDir, DirectoryInfo gameDir, string gameExecutableName) + public InstallerPaths(DirectoryInfo bundleDir, DirectoryInfo gameDir) { + // base paths this.BundleDir = bundleDir; this.GameDir = gameDir; this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods")); + this.GameDllPath = Path.Combine(gameDir.FullName, Constants.GameDllName); - this.BundleApiUserConfigPath = Path.Combine(bundleDir.FullName, "smapi-internal", "config.user.json"); + // launch scripts + this.VanillaLaunchScriptPath = Path.Combine(gameDir.FullName, "StardewValley"); + this.NewLaunchScriptPath = Path.Combine(gameDir.FullName, "unix-launcher.sh"); + this.BackupLaunchScriptPath = Path.Combine(gameDir.FullName, "StardewValley-original"); + this.UnixSmapiExecutablePath = Path.Combine(gameDir.FullName, "StardewModdingAPI"); - this.ExecutablePath = Path.Combine(gameDir.FullName, gameExecutableName); - this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley"); - this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI"); - this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original"); + // internal files + this.BundleApiUserConfigPath = Path.Combine(bundleDir.FullName, "smapi-internal", "config.user.json"); 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 9f49137f..d8c27a2d 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -39,18 +39,19 @@ namespace StardewModdingApi.Installer string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); // current files - yield return GetInstallPath("libgdiplus.dylib"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only + yield return GetInstallPath("StardewModdingAPI.dll"); yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.exe.config"); yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only + yield return GetInstallPath("StardewModdingAPI.runtimeconfig.json"); yield return GetInstallPath("StardewModdingAPI.xml"); - yield return GetInstallPath("StardewModdingAPI-x64.exe"); // not normally added to game folder, but may be mistakenly added by a manual install yield return GetInstallPath("smapi-internal"); yield return GetInstallPath("steam_appid.txt"); // obsolete + yield return GetInstallPath("libgdiplus.dylib"); // before 3.13 (macOS only) yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 @@ -70,9 +71,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 - yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8 - yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8 - yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8 + yield return GetInstallPath("StardewModdingAPI-x64.exe"); // before 3.13 if (modsDir.Exists) { @@ -150,30 +149,6 @@ namespace StardewModdingApi.Installer #endif /**** - ** Check Windows dependencies - ****/ - if (context.IsWindows) - { - // .NET Framework 4.5+ - if (!context.HasNetFramework45()) - { - this.PrintError(context.CanInstallLatestNetFramework() - ? "Please install the latest version of .NET Framework before installing SMAPI." - : "Please install .NET Framework 4.5 before installing SMAPI." - ); - this.PrintError("See the download page at https://www.microsoft.com/net/download/framework for details."); - Console.ReadLine(); - return; - } - if (!context.HasXna()) - { - this.PrintError("You don't seem to have XNA Framework installed. Please run the game at least once before installing SMAPI, so it can perform its first-time setup."); - Console.ReadLine(); - return; - } - } - - /**** ** read command-line arguments ****/ // get action from CLI @@ -270,51 +245,20 @@ namespace StardewModdingApi.Installer // get folders DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath); - paths = new InstallerPaths(bundleDir, installDir, context.ExecutableName); + paths = new InstallerPaths(bundleDir, installDir); } /********* ** Step 4: validate assumptions *********/ - // not 64-bit on Windows - if (context.Platform == Platform.Windows) - { - FileInfo linuxExecutable = new FileInfo(Path.Combine(paths.GamePath, "StardewValley.exe")); - if (linuxExecutable.Exists && this.Is64Bit(linuxExecutable.FullName)) - { - this.PrintError("Oops! The detected game install path seems to be unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info."); - Console.ReadLine(); - return; - } - } - // executable exists - if (!File.Exists(paths.ExecutablePath)) + if (!File.Exists(paths.GameDllPath)) { this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); Console.ReadLine(); return; } - - // not Stardew Valley 1.5.5+ - if (File.Exists(Path.Combine(paths.GamePath, "Stardew Valley.dll"))) - { - this.PrintError("Oops! The detected game install path seems to be Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io."); - Console.ReadLine(); - return; - } - - // game folder doesn't contain paths beyond the max limit - { - string[] tooLongPaths = PathUtilities.GetTooLongPaths(Path.Combine(paths.GamePath, "Mods")).ToArray(); - if (tooLongPaths.Any()) - { - this.PrintError($"SMAPI can't install to the detected game folder, because some of its files exceed the maximum {context.Platform} path length.\nIf you need help fixing this error, see https://smapi.io/help\n\nAffected paths:\n {string.Join("\n ", tooLongPaths)}"); - Console.ReadLine(); - return; - } - } Console.Clear(); @@ -387,11 +331,11 @@ namespace StardewModdingApi.Installer ** Always uninstall old files ****/ // restore game launcher - if (context.IsUnix && File.Exists(paths.UnixBackupLauncherPath)) + if (context.IsUnix && File.Exists(paths.BackupLaunchScriptPath)) { this.PrintDebug("Removing SMAPI launcher..."); - this.InteractivelyDelete(paths.UnixLauncherPath); - File.Move(paths.UnixBackupLauncherPath, paths.UnixLauncherPath); + this.InteractivelyDelete(paths.VanillaLaunchScriptPath); + File.Move(paths.BackupLaunchScriptPath, paths.VanillaLaunchScriptPath); } // remove old files @@ -439,30 +383,41 @@ namespace StardewModdingApi.Installer this.PrintDebug("Safely replacing game launcher..."); // back up & remove current launcher - if (File.Exists(paths.UnixLauncherPath)) + if (File.Exists(paths.VanillaLaunchScriptPath)) { - if (!File.Exists(paths.UnixBackupLauncherPath)) - File.Move(paths.UnixLauncherPath, paths.UnixBackupLauncherPath); + if (!File.Exists(paths.BackupLaunchScriptPath)) + File.Move(paths.VanillaLaunchScriptPath, paths.BackupLaunchScriptPath); else - this.InteractivelyDelete(paths.UnixLauncherPath); + this.InteractivelyDelete(paths.VanillaLaunchScriptPath); } // add new launcher - File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath); + File.Move(paths.NewLaunchScriptPath, paths.VanillaLaunchScriptPath); - // mark file executable + // mark files executable // (MSBuild doesn't keep permission flags for files zipped in a build task.) - new Process + foreach (string path in new[] { paths.VanillaLaunchScriptPath, paths.UnixSmapiExecutablePath }) { - StartInfo = new ProcessStartInfo + new Process { - FileName = "chmod", - Arguments = $"755 \"{paths.UnixLauncherPath}\"", - CreateNoWindow = true - } - }.Start(); + StartInfo = new ProcessStartInfo + { + FileName = "chmod", + Arguments = $"755 \"{path}\"", + CreateNoWindow = true + } + }.Start(); + } } + // copy the game's deps.json file + // (This is needed to resolve native DLLs like libSkiaSharp.) + File.Copy( + sourceFileName: Path.Combine(paths.GamePath, "Stardew Valley.deps.json"), + destFileName: Path.Combine(paths.GamePath, "StardewModdingAPI.deps.json"), + overwrite: true + ); + // create mods directory (if needed) if (!paths.ModsDir.Exists) { @@ -527,7 +482,7 @@ namespace StardewModdingApi.Installer /********* - ** Step 6: final instructions + ** Step 7: final instructions *********/ if (context.IsWindows) { @@ -556,13 +511,6 @@ 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 LowLevelEnvironmentUtility.Is64BitAssembly(executablePath); - } - /// <summary>Get the display text for a color scheme.</summary> /// <param name="scheme">The color scheme.</param> private string GetDisplayText(MonitorColorScheme scheme) @@ -725,7 +673,7 @@ namespace StardewModdingApi.Installer { // get path from user Console.WriteLine(); - this.PrintInfo($"Type the file path to the game directory (the one containing '{context.ExecutableName}'), then press enter."); + this.PrintInfo($"Type the file path to the game directory (the one containing '{Constants.GameDllName}'), then press enter."); string path = Console.ReadLine()?.Trim(); if (string.IsNullOrWhiteSpace(path)) { diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index d9c31dd6..45cfea75 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -31,8 +31,7 @@ namespace StardewModdingApi.Installer public static void Main(string[] args) { // find install bundle - PlatformID platform = Environment.OSVersion.Platform; - FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-install.dat")); + FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, "install.dat")); if (!zipFile.Exists) { Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})"); diff --git a/src/SMAPI.Installer/SMAPI.Installer.csproj b/src/SMAPI.Installer/SMAPI.Installer.csproj index c47f3e6e..e3e01467 100644 --- a/src/SMAPI.Installer/SMAPI.Installer.csproj +++ b/src/SMAPI.Installer/SMAPI.Installer.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <RootNamespace>StardewModdingAPI.Installer</RootNamespace> <Description>The SMAPI installer for players.</Description> - <TargetFramework>net452</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <OutputType>Exe</OutputType> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> </PropertyGroup> diff --git a/src/SMAPI.Installer/assets/README.txt b/src/SMAPI.Installer/assets/README.txt index c3a7e271..5c20529a 100644 --- a/src/SMAPI.Installer/assets/README.txt +++ b/src/SMAPI.Installer/assets/README.txt @@ -24,7 +24,7 @@ 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 +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 diff --git a/src/SMAPI.Installer/assets/System.Numerics.dll b/src/SMAPI.Installer/assets/System.Numerics.dll Binary files differdeleted file mode 100644 index fed0f92c..00000000 --- a/src/SMAPI.Installer/assets/System.Numerics.dll +++ /dev/null diff --git a/src/SMAPI.Installer/assets/System.Runtime.Caching.dll b/src/SMAPI.Installer/assets/System.Runtime.Caching.dll Binary files differdeleted file mode 100644 index a062391d..00000000 --- a/src/SMAPI.Installer/assets/System.Runtime.Caching.dll +++ /dev/null diff --git a/src/SMAPI.Installer/assets/runtimeconfig.unix.json b/src/SMAPI.Installer/assets/runtimeconfig.unix.json new file mode 100644 index 00000000..8a01ceb1 --- /dev/null +++ b/src/SMAPI.Installer/assets/runtimeconfig.unix.json @@ -0,0 +1,14 @@ +{ + "runtimeOptions": { + "tfm": "net5.0", + "includedFrameworks": [ + { + "name": "Microsoft.NETCore.App", + "version": "5.0.7" + } + ], + "configProperties": { + "System.Runtime.TieredCompilation": false + } + } +} diff --git a/src/SMAPI.Installer/assets/runtimeconfig.windows.json b/src/SMAPI.Installer/assets/runtimeconfig.windows.json new file mode 100644 index 00000000..b693d809 --- /dev/null +++ b/src/SMAPI.Installer/assets/runtimeconfig.windows.json @@ -0,0 +1,12 @@ +{ + "runtimeOptions": { + "tfm": "net5.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "5.0.0" + }, + "configProperties": { + "System.Runtime.TieredCompilation": false + } + } +} diff --git a/src/SMAPI.Installer/assets/unix-install.sh b/src/SMAPI.Installer/assets/unix-install.sh index 311c5469..07df4e6c 100644 --- a/src/SMAPI.Installer/assets/unix-install.sh +++ b/src/SMAPI.Installer/assets/unix-install.sh @@ -1,24 +1,14 @@ #!/bin/bash -# Run the SMAPI installer through Mono on Linux or macOS. # Move to script's directory cd "`dirname "$0"`" -# get cross-distro version of POSIX command -COMMAND="" -if command -v command >/dev/null 2>&1; then - COMMAND="command -v" -elif type type >/dev/null 2>&1; then - COMMAND="type" +# make sure .NET 5 is installed +if ! command -v dotnet >/dev/null 2>&1; then + echo "Oops! You must have .NET 5 installed to use SMAPI: https://dotnet.microsoft.com/download"; + read + exit 1 fi -# if $TERM is not set to xterm, mono will bail out when attempting to write to the console. -export TERM=xterm - -# validate Mono & run installer -if $COMMAND mono >/dev/null 2>&1; then - mono internal/unix-install.exe -else - echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again." - read -fi +# run installer +dotnet internal/unix/SMAPI.Installer.dll diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh index d309f750..58f7a5ae 100644 --- a/src/SMAPI.Installer/assets/unix-launcher.sh +++ b/src/SMAPI.Installer/assets/unix-launcher.sh @@ -1,51 +1,19 @@ #!/usr/bin/env bash -# MonoKickstart Shell Script -# Written by Ethan "flibitijibibo" Lee -# Modified for SMAPI by various contributors -# Move to script's directory +########## +## Initial setup +########## +# move to script's directory cd "$(dirname "$0")" || exit $? -# Get the system architecture -UNAME=$(uname) -ARCH=$(uname -m) - -# MonoKickstart picks the right libfolder, so just execute the right binary. -if [ "$UNAME" == "Darwin" ]; then - # ... Except on OSX. - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ - - # El Capitan is a total idiot and wipes this variable out, making the - # Steam overlay disappear. This sidesteps "System Integrity Protection" - # and resets the variable with Valve's own variable (they provided this - # fix by the way, thanks Valve!). Note that you will need to update your - # launch configuration to the script location, NOT just the app location - # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). - # -flibit - if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then - export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" - fi - - # this was here before - ln -sf mcs.bin.osx mcs - - # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI - if [ -f libgdiplus.dylib ]; then - rm libgdiplus.dylib - fi - if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; 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 - - # Make sure we're running in Terminal (so the user can see errors/warnings/update alerts). - # Previously we would just use `open -a Terminal` to launch the .bin.osx file, but that - # doesn't let us set environment variables. +########## +## Open terminal if needed +########## +# on macOS, make sure we're running in a Terminal +# Besides letting the player see errors/warnings/alerts in the console, this is also needed because +# Steam messes with the PATH. +if [ "$(uname)" == "Darwin" ]; then if [ ! -t 1 ]; then # https://stackoverflow.com/q/911168/262123 # sanity check to make sure we don't have an infinite loop of opening windows SKIP_TERMINAL=false @@ -68,21 +36,38 @@ if [ "$UNAME" == "Darwin" ]; then exit 0 fi fi +fi + - # launch SMAPI - LC_ALL="C" ./StardewModdingAPI.bin.osx "$@" +########## +## Validate assumptions +########## +# script must be run from the game folder +if [ ! -f "Stardew Valley.dll" ]; then + echo "Oops! SMAPI must be placed in the Stardew Valley game folder.\nSee instructions: https://stardewvalleywiki.com/Modding:Player_Guide"; + read + exit 1 +fi + +# .NET 5 must be installed +if ! command -v dotnet >/dev/null 2>&1; then + echo "Oops! You must have .NET 5 installed to use SMAPI: https://dotnet.microsoft.com/download"; + read + exit 1 +fi + + +########## +## Launch SMAPI +########## +# macOS +if [ "$(uname)" == "Darwin" ]; then + dotnet StardewModdingAPI.dll "$@" + +# Linux else # 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 - LAUNCH_FILE="./StardewModdingAPI.bin.x86_64" - else - ln -sf mcs.bin.x86 mcs - cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCH_FILE="./StardewModdingAPI.bin.x86" - fi + LAUNCH_FILE="./StardewModdingAPI" export LAUNCH_FILE # select terminal (prefer xterm for best compatibility, then known supported terminals) @@ -105,44 +90,44 @@ else terminal|termite) # consumes only one argument after -e # options containing space characters are unsupported - exec $TERMINAL_NAME -e "env TERM=xterm LC_ALL=\"C\" $LAUNCH_FILE $@" + exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@" ;; xterm|konsole|alacritty) # consumes all arguments after -e - exec $TERMINAL_NAME -e env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@" + 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 LC_ALL="C" $LAUNCH_FILE "$@" + exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@" ;; gnome-terminal) # consumes all arguments after -- - exec $TERMINAL_NAME -- env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@" + exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@" ;; kitty) # consumes all trailing arguments - exec $TERMINAL_NAME env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@" + 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 LC_ALL="C" $LAUNCH_FILE "$@" + env TERM=xterm $LAUNCH_FILE "$@" if [ $? -eq 127 ]; then - exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@" + 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 LC_ALL="C" $LAUNCH_FILE "$@" + env TERM=xterm $LAUNCH_FILE "$@" if [ $? -eq 127 ]; then - exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@" + exec $LAUNCH_FILE --no-terminal "$@" fi fi fi diff --git a/src/SMAPI.Installer/assets/windows-install.bat b/src/SMAPI.Installer/assets/windows-install.bat index 2cd98554..2e0be906 100644 --- a/src/SMAPI.Installer/assets/windows-install.bat +++ b/src/SMAPI.Installer/assets/windows-install.bat @@ -1,8 +1,49 @@ @echo off -echo "%~dp0" | findstr /C:"%TEMP%" 1>nul -if not errorlevel 1 ( - echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first. - pause -) else ( - start /WAIT /B internal\windows-install.exe + +SET installerDir=%~dp0 + +REM make sure we're not running within a zip folder +echo %installerDir% | findstr /C:"%TEMP%" 1>nul +if %ERRORLEVEL% EQU 0 ( + echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first. + echo. + pause + exit +) + +REM make sure .NET 5 is installed +WHERE dotnet /q +if %ERRORLEVEL% NEQ 0 ( + echo Oops! You must have .NET 5 ^(desktop x64^) installed to use SMAPI: https://dotnet.microsoft.com/download/dotnet/5.0/runtime + echo. + pause + exit +) +dotnet --info | findstr /C:"Microsoft.WindowsDesktop.App 5." 1>nul +if %ERRORLEVEL% NEQ 0 ( + echo Oops! You must have .NET 5 ^(desktop x64^) installed to use SMAPI: https://dotnet.microsoft.com/download/dotnet/5.0/runtime + echo. + pause + exit +) + +REM make sure an antivirus hasn't deleted the installer DLL +if not exist "%installerDir%internal\windows\SMAPI.Installer.dll" ( + echo Oops! SMAPI is missing one of its files. Your antivirus might have deleted it. + echo Missing file: %installerDir%internal\windows\SMAPI.Installer.dll + echo. + pause + exit +) + +REM start installer +dotnet internal\windows\SMAPI.Installer.dll + +REM keep window open if it failed +if %ERRORLEVEL% NEQ 0 ( + echo. + echo Oops! The SMAPI installer seems to have failed. The error details may be shown above. + echo. + pause + exit ) 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 8cc61f44..264932e4 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -1,18 +1,14 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>netcoreapp2.0</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <LangVersion>latest</LangVersion> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.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> - </PackageReference> + <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> </ItemGroup> <ItemGroup> @@ -20,5 +16,4 @@ </ItemGroup> <Import Project="..\..\build\common.targets" /> - </Project> diff --git a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj index 0d109b83..3fadc37a 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj @@ -9,8 +9,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" PrivateAssets="all" /> - <PackageReference Update="NETStandard.Library" PrivateAssets="all" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 9ee6be12..140933bd 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using StardewModdingAPI.ModBuildConfig.Framework; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.ModBuildConfig { @@ -17,6 +18,10 @@ namespace StardewModdingAPI.ModBuildConfig /********* ** Accessors *********/ + /// <summary>The name (without extension or path) of the current mod's DLL.</summary> + [Required] + public string ModDllName { get; set; } + /// <summary>The name of the mod folder.</summary> [Required] public string ModFolderName { get; set; } @@ -45,9 +50,15 @@ namespace StardewModdingAPI.ModBuildConfig [Required] public bool EnableModZip { get; set; } - /// <summary>Custom comma-separated regex patterns matching files to ignore when deploying or zipping the mod.</summary> + /// <summary>A comma-separated list of regex patterns matching files to ignore when deploying or zipping the mod.</summary> public string IgnoreModFilePatterns { get; set; } + /// <summary>A comma-separated list of relative file paths to ignore when deploying or zipping the mod.</summary> + public string IgnoreModFilePaths { get; set; } + + /// <summary>A comma-separated list of <see cref="ExtraAssemblyTypes"/> values which indicate which extra DLLs to bundle.</summary> + public string BundleExtraAssemblies { get; set; } + /********* ** Public methods @@ -69,11 +80,15 @@ namespace StardewModdingAPI.ModBuildConfig try { + // parse extra DLLs to bundle + ExtraAssemblyTypes bundleAssemblyTypes = this.GetExtraAssembliesToBundleOption(); + // parse ignore patterns + string[] ignoreFilePaths = this.GetCustomIgnoreFilePaths().ToArray(); Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray(); // get mod info - ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePatterns, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip); + ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip); // deploy mod files if (this.EnableModDeploy) @@ -134,6 +149,28 @@ namespace StardewModdingAPI.ModBuildConfig } } + /// <summary>Parse the extra assembly types which should be bundled with the mod.</summary> + private ExtraAssemblyTypes GetExtraAssembliesToBundleOption() + { + ExtraAssemblyTypes flags = ExtraAssemblyTypes.None; + + if (!string.IsNullOrWhiteSpace(this.BundleExtraAssemblies)) + { + foreach (string raw in this.BundleExtraAssemblies.Split(',')) + { + if (!Enum.TryParse(raw, out ExtraAssemblyTypes type)) + { + this.Log.LogWarning($"[mod build package] Ignored invalid <{nameof(this.BundleExtraAssemblies)}> value '{raw}', expected one of '{string.Join("', '", Enum.GetNames(typeof(ExtraAssemblyTypes)))}'."); + continue; + } + + flags |= type; + } + } + + return flags; + } + /// <summary>Get the custom ignore patterns provided by the user.</summary> private IEnumerable<Regex> GetCustomIgnorePatterns() { @@ -157,6 +194,29 @@ namespace StardewModdingAPI.ModBuildConfig } } + /// <summary>Get the custom relative file paths provided by the user to ignore.</summary> + private IEnumerable<string> GetCustomIgnoreFilePaths() + { + if (string.IsNullOrWhiteSpace(this.IgnoreModFilePaths)) + yield break; + + foreach (string raw in this.IgnoreModFilePaths.Split(',')) + { + string path; + try + { + path = PathUtilities.NormalizePath(raw); + } + catch (Exception ex) + { + this.Log.LogWarning($"[mod build package] Ignored invalid <{nameof(this.IgnoreModFilePaths)}> path {raw}:\n{ex}"); + continue; + } + + yield return path; + } + } + /// <summary>Copy the mod files into the game's mod folder.</summary> /// <param name="files">The files to include.</param> /// <param name="modFolderPath">The folder path to create with the mod files.</param> diff --git a/src/SMAPI.ModBuildConfig/Framework/ExtraAssemblyType.cs b/src/SMAPI.ModBuildConfig/Framework/ExtraAssemblyType.cs new file mode 100644 index 00000000..571bf7c7 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Framework/ExtraAssemblyType.cs @@ -0,0 +1,21 @@ +using System; + +namespace StardewModdingAPI.ModBuildConfig.Framework +{ + /// <summary>An extra assembly type for the <see cref="DeployModTask.BundleExtraAssemblies"/> field.</summary> + [Flags] + internal enum ExtraAssemblyTypes + { + /// <summary>Don't include extra assemblies.</summary> + None = 0, + + /// <summary>Assembly files which are part of MonoGame, SMAPI, or Stardew Valley.</summary> + Game = 1, + + /// <summary>Assembly files whose names start with <c>Microsoft.*</c> or <c>System.*</c>.</summary> + System = 2, + + /// <summary>Assembly files which don't match any other category.</summary> + ThirdParty = 4 + } +} diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 6dd595e5..ad4ffdf9 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -21,6 +21,45 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /// <summary>The files that are part of the package.</summary> private readonly IDictionary<string, FileInfo> Files; + /// <summary>The file extensions used by assembly files.</summary> + private readonly ISet<string> AssemblyFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + { + ".dll", + ".exe", + ".pdb", + ".xml" + }; + + /// <summary>The DLLs which match the <see cref="ExtraAssemblyTypes.Game"/> type.</summary> + private readonly ISet<string> GameDllNames = new HashSet<string> + { + // SMAPI + "0Harmony", + "Mono.Cecil", + "Mono.Cecil.Mdb", + "Mono.Cecil.Pdb", + "MonoMod.Common", + "Newtonsoft.Json", + "StardewModdingAPI", + "SMAPI.Toolkit", + "SMAPI.Toolkit.CoreInterfaces", + "TMXTile", + + // game + framework + "BmFont", + "FAudio-CS", + "GalaxyCSharp", + "GalaxyCSharpGlue", + "Lidgren.Network", + "MonoGame.Framework", + "SkiaSharp", + "Stardew Valley", + "StardewValley.GameData", + "Steamworks.NET", + "TextCopy", + "xTile" + }; + /********* ** Public methods @@ -28,10 +67,13 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /// <summary>Construct an instance.</summary> /// <param name="projectDir">The folder containing the project files.</param> /// <param name="targetDir">The folder containing the build output.</param> + /// <param name="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param> /// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param> + /// <param name="bundleAssemblyTypes">The extra assembly types which should be bundled with the mod.</param> + /// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param> /// <param name="validateRequiredModFiles">Whether to validate that required mod files like the manifest are present.</param> /// <exception cref="UserErrorException">The mod package isn't valid.</exception> - public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns, bool validateRequiredModFiles) + public ModFileManager(string projectDir, string targetDir, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, ExtraAssemblyTypes bundleAssemblyTypes, string modDllName, bool validateRequiredModFiles) { this.Files = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase); @@ -47,7 +89,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework string relativePath = entry.Item1; FileInfo file = entry.Item2; - if (!this.ShouldIgnore(file, relativePath, ignoreFilePatterns)) + if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName)) this.Files[relativePath] = file; } @@ -149,36 +191,72 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /// <summary>Get whether a build output file should be ignored.</summary> /// <param name="file">The file to check.</param> /// <param name="relativePath">The file's relative path in the package.</param> + /// <param name="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param> /// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param> - private bool ShouldIgnore(FileInfo file, string relativePath, Regex[] ignoreFilePatterns) + /// <param name="bundleAssemblyTypes">The extra assembly types which should be bundled with the mod.</param> + /// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param> + private bool ShouldIgnore(FileInfo file, string relativePath, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, ExtraAssemblyTypes bundleAssemblyTypes, string modDllName) { - return - // release zips - this.EqualsInvariant(file.Extension, ".zip") + // apply custom patterns + if (ignoreFilePaths.Any(p => p == relativePath) || ignoreFilePatterns.Any(p => p.IsMatch(relativePath))) + return true; + + // ignore unneeded files + { + bool shouldIgnore = + // release zips + this.EqualsInvariant(file.Extension, ".zip") + + // *.deps.json (only SMAPI's top-level one is used) + || file.Name.EndsWith(".deps.json") + + // code analysis files + || file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.OrdinalIgnoreCase) + || file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.OrdinalIgnoreCase) - // Harmony (bundled into SMAPI) - || this.EqualsInvariant(file.Name, "0Harmony.dll") + // translation class builder (not used at runtime) + || ( + file.Name.StartsWith("Pathoschild.Stardew.ModTranslationClassBuilder") + && this.AssemblyFileExtensions.Contains(file.Extension) + ) - // Json.NET (bundled into SMAPI) - || this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") - || this.EqualsInvariant(file.Name, "Newtonsoft.Json.pdb") - || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml") + // OS metadata files + || this.EqualsInvariant(file.Name, ".DS_Store") + || this.EqualsInvariant(file.Name, "Thumbs.db"); + if (shouldIgnore) + return true; + } + + // check for bundled assembly types + // When bundleAssemblyTypes is set, *all* dependencies are copied into the build output but only those which match the given assembly types should be bundled. + if (bundleAssemblyTypes != ExtraAssemblyTypes.None) + { + var type = this.GetExtraAssemblyType(file, modDllName); + if (type != ExtraAssemblyTypes.None && !bundleAssemblyTypes.HasFlag(type)) + return true; + } + + return false; + } + + /// <summary>Get the extra assembly type for a file, assuming that the user specified one or more extra types to bundle.</summary> + /// <param name="file">The file to check.</param> + /// <param name="modDllName">The name (without extension or path) for the current mod's DLL.</param> + private ExtraAssemblyTypes GetExtraAssemblyType(FileInfo file, string modDllName) + { + string baseName = Path.GetFileNameWithoutExtension(file.Name); + string extension = file.Extension; - // mod translation class builder (not used at runtime) - || this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.dll") - || this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.pdb") - || this.EqualsInvariant(file.Name, "Pathoschild.Stardew.ModTranslationClassBuilder.xml") + if (baseName == modDllName || !this.AssemblyFileExtensions.Contains(extension)) + return ExtraAssemblyTypes.None; - // code analysis files - || file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.OrdinalIgnoreCase) - || file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.OrdinalIgnoreCase) + if (this.GameDllNames.Contains(baseName)) + return ExtraAssemblyTypes.Game; - // OS metadata files - || this.EqualsInvariant(file.Name, ".DS_Store") - || this.EqualsInvariant(file.Name, "Thumbs.db") + if (baseName.StartsWith("System.", StringComparison.OrdinalIgnoreCase) || baseName.StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase)) + return ExtraAssemblyTypes.System; - // custom ignore patterns - || ignoreFilePatterns.Any(p => p.IsMatch(relativePath)); + return ExtraAssemblyTypes.ThirdParty; } /// <summary>Get whether a string is equal to another case-insensitively.</summary> diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index 93769650..0bc8c45e 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -2,28 +2,28 @@ <PropertyGroup> <!--build--> <RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace> - <TargetFramework>net452</TargetFramework> + <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>latest</LangVersion> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!--NuGet package--> <PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId> <Title>Build package for SMAPI mods</Title> - <Version>3.3.0</Version> + <Version>4.0.0</Version> <Authors>Pathoschild</Authors> - <Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later.</Description> + <Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description> <PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageIcon>images/icon.png</PackageIcon> <PackageProjectUrl>https://smapi.io/package/readme</PackageProjectUrl> <IncludeBuildOutput>false</IncludeBuildOutput> + + <!--copy dependency DLLs to bin folder so we can include them in package --> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <ItemGroup> - <Reference Include="Microsoft.Build" /> - <Reference Include="Microsoft.Build.Framework" /> - <Reference Include="Microsoft.Build.Utilities.v4.0" /> - <Reference Include="System.IO.Compression" /> - <Reference Include="System.Web.Extensions" /> + <PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" /> + <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 698765ad..b66ec27b 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -12,8 +12,8 @@ <DebugType>pdbonly</DebugType> <DebugSymbols>true</DebugSymbols> - <!-- recognise XNA Framework DLLs in the GAC (only affects mods using new csproj format) --> - <AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> + <!-- don't create the 'refs' folder (which isn't useful for mods) --> + <ProduceReferenceAssembly>false</ProduceReferenceAssembly> <!-- suppress processor architecture mismatch warning (mods should be compiled in 'Any CPU' so they work in both 32-bit and 64-bit mode) --> <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch> @@ -26,10 +26,10 @@ <EnableModZip Condition="'$(EnableModZip)' == ''">true</EnableModZip> <EnableHarmony Condition="'$(EnableHarmony)' == ''">false</EnableHarmony> <EnableGameDebugging Condition="'$(EnableGameDebugging)' == ''">true</EnableGameDebugging> - <CopyModReferencesToBuildOutput Condition="'$(CopyModReferencesToBuildOutput)' == '' OR ('$(CopyModReferencesToBuildOutput)' != 'true' AND '$(CopyModReferencesToBuildOutput)' != 'false')">false</CopyModReferencesToBuildOutput> + <BundleExtraAssemblies Condition="'$(BundleExtraAssemblies)' == ''"></BundleExtraAssemblies> - <GameFramework Condition="'$(GameFramework)' == '' AND '$(OS)' == 'Windows_NT'">Xna</GameFramework> - <GameFramework Condition="'$(GameFramework)' == ''">MonoGame</GameFramework> + <!-- coppy referenced DLLs into build output --> + <CopyLocalLockFileAssemblies Condition="$(BundleExtraAssemblies.Contains('ThirdParty')) OR $(BundleExtraAssemblies.Contains('Game')) OR $(BundleExtraAssemblies.Contains('System')) OR $(BundleExtraAssemblies.Contains('All'))">true</CopyLocalLockFileAssemblies> </PropertyGroup> <PropertyGroup Condition="'$(OS)' == 'Windows_NT' AND '$(EnableGameDebugging)' == 'true'"> @@ -43,38 +43,21 @@ <!--********************************************* ** Add assembly references **********************************************--> - <!-- common --> <ItemGroup> - <Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.exe" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(CopyModReferencesToBuildOutput)" /> + <!-- game --> + <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> + <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> + <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> + <Reference Include="xTile" HintPath="$(GamePath)\xTile.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> + + <!-- SMAPI --> + <Reference Include="StardewModdingAPI" HintPath="$(GamePath)\StardewModdingAPI.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> + <Reference Include="SMAPI.Toolkit.CoreInterfaces" HintPath="$(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> + + <!-- Harmony --> + <Reference Include="0Harmony" Condition="'$(EnableHarmony)' == 'true'" HintPath="$(GamePath)\smapi-internal\0Harmony.dll" Private="$(BundleExtraAssemblies.Contains('Game'))" /> </ItemGroup> - <!-- Windows only --> - <ItemGroup Condition="'$(OS)' == 'Windows_NT'"> - <Reference Include="Netcode" HintPath="$(GamePath)\Netcode.dll" Private="$(CopyModReferencesToBuildOutput)" /> - </ItemGroup> - - <!-- Game framework --> - <Choose> - <When Condition="'$(GameFramework)' == 'Xna'"> - <ItemGroup> - <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" /> - <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="$(CopyModReferencesToBuildOutput)" /> - </ItemGroup> - </When> - <Otherwise> - <ItemGroup> - <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="$(CopyModReferencesToBuildOutput)" /> - </ItemGroup> - </Otherwise> - </Choose> - <!--********************************************* ** Show validation messages @@ -85,8 +68,8 @@ <!-- invalid game path --> <Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see https://smapi.io/package/custom-game-path." ContinueOnError="false" /> - <Error Condition="!Exists('$(GamePath)\$(GameExecutableName).exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the $(GameExecutableName) file. If this folder is invalid, delete it and the package will autodetect another game install path." ContinueOnError="false" /> - <Error Condition="!Exists('$(GamePath)\StardewModdingAPI.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." ContinueOnError="false" /> + <Error Condition="!Exists('$(GamePath)\Stardew Valley.dll')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain the Stardew Valley file. If this folder is invalid, delete it and the package will autodetect another game install path." ContinueOnError="false" /> + <Error Condition="!Exists('$(GamePath)\StardewModdingAPI.dll')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." ContinueOnError="false" /> <!-- invalid target architecture (note: internal value is 'AnyCPU', value shown in Visual Studio is 'Any CPU') --> <Warning Condition="'$(Platform)' != 'AnyCPU'" Text="The target platform should be set to 'Any CPU' for compatibility with both 32-bit and 64-bit versions of Stardew Valley (currently set to '$(Platform)'). See https://smapi.io/package/wrong-processor-architecture for details." HelpLink="https://smapi.io/package/wrong-processor-architecture" /> @@ -98,6 +81,7 @@ **********************************************--> <Target Name="AfterBuild"> <DeployModTask + ModDllName="$(TargetName)" ModFolderName="$(ModFolderName)" ModZipPath="$(ModZipPath)" @@ -108,6 +92,9 @@ TargetDir="$(TargetDir)" GameModsDir="$(GameModsPath)" IgnoreModFilePatterns="$(IgnoreModFilePatterns)" + IgnoreModFilePaths="$(IgnoreModFilePaths)" + + BundleExtraAssemblies="$(BundleExtraAssemblies)" /> </Target> </Project> diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs new file mode 100644 index 00000000..6b3b27cd --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using StardewValley; +using StardewValley.GameData; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player +{ + /// <summary>A command which changes the player's farm type.</summary> + internal class SetFarmTypeCommand : ConsoleCommand + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public SetFarmTypeCommand() + : base("set_farm_type", "Sets the current player's farm type.\n\nUsage: set_farm_type <farm type>\n- farm type: the farm type to set. Enter `set_farm_type list` for a list of available farm types.") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!Context.IsWorldReady) + { + monitor.Log("You must load a save to use this command.", LogLevel.Error); + return; + } + + // parse arguments + if (!args.TryGet(0, "farm type", out string farmType)) + return; + bool isVanillaId = int.TryParse(farmType, out int vanillaId) && vanillaId is (>= 0 and < Farm.layout_max); + + // handle argument + if (farmType == "list") + this.HandleList(monitor); + else if (isVanillaId) + this.HandleVanillaFarmType(vanillaId, monitor); + else + this.HandleCustomFarmType(farmType, monitor); + } + + + /********* + ** Private methods + *********/ + /**** + ** Handlers + ****/ + /// <summary>Print a list of available farm types.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + private void HandleList(IMonitor monitor) + { + StringBuilder result = new(); + + // list vanilla types + result.AppendLine("The farm type can be one of these vanilla types:"); + foreach (var type in this.GetVanillaFarmTypes()) + result.AppendLine($" - {type.Key} ({type.Value})"); + result.AppendLine(); + + // list custom types + { + var customTypes = this.GetCustomFarmTypes(); + if (customTypes.Any()) + { + result.AppendLine("Or one of these custom farm types:"); + foreach (var type in customTypes.Values.OrderBy(p => p.ID)) + result.AppendLine($" - {type.ID} ({this.GetCustomName(type)})"); + } + else + result.AppendLine("Or a custom farm type (though none is loaded currently)."); + } + + // print + monitor.Log(result.ToString(), LogLevel.Info); + } + + /// <summary>Set a vanilla farm type.</summary> + /// <param name="type">The farm type.</param> + /// <param name="monitor">Writes messages to the console and log file.</param> + private void HandleVanillaFarmType(int type, IMonitor monitor) + { + if (Game1.whichFarm == type) + { + monitor.Log($"Your current farm is already set to {type} ({this.GetVanillaName(type)}).", LogLevel.Info); + return; + } + + this.SetFarmType(type, null); + this.PrintSuccess(monitor, $"{type} ({this.GetVanillaName(type)}"); + } + + /// <summary>Set a custom farm type.</summary> + /// <param name="id">The farm type ID.</param> + /// <param name="monitor">Writes messages to the console and log file.</param> + private void HandleCustomFarmType(string id, IMonitor monitor) + { + if (Game1.whichModFarm?.ID == id) + { + monitor.Log($"Your current farm is already set to {id} ({this.GetCustomName(Game1.whichModFarm)}).", LogLevel.Info); + return; + } + + if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType customFarmType)) + { + monitor.Log($"Invalid farm type '{id}'. Enter `help set_farm_type` for more info.", LogLevel.Error); + return; + } + + this.SetFarmType(Farm.mod_layout, customFarmType); + this.PrintSuccess(monitor, $"{id} ({this.GetCustomName(customFarmType)})"); + } + + /// <summary>Change the farm type.</summary> + /// <param name="type">The farm type ID.</param> + /// <param name="customFarmData">The custom farm type data, if applicable.</param> + private void SetFarmType(int type, ModFarmType customFarmData) + { + // set flags + Game1.whichFarm = type; + Game1.whichModFarm = customFarmData; + + // update farm map + Farm farm = Game1.getFarm(); + farm.mapPath.Value = $@"Maps\{Farm.getMapNameFromTypeInt(Game1.whichFarm)}"; + farm.reloadMap(); + + // clear spouse area cache to avoid errors + FieldInfo cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (cacheField == null) + throw new InvalidOperationException("Failed to access '_baseSpouseAreaTiles' field to clear spouse area cache."); + if (cacheField.GetValue(farm) is not IDictionary cache) + throw new InvalidOperationException($"The farm's '_baseSpouseAreaTiles' field didn't match the expected {nameof(IDictionary)} type."); + cache.Clear(); + } + + private void PrintSuccess(IMonitor monitor, string label) + { + StringBuilder result = new(); + result.AppendLine($"Your current farm has been converted to {label}. Saving and reloading is recommended to make sure everything is updated for the change."); + result.AppendLine(); + result.AppendLine("This doesn't move items that are out of bounds on the new map. If you need to clean up, you can..."); + result.AppendLine(" - temporarily switch back to the previous farm type;"); + result.AppendLine(" - or use a mod like Noclip Mode: https://www.nexusmods.com/stardewvalley/mods/3900 ;"); + result.AppendLine(" - or use the world_clear console command (enter `help world_clear` for details)."); + + monitor.Log(result.ToString(), LogLevel.Warn); + } + + /**** + ** Vanilla farm types + ****/ + /// <summary>Get the display name for a vanilla farm type.</summary> + /// <param name="type">The farm type.</param> + private string GetVanillaName(int type) + { + string translationKey = type switch + { + Farm.default_layout => "Character_FarmStandard", + Farm.riverlands_layout => "Character_FarmFishing", + Farm.forest_layout => "Character_FarmForaging", + Farm.mountains_layout => "Character_FarmMining", + Farm.combat_layout => "Character_FarmCombat", + Farm.fourCorners_layout => "Character_FarmFourCorners", + Farm.beach_layout => "Character_FarmBeach", + _ => null + }; + + return translationKey != null + ? Game1.content.LoadString(@$"Strings\UI:{translationKey}").Split('_')[0] + : type.ToString(); + } + + /// <summary>Get the available vanilla farm types by ID.</summary> + private IDictionary<int, string> GetVanillaFarmTypes() + { + IDictionary<int, string> farmTypes = new Dictionary<int, string>(); + + foreach (int id in Enumerable.Range(0, Farm.layout_max)) + farmTypes[id] = this.GetVanillaName(id); + + return farmTypes; + } + + /**** + ** Custom farm types + ****/ + /// <summary>Get the display name for a custom farm type.</summary> + /// <param name="farmType">The custom farm type.</param> + private string GetCustomName(ModFarmType farmType) + { + if (string.IsNullOrWhiteSpace(farmType?.TooltipStringPath)) + return farmType?.ID; + + return Game1.content.LoadString(farmType.TooltipStringPath)?.Split('_')[0] ?? farmType.ID; + } + + /// <summary>Get the available custom farm types by ID.</summary> + private IDictionary<string, ModFarmType> GetCustomFarmTypes() + { + IDictionary<string, ModFarmType> farmTypes = new Dictionary<string, ModFarmType>(StringComparer.OrdinalIgnoreCase); + + foreach (ModFarmType farmType in Game1.content.Load<List<ModFarmType>>("Data\\AdditionalFarms")) + { + if (string.IsNullOrWhiteSpace(farmType.ID)) + continue; + + farmTypes[farmType.ID] = farmType; + } + + return farmTypes; + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index 528348a0..e3db8b47 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <AssemblyName>ConsoleCommands</AssemblyName> <RootNamespace>StardewModdingAPI.Mods.ConsoleCommands</RootNamespace> - <TargetFramework>net452</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> </PropertyGroup> @@ -13,32 +13,12 @@ </ItemGroup> <ItemGroup> - <Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" /> + <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" /> + <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" /> <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" 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" /> - </ItemGroup> - - <!-- Game framework --> - <Choose> - <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))"> - <ItemGroup> - <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - </ItemGroup> - </When> - <Otherwise> - <ItemGroup> - <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" /> - </ItemGroup> - </Otherwise> - </Choose> - <ItemGroup> <None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 9af3ce5d..85653a7d 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.12.8", + "Version": "3.13.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.12.8" + "MinimumApiVersion": "3.13.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs index 6860a4ec..f243c6d1 100644 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs +++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs @@ -19,9 +19,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches public override void Apply(Harmony harmony, IMonitor monitor) { harmony.Patch( - original: Constants.GameFramework == GameFramework.Xna - ? this.RequireMethod<SpriteBatch>("InternalDraw") - : this.RequireMethod<SpriteBatch>("CheckValid", new[] { typeof(Texture2D) }), + original: this.RequireMethod<SpriteBatch>("CheckValid", new[] { typeof(Texture2D) }), postfix: this.GetHarmonyMethod(nameof(SpriteBatchPatcher.After_CheckValid)) ); } @@ -30,13 +28,8 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches /********* ** Private methods *********/ -#if SMAPI_FOR_XNA - /// <summary>The method to call after <see cref="SpriteBatch.InternalDraw"/>.</summary> - /// <param name="texture">The texture to validate.</param> -#else /// <summary>The method to call after <see cref="SpriteBatch.CheckValid"/>.</summary> /// <param name="texture">The texture to validate.</param> -#endif private static void After_CheckValid(Texture2D texture) { if (texture?.IsDisposed == true) diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index 182a978e..78cdb315 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <AssemblyName>ErrorHandler</AssemblyName> <RootNamespace>StardewModdingAPI.Mods.ErrorHandler</RootNamespace> - <TargetFramework>net452</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> </PropertyGroup> @@ -14,33 +14,12 @@ </ItemGroup> <ItemGroup> - <Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" /> + <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" /> + <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" /> <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" 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" /> - </ItemGroup> - - <!-- Game framework --> - <Choose> - <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))"> - <ItemGroup> - <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - </ItemGroup> - </When> - <Otherwise> - <ItemGroup> - <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" /> - </ItemGroup> - </Otherwise> - </Choose> - <ItemGroup> <None Update="i18n\*.json" CopyToOutputDirectory="PreserveNewest" /> <None Update="manifest.json" CopyToOutputDirectory="PreserveNewest" /> diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index cfbaa661..da8306d1 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.12.8", + "Version": "3.13.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.12.8" + "MinimumApiVersion": "3.13.0" } diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj index 079beb08..a8b0dfdb 100644 --- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <AssemblyName>SaveBackup</AssemblyName> <RootNamespace>StardewModdingAPI.Mods.SaveBackup</RootNamespace> - <TargetFramework>net452</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> </PropertyGroup> @@ -13,7 +13,7 @@ </ItemGroup> <ItemGroup> - <Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" /> + <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 1efd1d89..4c57bd49 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.12.8", + "Version": "3.13.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.12.8" + "MinimumApiVersion": "3.13.0" } diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 8f7bfab4..8329b2e1 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <AssemblyName>SMAPI.Tests</AssemblyName> <RootNamespace>SMAPI.Tests</RootNamespace> - <TargetFramework>net452</TargetFramework> + <TargetFramework>net5.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <LangVersion>latest</LangVersion> </PropertyGroup> @@ -16,16 +16,14 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="NUnit" Version="3.13.2" /> </ItemGroup> <ItemGroup> - <Reference Include="$(GameExecutableName)"> - <HintPath>$(GamePath)\$(GameExecutableName).exe</HintPath> - <Private>True</Private> - </Reference> + <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="True" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs index c18f47a5..ab4c2618 100644 --- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs @@ -182,18 +182,14 @@ namespace SMAPI.Tests.Utilities [TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))] public void NormalizeAssetName(SamplePath path) { - if (Path.IsPathRooted(path.OriginalPath) || path.OriginalPath.StartsWith("/") || path.OriginalPath.StartsWith("\\")) + if (Path.IsPathRooted(path.OriginalPath) || path.OriginalPath.StartsWith('/') || path.OriginalPath.StartsWith('\\')) Assert.Ignore("Absolute paths can't be used as asset names."); // act string normalized = PathUtilities.NormalizeAssetName(path.OriginalPath); // assert -#if SMAPI_FOR_WINDOWS - Assert.AreEqual(path.NormalizedOnWindows, normalized); -#else - Assert.AreEqual(path.NormalizedOnUnix, normalized); -#endif + Assert.AreEqual(path.NormalizedOnUnix, normalized); // MonoGame uses the Linux format } /**** diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj index 0e1e40b2..4c92b4db 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj +++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <RootNamespace>StardewModdingAPI</RootNamespace> <Description>Provides toolkit interfaces which are available to SMAPI mods.</Description> - <TargetFrameworks>net452;netstandard2.0</TargetFrameworks> + <TargetFrameworks>net5.0; netstandard2.0</TargetFrameworks> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/SMAPI.Toolkit/Framework/Constants.cs b/src/SMAPI.Toolkit/Framework/Constants.cs new file mode 100644 index 00000000..55f26582 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Constants.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI.Toolkit.Framework +{ + /// <summary>Contains the SMAPI installer's constants and assumptions.</summary> + internal static class Constants + { + /// <summary>The name of the game's main DLL, used to detect game folders.</summary> + public const string GameDllName = "Stardew Valley.dll"; + } +} diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index c90fc1d3..7553c07f 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -56,10 +56,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning { return dir.Exists - && ( - dir.EnumerateFiles("StardewValley.exe").Any() - || dir.EnumerateFiles("Stardew Valley.exe").Any() - ); + && dir.EnumerateFiles("Stardew Valley.dll").Any(); } diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index 2636aae0..8b6eb5fb 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -80,15 +80,6 @@ namespace StardewModdingAPI.Toolkit.Framework return name; } - /// <summary>Get the name of the Stardew Valley executable.</summary> - /// <param name="platform">The current platform.</param> - public static string GetExecutableName(string platform) - { - return platform == nameof(Platform.Windows) - ? "Stardew Valley.exe" - : "StardewValley.exe"; - } - /// <summary>Get whether an executable is 64-bit.</summary> /// <param name="path">The absolute path to the assembly file.</param> public static bool Is64BitAssembly(string path) diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 3d87c169..ec27bf79 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <RootNamespace>StardewModdingAPI.Toolkit</RootNamespace> <Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description> - <TargetFrameworks>net452;netstandard2.0</TargetFrameworks> + <TargetFrameworks>net5.0; netstandard2.0</TargetFrameworks> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -12,8 +12,8 @@ <PackageReference Include="HtmlAgilityPack" Version="1.11.33" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <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'" /> + <PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> + <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs index 6de79a85..7536337a 100644 --- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs @@ -40,13 +40,6 @@ namespace StardewModdingAPI.Toolkit.Utilities return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString()); } - /// <summary>Get the name of the Stardew Valley executable.</summary> - /// <param name="platform">The current platform.</param> - public static string GetExecutableName(Platform platform) - { - return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString()); - } - /// <summary>Get whether an executable is 64-bit.</summary> /// <param name="path">The absolute path to the assembly file.</param> public static bool Is64BitAssembly(string path) diff --git a/src/SMAPI.Toolkit/Utilities/FileUtilities.cs b/src/SMAPI.Toolkit/Utilities/FileUtilities.cs index 7856fdb1..a6bf5929 100644 --- a/src/SMAPI.Toolkit/Utilities/FileUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/FileUtilities.cs @@ -1,4 +1,6 @@ +using System; using System.IO; +using System.Security.Cryptography; using System.Threading; namespace StardewModdingAPI.Toolkit.Utilities @@ -42,5 +44,16 @@ namespace StardewModdingAPI.Toolkit.Utilities if (entry.Exists) throw new IOException($"Timed out trying to delete {entry.FullName}"); } + + /// <summary>Get the MD5 hash for a file.</summary> + /// <param name="absolutePath">The absolute file path.</param> + public static string GetFileHash(string absolutePath) + { + using FileStream stream = File.OpenRead(absolutePath); + using MD5 md5 = MD5.Create(); + + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } } } diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 020ebc6d..2e9e5eac 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; using System.Linq; @@ -27,7 +26,7 @@ namespace StardewModdingAPI.Toolkit.Utilities public static readonly char PreferredPathSeparator = Path.DirectorySeparatorChar; /// <summary>The preferred directory separator character in an asset key.</summary> - public static readonly char PreferredAssetSeparator = PathUtilities.PreferredPathSeparator; + public static readonly char PreferredAssetSeparator = '/'; /********* @@ -88,14 +87,18 @@ namespace StardewModdingAPI.Toolkit.Utilities /// <summary>Get a directory or file path relative to a given source path. If no relative path is possible (e.g. the paths are on different drives), an absolute path is returned.</summary> /// <param name="sourceDir">The source folder path.</param> /// <param name="targetPath">The target folder or file path.</param> - /// <remarks> - /// - /// NOTE: this is a heuristic implementation that works in the cases SMAPI needs it for, but it doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between UNC paths on Windows). This should be replaced with the more comprehensive <c>Path.GetRelativePath</c> if the game ever migrates to .NET Core. - /// - /// </remarks> [Pure] public static string GetRelativePath(string sourceDir, string targetPath) { +#if NET5_0 + return Path.GetRelativePath(sourceDir, targetPath); +#else + // NOTE: + // this is a heuristic implementation that works in the cases SMAPI needs it for, but it + // doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between + // UNC paths on Windows). SMAPI and mods will use the more robust .NET 5 version anyway + // though, this is only for compatibility with the mod build package. + // convert to URIs Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); @@ -123,6 +126,7 @@ namespace StardewModdingAPI.Toolkit.Utilities } return relative; +#endif } /// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary> @@ -145,32 +149,5 @@ namespace StardewModdingAPI.Toolkit.Utilities { return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); } - - /// <summary>Get the paths which exceed the OS length limit.</summary> - /// <param name="rootPath">The root path to search.</param> - internal static IEnumerable<string> GetTooLongPaths(string rootPath) - { - if (!Directory.Exists(rootPath)) - return new string[0]; - - return Directory - .EnumerateFileSystemEntries(rootPath, "*.*", SearchOption.AllDirectories) - .Where(PathUtilities.IsPathTooLong); - } - - /// <summary>Get whether a file or directory path exceeds the OS path length limit.</summary> - /// <param name="path">The path to test.</param> - internal static bool IsPathTooLong(string path) - { - try - { - _ = Path.GetFullPath(path); - return false; - } - catch (PathTooLongException) - { - return true; - } - } } } diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 080285ab..f2f4c342 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -57,21 +57,16 @@ namespace StardewModdingAPI.Web.Controllers { // choose versions ReleaseVersion[] versions = await this.GetReleaseVersionsAsync(); - ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsBeta && !version.IsForDevs); - ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => !version.IsBeta && version.IsForDevs); - ReleaseVersion betaVersion = versions.LastOrDefault(version => version.IsBeta && !version.IsForDevs); - ReleaseVersion betaVersionForDevs = versions.LastOrDefault(version => version.IsBeta && version.IsForDevs); + ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsForDevs); + ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs); // render view IndexVersionModel stableVersionModel = stableVersion != null ? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl) - : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong) - IndexVersionModel betaVersionModel = betaVersion != null && this.SiteConfig.BetaEnabled - ? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl) - : null; + : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong // render view - var model = new IndexModel(stableVersionModel, betaVersionModel, this.SiteConfig.BetaBlurb, this.SiteConfig.SupporterList); + var model = new IndexModel(stableVersionModel, this.SiteConfig.OtherBlurb, this.SiteConfig.SupporterList); return this.View(model); } @@ -93,27 +88,12 @@ namespace StardewModdingAPI.Web.Controllers { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - // get latest release (whether preview or stable) - GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); + // get latest stable release + GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); - // split stable/prerelease if applicable - GitRelease betaRelease = null; - if (stableRelease.IsPrerelease) + // strip 'noinclude' blocks from release description + if (release != null) { - GitRelease result = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); - if (result != null) - { - betaRelease = stableRelease; - stableRelease = result; - } - } - - // strip 'noinclude' blocks from release descriptions - foreach (GitRelease release in new[] { stableRelease, betaRelease }) - { - if (release == null) - continue; - HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(release.Body); foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? new HtmlNode[0]) @@ -122,10 +102,8 @@ namespace StardewModdingAPI.Web.Controllers } // get versions - ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray(); - ReleaseVersion[] betaVersions = this.ParseReleaseVersions(betaRelease).ToArray(); - return stableVersions - .Concat(betaVersions) + return this + .ParseReleaseVersions(release) .OrderBy(p => p.Version) .ToArray(); }); @@ -146,10 +124,9 @@ namespace StardewModdingAPI.Web.Controllers Match match = Regex.Match(asset.FileName, @"SMAPI-(?<version>[\d\.]+(?:-.+)?)-installer(?<forDevs>-for-developers)?.zip"); if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) continue; - bool isBeta = version.IsPrerelease(); bool isForDevs = match.Groups["forDevs"].Success; - yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs); + yield return new ReleaseVersion(release, asset, version, isForDevs); } } @@ -168,9 +145,6 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>The SMAPI version.</summary> public ISemanticVersion Version { get; } - /// <summary>Whether this is a beta download.</summary> - public bool IsBeta { get; } - /// <summary>Whether this is a 'for developers' download.</summary> public bool IsForDevs { get; } @@ -182,14 +156,12 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="release">The underlying GitHub release.</param> /// <param name="asset">The underlying download asset.</param> /// <param name="version">The SMAPI version.</param> - /// <param name="isBeta">Whether this is a beta download.</param> /// <param name="isForDevs">Whether this is a 'for developers' download.</param> - public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isBeta, bool isForDevs) + public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isForDevs) { this.Release = release; this.Asset = asset; this.Version = version; - this.IsBeta = isBeta; this.IsForDevs = isForDevs; } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index dcddaf10..37d763cc 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -81,6 +81,8 @@ namespace StardewModdingAPI.Web.Controllers if (model?.Mods == null) return new ModEntryModel[0]; + ModUpdateCheckConfig config = this.Config.Value; + // fetch wiki data WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray(); IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase); @@ -89,6 +91,11 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; + // special case: if this is an update check for the official SMAPI repo, check the Nexus mod page for beta versions + if (mod.ID == config.SmapiInfo.ID && mod.UpdateKeys?.Any(key => key == config.SmapiInfo.DefaultUpdateKey) == true && mod.InstalledVersion?.IsPrerelease() == true) + mod.UpdateKeys = mod.UpdateKeys.Concat(config.SmapiInfo.AddBetaUpdateKeys).ToArray(); + + // fetch result ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion); if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) { diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index bd58dba0..aea695b8 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -14,5 +14,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// <summary>Update-check metadata to override.</summary> public ModOverrideConfig[] ModOverrides { get; set; } + + /// <summary>The update-check config for SMAPI's own update checks.</summary> + public SmapiInfoConfig SmapiInfo { get; set; } } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs index 43969f51..664dbef3 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs @@ -6,11 +6,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /********* ** Accessors *********/ - /// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary> - public bool BetaEnabled { get; set; } - - /// <summary>A short sentence shown under the beta download button, if any.</summary> - public string BetaBlurb { get; set; } + /// <summary>A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</summary> + public string OtherBlurb { get; set; } /// <summary>A list of supports to credit on the main page, in Markdown format.</summary> public string SupporterList { get; set; } diff --git a/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs new file mode 100644 index 00000000..d69fabb3 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// <summary>The update-check config for SMAPI's own update checks.</summary> + internal class SmapiInfoConfig + { + /// <summary>The mod ID used for SMAPI update checks.</summary> + public string ID { get; set; } + + /// <summary>The default update key used for SMAPI update checks.</summary> + public string DefaultUpdateKey { get; set; } + + /// <summary>The update keys to add for SMAPI update checks when the player has a beta version installed.</summary> + public string[] AddBetaUpdateKeys { get; set; } + } +} diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 4c8465a6..f1400e62 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -2,27 +2,29 @@ <PropertyGroup> <AssemblyName>SMAPI.Web</AssemblyName> <RootNamespace>StardewModdingAPI.Web</RootNamespace> - <TargetFramework>net5.0</TargetFramework> + <TargetFramework>net6.0</TargetFramework> <LangVersion>latest</LangVersion> </PropertyGroup> <Import Project="..\..\build\common.targets" /> <ItemGroup> + <None Remove="Properties\PublishProfiles\**" /> + <None Remove="Properties\ServiceDependencies\**" /> <Content Remove="aws-beanstalk-tools-defaults.json" /> </ItemGroup> <ItemGroup> - <PackageReference Include="Azure.Storage.Blobs" Version="12.8.3" /> - <PackageReference Include="Hangfire.AspNetCore" Version="1.7.22" /> + <PackageReference Include="Azure.Storage.Blobs" Version="12.10.0" /> + <PackageReference Include="Hangfire.AspNetCore" Version="1.7.27" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.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="Humanizer.Core" Version="2.13.14" /> + <PackageReference Include="JetBrains.Annotations" Version="2021.3.0" /> + <PackageReference Include="Markdig" Version="0.26.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> - <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.2" /> + <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.5" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.0" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Web/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs index 450fdc0e..d8d2d27f 100644 --- a/src/SMAPI.Web/ViewModels/IndexModel.cs +++ b/src/SMAPI.Web/ViewModels/IndexModel.cs @@ -9,11 +9,8 @@ namespace StardewModdingAPI.Web.ViewModels /// <summary>The latest stable SMAPI version.</summary> public IndexVersionModel StableVersion { get; set; } - /// <summary>The latest prerelease SMAPI version (if newer than <see cref="StableVersion"/>).</summary> - public IndexVersionModel BetaVersion { get; set; } - - /// <summary>A short sentence shown under the beta download button, if any.</summary> - public string BetaBlurb { get; set; } + /// <summary>A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</summary> + public string OtherBlurb { get; set; } /// <summary>A list of supports to credit on the main page, in Markdown format.</summary> public string SupporterList { get; set; } @@ -27,14 +24,12 @@ namespace StardewModdingAPI.Web.ViewModels /// <summary>Construct an instance.</summary> /// <param name="stableVersion">The latest stable SMAPI version.</param> - /// <param name="betaVersion">The latest prerelease SMAPI version (if newer than <paramref name="stableVersion"/>).</param> - /// <param name="betaBlurb">A short sentence shown under the beta download button, if any.</param> + /// <param name="otherBlurb">A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</param> /// <param name="supporterList">A list of supports to credit on the main page, in Markdown format.</param> - internal IndexModel(IndexVersionModel stableVersion, IndexVersionModel betaVersion, string betaBlurb, string supporterList) + internal IndexModel(IndexVersionModel stableVersion, string otherBlurb, string supporterList) { this.StableVersion = stableVersion; - this.BetaVersion = betaVersion; - this.BetaBlurb = betaBlurb; + this.OtherBlurb = otherBlurb; this.SupporterList = supporterList; } } diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 9d6e4bed..669cfd99 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -8,9 +8,9 @@ ViewData["ViewTitle"] = string.Empty; } @section Head { - <link rel="stylesheet" href="~/Content/css/index.css?r=20200105" /> + <link rel="stylesheet" href="~/Content/css/index.css" /> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script> - <script src="~/Content/js/index.js?r=20200105"></script> + <script src="~/Content/js/index.js"></script> } <h1> @@ -29,25 +29,12 @@ <a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a> <a href="@Model.StableVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a> </div> - </div><br /> - - @if (Model.BetaVersion != null) + </div> + <div><a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a></div> + @if (Model.OtherBlurb != null) { - <div class="cta-dropdown secondary-cta-dropdown"> - <a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta download"> - Download SMAPI @Model.BetaVersion.Version - @if (!string.IsNullOrWhiteSpace(Model.BetaBlurb)) - { - <br /><small>@Model.BetaBlurb</small> - } - </a><br /> - <div class="dropdown-content"> - <a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a> - <a href="@Model.BetaVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a> - </div> - </div><br /> + <div>@Html.Raw(Markdig.Markdown.ToHtml(Model.OtherBlurb))</div> } - <div><a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a></div> </div> <div class="area"> @@ -61,29 +48,11 @@ </div> <div class="area"> - @if (Model.BetaVersion == null) - { - <h2 id="whatsnew">What's new</h2> - <div class="github-description"> - @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) - </div> - <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> - } - else - { - <h2 id="whatsnew">What's new in...</h2> - <h3>SMAPI @Model.StableVersion.Version?</h3> - <div class="github-description"> - @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) - </div> - <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> - - <h3>SMAPI @Model.BetaVersion.Version?</h3> - <div class="github-description"> - @Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description)) - </div> - <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> - } + <h2 id="whatsnew">What's new</h2> + <div class="github-description"> + @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) + </div> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> </div> <div class="area"> @@ -122,10 +91,6 @@ <h2 id="modcreators">For mod creators</h2> <ul> <li><a href="@Model.StableVersion.DevDownloadUrl">SMAPI @Model.StableVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li> - @if (Model.BetaVersion != null) - { - <li><a href="@Model.BetaVersion.DevDownloadUrl">SMAPI @Model.BetaVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li> - } <li><a href="https://stardewvalleywiki.com/Modding:Index">Modding documentation</a></li> <li><a href="https://github.com/Pathoschild/SMAPI">Source code</a></li> </ul> diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 22fd7396..0265a928 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -17,8 +17,7 @@ "Site": { "BetaEnabled": false, - "BetaBlurb": null, - "SupporterList": null + "OtherBlurb": null }, "ApiClients": { @@ -76,6 +75,11 @@ "ID": "MartyrPher.SMAPI-Android-Installer", "AllowNonStandardVersions": true } - ] + ], + "SmapiInfo": { + "ID": "Pathoschild.SMAPI", + "DefaultUpdateKey": "GitHub:Pathoschild/SMAPI", + "AddBetaUpdateKeys": [ "Nexus:2400" ] + } } } diff --git a/src/SMAPI.Web/wwwroot/Content/css/index.css b/src/SMAPI.Web/wwwroot/Content/css/index.css index 1cf8d261..150ccc0a 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/index.css +++ b/src/SMAPI.Web/wwwroot/Content/css/index.css @@ -97,6 +97,10 @@ h1 { display: block; } +.cta-blurb { + font-size: 0.85em; +} + .sublinks { font-size: 0.9em; margin-bottom: 1em; diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index dcdd6298..bb166017 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -168,6 +168,60 @@ }, /********* + ** Broke in SDV 1.5.5 + *********/ + "Animated Portrait Framework": { + "ID": "akai.AnimatedPortrait", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "requires the 'System.Windows.Forms' API, which isn't available in .NET 5" + }, + "Audio Devices": { + "ID": "maxvollmer.audiodevices", + "~3.0.1 | Status": "AssumeBroken", + "~3.0.1 | StatusReasonDetails": "fails to load due to an outdated implementation of the game's 'IAudioEngine' interface" + }, + "Battery Warning": { + "ID": "Husky110.BatteryWarningMod", + "~1.0.4 | Status": "AssumeBroken", + "~1.0.4 | StatusReasonDetails": "requires the 'System.Management' API, which is a different DLL in .NET 5" + }, + "Junimo Studio": { + "ID": "Becks723.JunimoStudio", + "~2.0.1 | Status": "AssumeBroken", + "~2.0.1 | StatusReasonDetails": "requires 'Microsoft.Xna.Framework.Audio.AudioCategory' which doesn't exist in MonoGame" + }, + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "~1.9.1 | Status": "AssumeBroken", + "~1.9.1 | StatusReasonDetails": "causes freeze during game launch" + }, + "Stardew Hack": { + "ID": "bcmpinc.StardewHack", + "~5.1.0 | Status": "AssumeBroken", + "~5.1.0 | StatusReasonDetails": "runtime error when initializing due to an API change between .NET Framework and .NET 5" + }, + "Stardew Valley Expanded": { + "ID": "FlashShifter.SVECode", + "~1.13.11 | Status": "AssumeBroken", + "~1.13.11 | StatusReasonDetails": "fails to load due to an outdated implementation of the game's 'ICue' interface" + }, + "Stardew Web": { + "ID": "prism99.stardewweb", + "~0.7.2 | Status": "AssumeBroken", + "~0.7.2 | StatusReasonDetails": "requires the 'System.Drawing' API, which isn't available in .NET 5" + }, + "Sundrop City": { + "ID": "SundropTeam.SundropCity", + "~0.4.1 | Status": "AssumeBroken", + "~0.4.1 | StatusReasonDetails": "causes freeze during game launch" + }, + "Video Player": { + "ID": "aedenthorn.VideoPlayer", + "~0.2.5 | Status": "AssumeBroken", + "~0.2.5 | StatusReasonDetails": "requires an XNA Framework API that's not available in MonoGame and causes a crash to desktop" + }, + + /********* ** Broke in SMAPI 3.12.0 *********/ "Always Scroll Map": { @@ -205,11 +259,6 @@ "~1.9.3 | Status": "AssumeBroken", "~1.9.3 | StatusReasonDetails": "fails to load with 'ReflectionTypeLoadException' error" }, - "Stardew Hack": { - "ID": "bcmpinc.StardewHack", - "~5.0.0 | Status": "AssumeBroken", - "~5.0.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" - }, "Tilled Soil Decay": { "ID": "bcmpinc.TilledSoilDecay", "~4.1.0 | Status": "AssumeBroken", @@ -238,12 +287,6 @@ /********* ** Broke in SDV 1.5 (SMAPI mods) *********/ - "Audio Devices": { - "ID": "maxvollmer.audiodevices", - "~2.0.0 | Status": "AssumeBroken", - "~2.0.0 | StatusReasonDetails": "causes crash to desktop when starting the game" - }, - "ChestEx": { "ID": "berkayylmao.ChestEx", "~1.3.4 | Status": "AssumeBroken", diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index aac4ff0f..d39574ef 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -12,11 +12,11 @@ "properties": { "Format": { "title": "Format version", - "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", + "description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.", "type": "string", - "const": "1.23.0", + "const": "1.24.0", "@errorMessages": { - "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.23.0'." + "const": "Incorrect value '@value'. You should always use the latest format version (currently 1.24.0) to enable the latest features, avoid obsolete behavior, and reduce load times." } }, "ConfigSchema": { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 42c3b21b..cf4bb677 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -40,21 +40,16 @@ namespace StardewModdingAPI internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform()); /// <summary>The game framework running the game.</summary> - internal static GameFramework GameFramework { get; } = -#if SMAPI_FOR_XNA - GameFramework.Xna; -#else - GameFramework.MonoGame; -#endif + internal static GameFramework GameFramework { get; } = GameFramework.MonoGame; /// <summary>The game's assembly name.</summary> - internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley"; + internal static string GameAssemblyName { get; } = "Stardew Valley"; /// <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.12.8"; + internal static string RawApiVersion = "3.13.0"; } /// <summary>Contains SMAPI's constants and assumptions.</summary> @@ -70,10 +65,10 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion); /// <summary>The minimum supported version of Stardew Valley.</summary> - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.5"); /// <summary>The maximum supported version of Stardew Valley.</summary> - public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.5.4"); + public static ISemanticVersion MaximumGameVersion { get; } = null; /// <summary>The target game platform.</summary> public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform; @@ -240,18 +235,13 @@ namespace StardewModdingAPI // The game assembly can have one of three names depending how the mod was compiled: // - 'StardewValley': assembly name on Linux/macOS; // - 'Stardew Valley': assembly name on Windows; - // - 'Netcode': an assembly that's separate on Windows only. - resolver.AddWithExplicitNames(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley" -#if !SMAPI_FOR_WINDOWS - , "Netcode" -#endif - ); + // - 'Netcode': an assembly that was separate on Windows only before Stardew Valley 1.5.5. + resolver.AddWithExplicitNames(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley", "Netcode"); } /// <summary>Get metadata for mapping assemblies to the current platform.</summary> /// <param name="targetPlatform">The target game platform.</param> - /// <param name="framework">The game framework running the game.</param> - internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform, GameFramework framework) + internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) { var removeAssemblyReferences = new List<string>(); var targetAssemblies = new List<Assembly>(); @@ -260,61 +250,26 @@ namespace StardewModdingAPI removeAssemblyReferences.Add("StardewModdingAPI.Toolkit.CoreInterfaces"); targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly); - // get changes for platform - if (Constants.Platform != Platform.Windows) - { - removeAssemblyReferences.AddRange(new[] - { - "Netcode", - "Stardew Valley" - }); - targetAssemblies.Add( - typeof(StardewValley.Game1).Assembly // note: includes Netcode types on Linux/macOS - ); - } - else + // XNA Framework before Stardew Valley 1.5.5 + removeAssemblyReferences.AddRange(new[] { - removeAssemblyReferences.Add( - "StardewValley" - ); - targetAssemblies.AddRange(new[] - { - typeof(Netcode.NetBool).Assembly, - typeof(StardewValley.Game1).Assembly - }); - } + "Microsoft.Xna.Framework", + "Microsoft.Xna.Framework.Game", + "Microsoft.Xna.Framework.Graphics", + "Microsoft.Xna.Framework.Xact" + }); + targetAssemblies.Add( + typeof(Microsoft.Xna.Framework.Vector2).Assembly + ); - // get changes for game framework - switch (framework) - { - case GameFramework.MonoGame: - removeAssemblyReferences.AddRange(new[] - { - "Microsoft.Xna.Framework", - "Microsoft.Xna.Framework.Game", - "Microsoft.Xna.Framework.Graphics", - "Microsoft.Xna.Framework.Xact" - }); - targetAssemblies.Add( - typeof(Microsoft.Xna.Framework.Vector2).Assembly - ); - break; - - case GameFramework.Xna: - removeAssemblyReferences.Add( - "MonoGame.Framework" - ); - targetAssemblies.AddRange(new[] - { - typeof(Microsoft.Xna.Framework.Vector2).Assembly, - typeof(Microsoft.Xna.Framework.Game).Assembly, - typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly - }); - break; + // `Netcode.dll` merged into the game assembly in Stardew Valley 1.5.5 + removeAssemblyReferences.Add( + "Netcode" + ); - default: - throw new InvalidOperationException($"Unknown game framework '{framework}'."); - } + // Stardew Valley reference + removeAssemblyReferences.Add("StardewValley"); + targetAssemblies.Add(typeof(StardewValley.Game1).Assembly); return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray()); } diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index 4f810948..0a5fa7e7 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; using xTile; using xTile.Layers; using xTile.Tiles; @@ -25,18 +26,17 @@ namespace StardewModdingAPI.Framework.Content : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// <inheritdoc /> - /// <remarks>Derived from <see cref="StardewValley.GameLocation.ApplyMapOverride"/> with a few changes: + /// <remarks>Derived from <see cref="GameLocation.ApplyMapOverride(Map,string,Rectangle?,Rectangle?)"/> with a few changes: /// - can be applied directly to the maps when loading, before the location is created; - /// - added support for source/target areas; + /// - added support for patch modes (overlay, replace by layer, or fully replace); /// - added disambiguation if source has a modified version of the same tilesheet, instead of copying tiles into the target tilesheet; - /// - changed to always overwrite tiles within the target area (to avoid edge cases where some tiles are only partly applied); /// - fixed copying tilesheets (avoid "The specified TileSheet was not created for use with this map" error); /// - fixed tilesheets not added at the end (via z_ prefix), which can cause crashes in game code which depends on hardcoded tilesheet indexes; /// - fixed issue where different tilesheets are linked by ID. /// </remarks> - public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null) + public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay) { - var target = this.Data; + Map target = this.Data; // get areas { @@ -84,10 +84,13 @@ namespace StardewModdingAPI.Framework.Content tilesheetMap[sourceSheet] = targetSheet; } - // get layer map - IDictionary<Layer, Layer> layerMap = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id)); + // get target layers + IDictionary<Layer, Layer> sourceToTargetLayers = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id)); + HashSet<Layer> orphanedTargetLayers = new HashSet<Layer>(target.Layers.Except(sourceToTargetLayers.Values)); // apply tiles + bool replaceAll = patchMode == PatchMapMode.Replace; + bool replaceByLayer = patchMode == PatchMapMode.ReplaceByLayer; for (int x = 0; x < sourceArea.Value.Width; x++) { for (int y = 0; y < sourceArea.Value.Height; y++) @@ -96,47 +99,37 @@ namespace StardewModdingAPI.Framework.Content Point sourcePos = new Point(sourceArea.Value.X + x, sourceArea.Value.Y + y); Point targetPos = new Point(targetArea.Value.X + x, targetArea.Value.Y + y); + // replace tiles on target-only layers + if (replaceAll) + { + foreach (Layer targetLayer in orphanedTargetLayers) + targetLayer.Tiles[targetPos.X, targetPos.Y] = null; + } + // merge layers foreach (Layer sourceLayer in source.Layers) { // get layer - Layer targetLayer = layerMap[sourceLayer]; + Layer targetLayer = sourceToTargetLayers[sourceLayer]; if (targetLayer == null) { target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize)); - layerMap[sourceLayer] = target.GetLayer(sourceLayer.Id); + sourceToTargetLayers[sourceLayer] = target.GetLayer(sourceLayer.Id); } // copy layer properties targetLayer.Properties.CopyFrom(sourceLayer.Properties); - // copy tiles + // create new tile Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y]; - Tile targetTile; - switch (sourceTile) - { - case StaticTile _: - targetTile = new StaticTile(targetLayer, tilesheetMap[sourceTile.TileSheet], sourceTile.BlendMode, sourceTile.TileIndex); - break; - - case AnimatedTile animatedTile: - { - StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length]; - for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame) - { - StaticTile frameTile = animatedTile.TileFrames[frame]; - tileFrames[frame] = new StaticTile(targetLayer, tilesheetMap[frameTile.TileSheet], frameTile.BlendMode, frameTile.TileIndex); - } - targetTile = new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval); - } - break; - - default: // null or unhandled type - targetTile = null; - break; - } - targetTile?.Properties.CopyFrom(sourceTile.Properties); - targetLayer.Tiles[targetPos.X, targetPos.Y] = targetTile; + Tile newTile = sourceTile != null + ? this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet]) + : null; + newTile?.Properties.CopyFrom(sourceTile.Properties); + + // replace tile + if (newTile != null || replaceByLayer || replaceAll) + targetLayer.Tiles[targetPos.X, targetPos.Y] = newTile; } } } @@ -146,6 +139,33 @@ namespace StardewModdingAPI.Framework.Content /********* ** Private methods *********/ + /// <summary>Create a new tile for the target map.</summary> + /// <param name="sourceTile">The source tile to copy.</param> + /// <param name="targetLayer">The target layer.</param> + /// <param name="targetSheet">The target tilesheet.</param> + private Tile CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet) + { + switch (sourceTile) + { + case StaticTile _: + return new StaticTile(targetLayer, targetSheet, sourceTile.BlendMode, sourceTile.TileIndex); + + case AnimatedTile animatedTile: + { + StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length]; + for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame) + { + StaticTile frameTile = animatedTile.TileFrames[frame]; + tileFrames[frame] = new StaticTile(targetLayer, targetSheet, frameTile.BlendMode, frameTile.TileIndex); + } + + return new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval); + } + + default: // null or unhandled type + return null; + } + } /// <summary>Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path.</summary> /// <param name="path">The path to normalize.</param> private string NormalizeTilesheetPathForComparison(string path) diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 7edc9ab9..8e0c6228 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; -using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -18,9 +17,6 @@ namespace StardewModdingAPI.Framework.Content /// <summary>The underlying asset cache.</summary> private readonly IDictionary<string, object> Cache; - /// <summary>Applies platform-specific asset key normalization so it's consistent with the underlying cache.</summary> - private readonly Func<string, string> NormalizeAssetNameForPlatform; - /********* ** Accessors @@ -48,17 +44,7 @@ namespace StardewModdingAPI.Framework.Content /// <param name="reflection">Simplifies access to private game code.</param> public ContentCache(LocalizedContentManager contentManager, Reflector reflection) { - // init this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue(); - - // get key normalization logic - if (Constants.GameFramework == GameFramework.Xna) - { - IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); - this.NormalizeAssetNameForPlatform = path => method.Invoke<string>(path); - } - else - this.NormalizeAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic } /**** @@ -75,23 +61,24 @@ namespace StardewModdingAPI.Framework.Content /**** ** Normalize ****/ - /// <summary>Normalize path separators in a file path. For asset keys, see <see cref="NormalizeKey"/> instead.</summary> + /// <summary>Normalize path separators in an asset name.</summary> /// <param name="path">The file path to normalize.</param> [Pure] public string NormalizePathSeparators(string path) { - return PathUtilities.NormalizePath(path); + return PathUtilities.NormalizeAssetName(path); } /// <summary>Normalize a cache key so it's consistent with the underlying cache.</summary> /// <param name="key">The asset key.</param> + /// <remarks>This is equivalent to <see cref="NormalizePathSeparators"/> with added file extension logic.</remarks> [Pure] public string NormalizeKey(string key) { key = this.NormalizePathSeparators(key); return key.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase) ? key.Substring(0, key.Length - 4) - : this.NormalizeAssetNameForPlatform(key); + : key; } /**** diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index d0e759c2..b6f1669a 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -64,6 +64,9 @@ namespace StardewModdingAPI.Framework /// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary> private readonly LocalizedContentManager VanillaContentManager; + /// <summary>The language enum values indexed by locale code.</summary> + private Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes; + /********* ** Accessors @@ -133,6 +136,7 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Add(contentManagerForAssetPropagation); this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory); this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, aggressiveMemoryOptimizations); + this.LocaleCodes = new Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>>(this.GetLocaleCodes); } /// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary> @@ -195,6 +199,10 @@ namespace StardewModdingAPI.Framework /// <summary>Perform any cleanup needed when the locale changes.</summary> public void OnLocaleChanged() { + // rebuild locale cache (which may change due to custom mod languages) + this.LocaleCodes = new Lazy<IDictionary<string, LocalizedContentManager.LanguageCode>>(this.GetLocaleCodes); + + // reload affected content this.ContentManagerLock.InReadLock(() => { foreach (IContentManager contentManager in this.ContentManagers) @@ -408,6 +416,25 @@ namespace StardewModdingAPI.Framework return tilesheets ?? new TilesheetReference[0]; } + /// <summary>Get the language enum which corresponds to a locale code (e.g. <see cref="LocalizedContentManager.LanguageCode.fr"/> given <c>fr-FR</c>).</summary> + /// <param name="locale">The locale code to search. This must exactly match the language; no fallback is performed.</param> + /// <param name="language">The matched language enum, if any.</param> + /// <returns>Returns whether a valid language was found.</returns> + public bool TryGetLanguageEnum(string locale, out LocalizedContentManager.LanguageCode language) + { + return this.LocaleCodes.Value.TryGetValue(locale, out language); + } + + /// <summary>Get the locale code which corresponds to a language enum (e.g. <c>fr-FR</c> given <see cref="LocalizedContentManager.LanguageCode.fr"/>).</summary> + /// <param name="language">The language enum to search.</param> + public string GetLocaleCode(LocalizedContentManager.LanguageCode language) + { + if (language == LocalizedContentManager.LanguageCode.mod && LocalizedContentManager.CurrentModLanguage == null) + return null; + + return Game1.content.LanguageCodeString(language); + } + /// <summary>Dispose held resources.</summary> public void Dispose() { @@ -457,5 +484,19 @@ namespace StardewModdingAPI.Framework return false; } } + + /// <summary>Get the language enums (like <see cref="LocalizedContentManager.LanguageCode.ja"/>) indexed by locale code (like <c>ja-JP</c>).</summary> + private IDictionary<string, LocalizedContentManager.LanguageCode> GetLocaleCodes() + { + IDictionary<string, LocalizedContentManager.LanguageCode> map = new Dictionary<string, LocalizedContentManager.LanguageCode>(); + foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) + { + string locale = this.GetLocaleCode(code); + if (locale != null) + map[locale] = code; + } + + return map; + } } } diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 7244a534..5645c0fa 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -38,9 +38,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>A callback to invoke when the content manager is being disposed.</summary> private readonly Action<BaseContentManager> OnDisposing; - /// <summary>The language enum values indexed by locale code.</summary> - protected IDictionary<string, LanguageCode> LanguageCodes { get; } - /// <summary>A list of disposable assets.</summary> private readonly List<WeakReference<IDisposable>> Disposables = new List<WeakReference<IDisposable>>(); @@ -92,7 +89,6 @@ namespace StardewModdingAPI.Framework.ContentManagers this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; // get asset data - this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.OrdinalIgnoreCase); this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue(); } @@ -292,7 +288,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (lastSepIndex >= 0) { string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.LanguageCodes.ContainsKey(suffix)) + if (this.Coordinator.TryGetLanguageEnum(suffix, out _)) { assetName = cacheKey.Substring(0, lastSepIndex); localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); @@ -311,17 +307,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="language">The language to check.</param> protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language); - /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> - private IDictionary<LanguageCode, string> GetKeyLocales() - { - // create locale => code map - IDictionary<LanguageCode, string> map = new Dictionary<LanguageCode, string>(); - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - map[code] = this.GetLocale(code); - - return map; - } - /// <summary>Get the asset name from a cache key.</summary> /// <param name="cacheKey">The input cache key.</param> private string GetAssetName(string cacheKey) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 38bcf153..7a49dd36 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -249,7 +249,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // extract language code int splitIndex = rawAsset.LastIndexOf('.'); - if (splitIndex != -1 && this.LanguageCodes.TryGetValue(rawAsset.Substring(splitIndex + 1), out language)) + if (splitIndex != -1 && this.Coordinator.TryGetLanguageEnum(rawAsset.Substring(splitIndex + 1), out language)) { assetName = rawAsset.Substring(0, splitIndex); return true; diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index d24ffb81..beb90a5d 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -2,12 +2,12 @@ using System; using System.Globalization; using System.IO; using System.Linq; +using BmFont; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -130,6 +130,14 @@ namespace StardewModdingAPI.Framework.ContentManagers } break; + // unpacked Bitmap font + case ".fnt": + { + string source = File.ReadAllText(file.FullName); + asset = (T)(object)new XmlSource(source); + } + break; + // unpacked data case ".json": { @@ -172,13 +180,11 @@ namespace StardewModdingAPI.Framework.ContentManagers break; default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.json', '.png', '.tbin', or '.xnb'."); + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', or '.xnb'."); } } catch (Exception ex) when (!(ex is SContentLoadException)) { - if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") - throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex); } diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index f5f2d916..b0bb7f80 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -157,11 +158,8 @@ namespace StardewModdingAPI.Framework.Input yield break; // buttons - foreach (var pair in this.ButtonStates) - { - if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button)) - yield return button.ToSButton(); - } + foreach (Buttons button in this.GetPressedGamePadButtons()) + yield return button.ToSButton(); // triggers if (this.LeftTrigger > 0.2f) @@ -201,7 +199,7 @@ namespace StardewModdingAPI.Framework.Input rightThumbStick: this.RightStickPos, leftTrigger: this.LeftTrigger, rightTrigger: this.RightTrigger, - buttons: this.GetButtonBitmask() // MonoGame requires one bitmask here; don't specify multiple values + buttons: this.GetPressedGamePadButtons().ToArray() ); return this.State.Value; @@ -211,17 +209,14 @@ namespace StardewModdingAPI.Framework.Input /********* ** Private methods *********/ - /// <summary>Get a bitmask representing the pressed buttons.</summary> - private Buttons GetButtonBitmask() + /// <summary>Get the pressed gamepad buttons.</summary> + private IEnumerable<Buttons> GetPressedGamePadButtons() { - Buttons flag = 0; foreach (var pair in this.ButtonStates) { if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button)) - flag |= button; + yield return button; } - - return flag; } } } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 6c9a5f3b..4cb77a45 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -6,7 +6,6 @@ using System.Threading; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Reflection; -using StardewValley; using StardewValley.Menus; namespace StardewModdingAPI.Framework @@ -150,11 +149,7 @@ namespace StardewModdingAPI.Framework /// <param name="reflection">The reflection helper with which to access private fields.</param> public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection) { - string fieldName = Constants.GameFramework == GameFramework.Xna - ? "inBeginEndPair" - : "_beginCalled"; - - return reflection.GetField<bool>(Game1.spriteBatch, fieldName).GetValue(); + return reflection.GetField<bool>(spriteBatch, "_beginCalled").GetValue(); } } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index f2876146..5a291d0a 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -250,36 +250,7 @@ namespace StardewModdingAPI.Framework.Logging /// <param name="exception">The exception details.</param> public void LogFatalLaunchError(Exception exception) { - switch (exception) - { - // audio crash - case InvalidOperationException ex when ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor"): - this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}"); - break; - - // missing content folder exception - case FileNotFoundException ex when ex.Message == "Couldn't find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.": // path in error is hardcoded regardless of install path - this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error); - this.Monitor.Log($"Technical details: {ex.GetLogSummary()}"); - break; - - // path too long exception - case PathTooLongException _: - { - string[] affectedPaths = PathUtilities.GetTooLongPaths(Constants.ModsPath).ToArray(); - string message = affectedPaths.Any() - ? $"SMAPI can't launch because some of your mod files exceed the maximum path length on {Constants.Platform}.\nIf you need help fixing this error, see https://smapi.io/help\n\nAffected paths:\n {string.Join("\n ", affectedPaths)}" - : $"The game failed to launch: {exception.GetLogSummary()}"; - this.MonitorForGame.Log(message, LogLevel.Error); - } - break; - - // generic exception - default: - this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error); - break; - } + this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error); } /**** @@ -290,7 +261,7 @@ namespace StardewModdingAPI.Framework.Logging /// <param name="customSettings">The custom SMAPI settings.</param> public void LogIntro(string modsPath, IDictionary<string, object> customSettings) { - // log platform & patches + // log platform this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); // log basic info diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 57a76a35..cb5fa2ae 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -53,16 +53,15 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// <summary>Construct an instance.</summary> /// <param name="targetPlatform">The current game platform.</param> - /// <param name="framework">The game framework running the game.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="paranoidMode">Whether to detect paranoid mode issues.</param> /// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param> - public AssemblyLoader(Platform targetPlatform, GameFramework framework, IMonitor monitor, bool paranoidMode, bool rewriteMods) + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods) { this.Monitor = monitor; this.ParanoidMode = paranoidMode; this.RewriteMods = rewriteMods; - this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform, framework)); + this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); // init resolver this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs index aefd1c20..a064f503 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs @@ -4,7 +4,7 @@ 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/macOS or Windows.</summary> + /// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility with mods written for XNA Framework before Stardew Valley 1.5.5.</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/macOS.")] @@ -19,14 +19,6 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades /**** - ** MonoGame signatures - ****/ - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); - } - - /**** ** XNA signatures ****/ public new void Begin() diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index 8d1b6034..5acba569 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Framework.Reflection /// <summary>Construct an instance.</summary> public InterfaceProxyFactory() { - AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6dffb1de..55a7f083 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -11,14 +11,10 @@ using System.Runtime.ExceptionServices; using System.Security; using System.Text; using System.Threading; -using System.Threading.Tasks; using Microsoft.Xna.Framework; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; #endif -#if SMAPI_FOR_XNA -using System.Windows.Forms; -#endif using Newtonsoft.Json; using StardewModdingAPI.Enums; using StardewModdingAPI.Events; @@ -224,10 +220,6 @@ namespace StardewModdingAPI.Framework this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); // add error handlers -#if SMAPI_FOR_XNA - Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); -#endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // add more lenient assembly resolver @@ -243,7 +235,7 @@ namespace StardewModdingAPI.Framework monitor: this.Monitor, reflection: this.Reflection, eventManager: this.EventManager, - modHooks: new SModHooks(this.OnNewDayAfterFade), + modHooks: new SModHooks(this.OnNewDayAfterFade, this.Monitor), multiplayer: this.Multiplayer, exitGameImmediately: this.ExitGameImmediately, @@ -657,13 +649,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Game loader done."); } - if (instance.NewDayTask?.Status == TaskStatus.Created) - { - this.Monitor.Log("New day task synchronizing..."); - instance.NewDayTask.RunSynchronously(); - this.Monitor.Log("New day task done."); - } - // While a background task is in progress, the game may make changes to the game // state while mods are running their code. This is risky, because data changes can // conflict (e.g. collection changed during enumeration errors) and data may change @@ -673,7 +658,7 @@ namespace StardewModdingAPI.Framework // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one // update tick are negligible and not worth the complications of bypassing Game1.Update. - if (instance.NewDayTask != null || Game1.gameMode == Game1.loadingMode) + if (Game1.gameMode == Game1.loadingMode) { events.UnvalidatedUpdateTicking.RaiseEmpty(); runUpdate(); @@ -766,7 +751,7 @@ namespace StardewModdingAPI.Framework ** Locale changed events *********/ if (state.Locale.IsChanged) - this.Monitor.Log($"Context: locale set to {state.Locale.New}."); + this.Monitor.Log($"Context: locale set to {state.Locale.New} ({this.ContentCore.GetLocaleCode(state.Locale.New)})."); /********* ** Load / return-to-title events @@ -776,7 +761,7 @@ namespace StardewModdingAPI.Framework else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready) { // print context - string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}."; + string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.GetLocale()}."; if (Context.IsMultiplayer) { int onlineCount = Game1.getOnlineFarmers().Count(); @@ -1304,9 +1289,6 @@ 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/macOS -#endif WebApiClient client = new WebApiClient(url, Constants.ApiVersion); this.Monitor.Log("Checking for updates..."); @@ -1491,7 +1473,7 @@ namespace StardewModdingAPI.Framework // load mods IList<IModMetadata> skippedMods = new List<IModMetadata>(); - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, Constants.GameFramework, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) { // init HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); @@ -1701,9 +1683,8 @@ namespace StardewModdingAPI.Framework catch (Exception ex) { errorReasonPhrase = "its DLL couldn't be loaded."; - // re-enable in Stardew Valley 1.5.5 - //if (ex is BadImageFormatException && !EnvironmentUtility.Is64BitAssembly(assemblyPath)) - // errorReasonPhrase = "it needs to be updated for 64-bit mode."; + if (ex is BadImageFormatException && !EnvironmentUtility.Is64BitAssembly(assemblyPath)) + errorReasonPhrase = "it needs to be updated for 64-bit mode."; errorDetails = $"Error: {ex.GetLogSummary()}"; failReason = ModFailReason.LoadFailed; diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4e134455..898ed1f5 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -252,7 +252,7 @@ namespace StardewModdingAPI.Framework /// <summary>Replicate the game's draw logic with some changes for SMAPI.</summary> /// <param name="gameTime">A snapshot of the game timing state.</param> /// <param name="target_screen">The render target, if any.</param> - /// <remarks>This implementation is identical to <see cref="Game1.Draw"/>, except for try..catch around menu draw code, private field references replaced by wrappers, and added events.</remarks> + /// <remarks>This implementation is identical to <see cref="Game1._draw"/>, except for try..catch around menu draw code, private field references replaced by wrappers, and added events.</remarks> [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] @@ -286,7 +286,7 @@ namespace StardewModdingAPI.Framework IClickableMenu menu = Game1.activeClickableMenu; if (menu != null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.Rendering.RaiseEmpty(); try { @@ -304,7 +304,7 @@ namespace StardewModdingAPI.Framework } if (Game1.overlayMenu != null) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } @@ -315,7 +315,7 @@ namespace StardewModdingAPI.Framework if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot) { Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.Rendering.RaiseEmpty(); IClickableMenu curMenu = null; @@ -346,11 +346,11 @@ namespace StardewModdingAPI.Framework } if (Game1.gameMode == 11) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.Rendering.RaiseEmpty(); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Microsoft.Xna.Framework.Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Microsoft.Xna.Framework.Color(0, 255, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Microsoft.Xna.Framework.Color.White); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, 255, 0)); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); return; @@ -368,8 +368,8 @@ namespace StardewModdingAPI.Framework if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) { Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); Game1.spriteBatch.End(); Game1.PopUIMode(); } @@ -388,7 +388,7 @@ namespace StardewModdingAPI.Framework if (Game1.showingEndOfNightStuff) { Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.Rendering.RaiseEmpty(); if (Game1.activeClickableMenu != null) { @@ -417,16 +417,16 @@ namespace StardewModdingAPI.Framework { Game1.PushUIMode(); base.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.Rendering.RaiseEmpty(); string addOn = ""; for (int i = 0; (double)i < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; i++) { addOn += "."; } - string str = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string msg = str + addOn; - string largestMessage = str + "... "; + string text = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); + string msg = text + addOn; + string largestMessage = text + "... "; int msgw = SpriteText.getWidthOfString(largestMessage); int msgh = 64; int msgx = 64; @@ -442,7 +442,7 @@ namespace StardewModdingAPI.Framework byte batchOpens = 0; // used for rendering event if (Game1.gameMode == 0) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if (++batchOpens == 1) events.Rendering.RaiseEmpty(); } @@ -456,7 +456,7 @@ namespace StardewModdingAPI.Framework if (Game1.drawLighting) { Game1.SetRenderTarget(Game1.lightmap); - base.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.White * 0f); + base.GraphicsDevice.Clear(Color.White * 0f); Matrix lighting_matrix = Matrix.Identity; if (this.useUnscaledLighting) { @@ -465,13 +465,13 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, lighting_matrix); if (++batchOpens == 1) events.Rendering.RaiseEmpty(); - Microsoft.Xna.Framework.Color lighting = (Game1.currentLocation.Name.StartsWith("UndergroundMine") && Game1.currentLocation is MineShaft) ? (Game1.currentLocation as MineShaft).getLightingColor(gameTime) : ((Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors)) ? Game1.outdoorLight : Game1.ambientLight); + Color lighting = ((Game1.currentLocation.Name.StartsWith("UndergroundMine") && Game1.currentLocation is MineShaft) ? (Game1.currentLocation as MineShaft).getLightingColor(gameTime) : ((Game1.ambientLight.Equals(Color.White) || (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors)) ? Game1.outdoorLight : Game1.ambientLight)); float light_multiplier = 1f; if (Game1.player.hasBuff(26)) { - if (lighting == Microsoft.Xna.Framework.Color.White) + if (lighting == Color.White) { - lighting = new Microsoft.Xna.Framework.Color(0.75f, 0.75f, 0.75f); + lighting = new Color(0.75f, 0.75f, 0.75f); } else { @@ -504,12 +504,8 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.End(); Game1.SetRenderTarget(target_screen); } - if (Game1.bloomDay && Game1.bloom != null) - { - Game1.bloom.BeginDraw(); - } base.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if (++batchOpens == 1) events.Rendering.RaiseEmpty(); events.RenderingWorld.RaiseEmpty(); @@ -522,10 +518,10 @@ namespace StardewModdingAPI.Framework Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); Game1.currentLocation.drawWater(Game1.spriteBatch); Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); Game1.currentLocation.drawFloorDecorations(Game1.spriteBatch); Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); this._farmerShadows.Clear(); if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) { @@ -555,17 +551,17 @@ namespace StardewModdingAPI.Framework { if (!k.swimming && !k.HideShadow && !k.IsInvisible && !this.checkCharacterTilesForShadowDrawFlag(k)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, k.GetShadowOffset() + k.Position + new Vector2((float)(k.GetSpriteWidthForPositioning() * 4) / 2f, k.GetBoundingBox().Height + ((!k.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)k.yJumpOffset / 40f) * (float)k.scale), SpriteEffects.None, Math.Max(0f, (float)k.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, k.GetShadowOffset() + k.Position + new Vector2((float)(k.GetSpriteWidthForPositioning() * 4) / 2f, k.GetBoundingBox().Height + ((!k.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)k.yJumpOffset / 40f) * (float)k.scale), SpriteEffects.None, Math.Max(0f, (float)k.getStandingY() / 10000f) - 1E-06f); } } } else { - foreach (NPC m in Game1.CurrentEvent.actors) + foreach (NPC l in Game1.CurrentEvent.actors) { - if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(m)) && !m.swimming && !m.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(m)) + if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(l)) && !l.swimming && !l.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(l)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, m.GetShadowOffset() + m.Position + new Vector2((float)(m.GetSpriteWidthForPositioning() * 4) / 2f, m.GetBoundingBox().Height + ((!m.IsMonster) ? ((m.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, 4f + (float)m.yJumpOffset / 40f) * (float)m.scale, SpriteEffects.None, Math.Max(0f, (float)m.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, l.GetShadowOffset() + l.Position + new Vector2((float)(l.GetSpriteWidthForPositioning() * 4) / 2f, l.GetBoundingBox().Height + ((!l.IsMonster) ? ((l.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, 4f + (float)l.yJumpOffset / 40f) * (float)l.scale, SpriteEffects.None, Math.Max(0f, (float)l.getStandingY() / 10000f) - 1E-06f); } } } @@ -573,7 +569,7 @@ namespace StardewModdingAPI.Framework { if (!Game1.multiplayer.isDisconnecting(f3.UniqueMultiplayerID) && !f3.swimming && !f3.isRidingHorse() && !f3.IsSitting() && (Game1.currentLocation == null || !this.checkCharacterTilesForShadowDrawFlag(f3))) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f3.GetShadowOffset() + f3.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f3.running || f3.UsingTool) && f3.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f3.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f3.GetShadowOffset() + f3.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f3.running || f3.UsingTool) && f3.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f3.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); } } } @@ -581,26 +577,26 @@ namespace StardewModdingAPI.Framework building_layer.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); Game1.mapDisplayDevice.EndScene(); Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); if (!Game1.currentLocation.shouldHideCharacters()) { if (Game1.CurrentEvent == null) { - foreach (NPC n in Game1.currentLocation.characters) + foreach (NPC m in Game1.currentLocation.characters) { - if (!n.swimming && !n.HideShadow && !n.isInvisible && this.checkCharacterTilesForShadowDrawFlag(n)) + if (!m.swimming && !m.HideShadow && !m.isInvisible && this.checkCharacterTilesForShadowDrawFlag(m)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n.GetShadowOffset() + n.Position + new Vector2((float)(n.GetSpriteWidthForPositioning() * 4) / 2f, n.GetBoundingBox().Height + ((!n.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n.yJumpOffset / 40f) * (float)n.scale), SpriteEffects.None, Math.Max(0f, (float)n.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, m.GetShadowOffset() + m.Position + new Vector2((float)(m.GetSpriteWidthForPositioning() * 4) / 2f, m.GetBoundingBox().Height + ((!m.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)m.yJumpOffset / 40f) * (float)m.scale), SpriteEffects.None, Math.Max(0f, (float)m.getStandingY() / 10000f) - 1E-06f); } } } else { - foreach (NPC n2 in Game1.CurrentEvent.actors) + foreach (NPC n in Game1.CurrentEvent.actors) { - if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n2)) && !n2.swimming && !n2.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n2)) + if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n)) && !n.swimming && !n.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n2.GetShadowOffset() + n2.Position + new Vector2((float)(n2.GetSpriteWidthForPositioning() * 4) / 2f, n2.GetBoundingBox().Height + ((!n2.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n2.yJumpOffset / 40f) * (float)n2.scale), SpriteEffects.None, Math.Max(0f, (float)n2.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n.GetShadowOffset() + n.Position + new Vector2((float)(n.GetSpriteWidthForPositioning() * 4) / 2f, n.GetBoundingBox().Height + ((!n.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n.yJumpOffset / 40f) * (float)n.scale), SpriteEffects.None, Math.Max(0f, (float)n.getStandingY() / 10000f) - 1E-06f); } } } @@ -609,7 +605,7 @@ namespace StardewModdingAPI.Framework float draw_layer = Math.Max(0.0001f, f4.getDrawLayer() + 0.00011f) - 0.0001f; if (!f4.swimming && !f4.isRidingHorse() && !f4.IsSitting() && Game1.currentLocation != null && this.checkCharacterTilesForShadowDrawFlag(f4)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f4.GetShadowOffset() + f4.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Microsoft.Xna.Framework.Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f4.running || f4.UsingTool) && f4.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f4.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, draw_layer); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f4.GetShadowOffset() + f4.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f4.running || f4.UsingTool) && f4.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f4.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, draw_layer); } } } @@ -619,7 +615,7 @@ namespace StardewModdingAPI.Framework } if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) { - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f); + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f); } Game1.currentLocation.draw(Game1.spriteBatch); foreach (Vector2 tile_position in Game1.crabPotOverlayTiles.Keys) @@ -646,14 +642,14 @@ namespace StardewModdingAPI.Framework } if (Game1.tvStation >= 0) { - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); } if (Game1.panMode) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Lime * 0.75f); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f); foreach (Warp w in Game1.currentLocation.warps) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Red * 0.75f); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); } } Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -661,7 +657,7 @@ namespace StardewModdingAPI.Framework Game1.mapDisplayDevice.EndScene(); Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) { Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -670,7 +666,7 @@ namespace StardewModdingAPI.Framework } if (Game1.toolHold > 400f && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) { - Microsoft.Xna.Framework.Color barColor = Microsoft.Xna.Framework.Color.White; + Color barColor = Color.White; switch ((int)(Game1.toolHold / 600f) + 2) { case 1: @@ -686,7 +682,7 @@ namespace StardewModdingAPI.Framework barColor = Tool.iridiumColor; break; } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Microsoft.Xna.Framework.Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Color.Black); Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0), (int)(Game1.toolHold % 600f * 0.08f), 8), barColor); } if (!Game1.IsFakedBlackScreen()) @@ -699,7 +695,7 @@ namespace StardewModdingAPI.Framework } if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); } if (Game1.screenGlow) { @@ -711,51 +707,51 @@ namespace StardewModdingAPI.Framework Game1.player.CurrentTool.draw(Game1.spriteBatch); } Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); if (Game1.eventUp && Game1.currentLocation.currentEvent != null) { - foreach (NPC l in Game1.currentLocation.currentEvent.actors) + foreach (NPC n2 in Game1.currentLocation.currentEvent.actors) { - if (l.isEmoting) + if (n2.isEmoting) { - Vector2 emotePosition = l.getLocalPosition(Game1.viewport); - if (l.NeedsBirdieEmoteHack()) + Vector2 emotePosition = n2.getLocalPosition(Game1.viewport); + if (n2.NeedsBirdieEmoteHack()) { emotePosition.X += 64f; } emotePosition.Y -= 140f; - if (l.Age == 2) + if (n2.Age == 2) { emotePosition.Y += 32f; } - else if (l.Gender == 1) + else if (n2.Gender == 1) { emotePosition.Y += 10f; } - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(l.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, l.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)l.getStandingY() / 10000f); + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(n2.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, n2.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)n2.getStandingY() / 10000f); } } } Game1.spriteBatch.End(); if (Game1.drawLighting && !Game1.IsFakedBlackScreen()) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp); Viewport vp = base.GraphicsDevice.Viewport; - vp.Bounds = (target_screen?.Bounds ?? base.GraphicsDevice.PresentationParameters.Bounds); + vp.Bounds = target_screen?.Bounds ?? base.GraphicsDevice.PresentationParameters.Bounds; base.GraphicsDevice.Viewport = vp; float render_zoom = Game1.options.lightingQuality / 2; if (this.useUnscaledLighting) { render_zoom /= Game1.options.zoomLevel; } - Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Microsoft.Xna.Framework.Color.White, 0f, Vector2.Zero, render_zoom, SpriteEffects.None, 1f); + Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Color.White, 0f, Vector2.Zero, render_zoom, SpriteEffects.None, 1f); if (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) { - Game1.spriteBatch.Draw(Game1.staminaRect, vp.Bounds, Microsoft.Xna.Framework.Color.OrangeRed * 0.45f); + Game1.spriteBatch.Draw(Game1.staminaRect, vp.Bounds, Color.OrangeRed * 0.45f); } Game1.spriteBatch.End(); } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.RenderedWorld.RaiseEmpty(); if (Game1.drawGrid) { @@ -763,11 +759,11 @@ namespace StardewModdingAPI.Framework float startingY = -Game1.viewport.Y % 64; for (int x = startingX; x < Game1.graphics.GraphicsDevice.Viewport.Width; x += 64) { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x, (int)startingY, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Microsoft.Xna.Framework.Color.Red * 0.5f); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x, (int)startingY, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); } for (float y = startingY; y < (float)Game1.graphics.GraphicsDevice.Viewport.Height; y += 64f) { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(startingX, (int)y, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Microsoft.Xna.Framework.Color.Red * 0.5f); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(startingX, (int)y, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); } } if (Game1.ShouldShowOnscreenUsernames() && Game1.currentLocation != null) @@ -780,14 +776,14 @@ namespace StardewModdingAPI.Framework } if (!Game1.eventUp && Game1.farmEvent == null && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !this.takingMapScreenshot && Game1.isOutdoorMapSmallerThanViewport()) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -Math.Min(Game1.viewport.X, GameRunner.MaxTextureSize), Game1.graphics.GraphicsDevice.Viewport.Height), Microsoft.Xna.Framework.Color.Black); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64, 0, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)), Game1.graphics.GraphicsDevice.Viewport.Height), Microsoft.Xna.Framework.Color.Black); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, Game1.graphics.GraphicsDevice.Viewport.Width, -Math.Min(Game1.viewport.Y, GameRunner.MaxTextureSize)), Microsoft.Xna.Framework.Color.Black); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, -Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64, Game1.graphics.GraphicsDevice.Viewport.Width, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Height - (-Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64))), Microsoft.Xna.Framework.Color.Black); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -Math.Min(Game1.viewport.X, GameRunner.MaxTextureSize), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64, 0, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, Game1.graphics.GraphicsDevice.Viewport.Width, -Math.Min(Game1.viewport.Y, GameRunner.MaxTextureSize)), Color.Black); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, -Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64, Game1.graphics.GraphicsDevice.Viewport.Width, Math.Min(GameRunner.MaxTextureSize, Game1.graphics.GraphicsDevice.Viewport.Height - (-Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64))), Color.Black); } Game1.spriteBatch.End(); Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if ((Game1.displayHUD || Game1.eventUp) && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !Game1.freezeControls && !Game1.panMode && !Game1.HostPaused && !this.takingMapScreenshot) { events.RenderingHud.RaiseEmpty(); @@ -807,13 +803,13 @@ namespace StardewModdingAPI.Framework } Game1.spriteBatch.End(); Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); } if (Game1.farmEvent != null) { Game1.farmEvent.draw(Game1.spriteBatch); Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); } Game1.PushUIMode(); if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)) && !this.takingMapScreenshot) @@ -822,35 +818,35 @@ namespace StardewModdingAPI.Framework } if (Game1.progressBar && !this.takingMapScreenshot) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Microsoft.Xna.Framework.Color.LightGray); - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(Game1.pauseAccumulator / Game1.pauseTime * (float)Game1.dialogueWidth), 32), Microsoft.Xna.Framework.Color.DimGray); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(Game1.pauseAccumulator / Game1.pauseTime * (float)Game1.dialogueWidth), 32), Color.DimGray); } Game1.spriteBatch.End(); Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) { Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); } if (!Game1.IsFakedBlackScreen() && Game1.IsRainingHere() && Game1.currentLocation != null && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) { - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Blue * 0.2f); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); } if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause) && !this.takingMapScreenshot) { Game1.spriteBatch.End(); Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); Game1.spriteBatch.End(); Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); } else if (Game1.flashAlpha > 0f && !this.takingMapScreenshot) { if (Game1.options.screenFlash) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.White * Math.Min(1f, Game1.flashAlpha)); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha)); } Game1.flashAlpha -= 0.1f; } @@ -866,14 +862,14 @@ namespace StardewModdingAPI.Framework } Game1.spriteBatch.End(); Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); foreach (TemporaryAnimatedSprite uiOverlayTempSprite in Game1.uiOverlayTempSprites) { uiOverlayTempSprite.draw(Game1.spriteBatch, localPosition: true); } Game1.spriteBatch.End(); Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); } if (Game1.debugMode) { @@ -905,14 +901,14 @@ namespace StardewModdingAPI.Framework sb.Append(Game1.getMouseY() + Game1.viewport.Y); sb.Append(" debugOutput: "); sb.Append(Game1.debugOutput); - Game1.spriteBatch.DrawString(Game1.smallFont, sb, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8), Microsoft.Xna.Framework.Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + Game1.spriteBatch.DrawString(Game1.smallFont, sb, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8), Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); } Game1.spriteBatch.End(); Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if (Game1.showKeyHelp && !this.takingMapScreenshot) { - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? (192 + (Game1.isQuestion ? (Game1.questionChoices.Count * 64) : 0)) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Microsoft.Xna.Framework.Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? (192 + (Game1.isQuestion ? (Game1.questionChoices.Count * 64) : 0)) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); } if (Game1.activeClickableMenu != null && !this.takingMapScreenshot) { diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs index 7dafc746..101e022a 100644 --- a/src/SMAPI/Framework/SModHooks.cs +++ b/src/SMAPI/Framework/SModHooks.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using StardewValley; namespace StardewModdingAPI.Framework @@ -12,15 +13,20 @@ namespace StardewModdingAPI.Framework /// <summary>A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</summary> private readonly Action BeforeNewDayAfterFade; + /// <summary>Writes messages to the console.</summary> + private readonly IMonitor Monitor; + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="beforeNewDayAfterFade">A callback to invoke before <see cref="Game1.newDayAfterFade"/> runs.</param> - public SModHooks(Action beforeNewDayAfterFade) + /// <param name="monitor">Writes messages to the console.</param> + public SModHooks(Action beforeNewDayAfterFade, IMonitor monitor) { this.BeforeNewDayAfterFade = beforeNewDayAfterFade; + this.Monitor = monitor; } /// <summary>A hook invoked when <see cref="Game1.newDayAfterFade"/> is called.</summary> @@ -30,5 +36,27 @@ namespace StardewModdingAPI.Framework this.BeforeNewDayAfterFade?.Invoke(); action(); } + + /// <summary>Start an asynchronous task for the game.</summary> + /// <param name="task">The task to start.</param> + /// <param name="id">A unique key which identifies the task.</param> + public override Task StartTask(Task task, string id) + { + this.Monitor.Log($"Synchronizing '{id}' task..."); + task.RunSynchronously(); + this.Monitor.Log(" task complete."); + return task; + } + + /// <summary>Start an asynchronous task for the game.</summary> + /// <param name="task">The task to start.</param> + /// <param name="id">A unique key which identifies the task.</param> + public override Task<T> StartTask<T>(Task<T> task, string id) + { + this.Monitor.Log($"Synchronizing '{id}' task..."); + task.RunSynchronously(); + this.Monitor.Log(" task complete."); + return task; + } } } diff --git a/src/SMAPI/GameFramework.cs b/src/SMAPI/GameFramework.cs index 7670ce8f..a0154329 100644 --- a/src/SMAPI/GameFramework.cs +++ b/src/SMAPI/GameFramework.cs @@ -1,12 +1,15 @@ +using System; + namespace StardewModdingAPI { /// <summary>The game framework running the game.</summary> public enum GameFramework { - /// <summary>The XNA Framework on Windows.</summary> + /// <summary>The XNA Framework, previously used on Windows.</summary> + [Obsolete("Stardew Valley no longer uses XNA Framework on any supported platform.")] Xna, - /// <summary>The MonoGame framework, usually on non-Windows platforms.</summary> + /// <summary>The MonoGame framework.</summary> MonoGame } } diff --git a/src/SMAPI/IAssetDataForMap.cs b/src/SMAPI/IAssetDataForMap.cs index bfaba9ba..47a33de8 100644 --- a/src/SMAPI/IAssetDataForMap.cs +++ b/src/SMAPI/IAssetDataForMap.cs @@ -13,6 +13,7 @@ namespace StardewModdingAPI /// <param name="source">The map from which to copy.</param> /// <param name="sourceArea">The tile area within the source map to copy, or <c>null</c> for the entire source map size. This must be within the bounds of the <paramref name="source"/> map.</param> /// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param> - void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null); + /// <param name="patchMode">Indicates how the map should be patched.</param> + void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay); } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 7efd99a0..552bc000 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -226,13 +226,8 @@ namespace StardewModdingAPI.Metadata ** Buildings ****/ case "buildings\\houses": // Farm - { - var field = reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)); - field.SetValue( - this.LoadAndDisposeIfNeeded(field.GetValue(), key) - ); - return true; - } + Farm.houseTextures = this.LoadAndDisposeIfNeeded(Farm.houseTextures, key); + return true; case "buildings\\houses_paintmask": // Farm { @@ -447,10 +442,6 @@ namespace StardewModdingAPI.Metadata Game1.objectSpriteSheet = content.Load<Texture2D>(key); return true; - case "maps\\walls_and_floors": // Wallpaper - Wallpaper.wallpaperTexture = content.Load<Texture2D>(key); - return true; - /**** ** Content\Minigames ****/ diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 76371e50..232e54ce 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -36,9 +36,6 @@ namespace StardewModdingAPI.Metadata // rewrite for crossplatform compatibility if (rewriteMods) { - if (platformChanged) - yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); - // rewrite for Stardew Valley 1.5 yield return new FieldReplaceRewriter(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture)); yield return new FieldReplaceRewriter(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)); @@ -48,9 +45,10 @@ namespace StardewModdingAPI.Metadata yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); - // rewrite for 64-bit mode - // re-enable in Stardew Valley 1.5.5 - //yield return new ArchitectureAssemblyRewriter(); + // rewrite for Stardew Valley 1.5.5 + if (platformChanged) + yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); + yield return new ArchitectureAssemblyRewriter(); // detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update) yield return new HarmonyRewriter(); diff --git a/src/SMAPI/PatchMapMode.cs b/src/SMAPI/PatchMapMode.cs new file mode 100644 index 00000000..1f1ac6a9 --- /dev/null +++ b/src/SMAPI/PatchMapMode.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI +{ + /// <summary>Indicates how a map should be patched.</summary> + public enum PatchMapMode + { + /// <summary>Replace matching tiles. Target tiles missing in the source area are kept as-is.</summary> + Overlay, + + /// <summary>Replace all tiles on layers that exist in the source map.</summary> + ReplaceByLayer, + + /// <summary>Replace all tiles with the source map.</summary> + Replace + } +} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3f97e531..67ff8322 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Reflection; using System.Threading; using StardewModdingAPI.Framework; -using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Serialization.Models; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI { @@ -26,7 +26,7 @@ namespace StardewModdingAPI /// <param name="args">The command-line arguments.</param> public static void Main(string[] args) { - Console.Title = $"SMAPI {EarlyConstants.RawApiVersion} - {Console.Title}"; + Console.Title = $"SMAPI {EarlyConstants.RawApiVersion}"; try { @@ -34,9 +34,10 @@ namespace StardewModdingAPI Program.AssertGamePresent(); Program.AssertGameVersion(); Program.AssertSmapiVersions(); + Program.AssertDepsJson(); Program.Start(args); } - catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // don't use EarlyConstants.GameAssemblyName, since we want to check both possible names + catch (BadImageFormatException ex) when (ex.FileName == EarlyConstants.GameAssemblyName) { Console.WriteLine($"SMAPI failed to initialize because your game's {ex.FileName}.exe seems to be invalid.\nThis may be a pirated version which modified the executable in an incompatible way; if so, you can try a different download or buy a legitimate version.\n\nTechnical details:\n{ex}"); } @@ -84,22 +85,10 @@ namespace StardewModdingAPI } catch (Exception ex) { - // unofficial 64-bit - if (EarlyConstants.Platform == GamePlatform.Windows) - { - FileInfo linuxExecutable = new FileInfo(Path.Combine(EarlyConstants.ExecutionPath, "StardewValley.exe")); - if (linuxExecutable.Exists && LowLevelEnvironmentUtility.Is64BitAssembly(linuxExecutable.FullName)) - Program.PrintErrorAndExit("Oops! You're running Stardew Valley in unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info."); - } - // file doesn't exist if (!File.Exists(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe"))) Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder."); - // Stardew Valley 1.5.5+ - if (File.Exists(Path.Combine(EarlyConstants.ExecutionPath, "Stardew Valley.dll"))) - Program.PrintErrorAndExit("Oops! You're running Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io."); - // can't load file Program.PrintErrorAndExit( message: "Oops! SMAPI couldn't load the game executable. The technical details below may have more info.", @@ -143,6 +132,20 @@ namespace StardewModdingAPI } } + /// <summary>Assert that SMAPI's <c>StardewModdingAPI.deps.json</c> matches <c>Stardew Valley.deps.json</c>, fixing it if necessary.</summary> + /// <remarks>This is needed to resolve native DLLs like libSkiaSharp.</remarks> + private static void AssertDepsJson() + { + string sourcePath = Path.Combine(Constants.ExecutionPath, "Stardew Valley.deps.json"); + string targetPath = Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.deps.json"); + + if (!File.Exists(targetPath) || FileUtilities.GetFileHash(sourcePath) != FileUtilities.GetFileHash(targetPath)) + { + File.Copy(sourcePath, targetPath, overwrite: true); + Program.PrintErrorAndExit($"The '{Path.GetFileName(targetPath)}' file didn't match the game's version. SMAPI fixed it automatically, but you must restart SMAPI for the change to take effect."); + } + } + /// <summary>Initialize SMAPI and launch the game.</summary> /// <param name="args">The command-line arguments.</param> /// <remarks>This method is separate from <see cref="Main"/> because that can't contain any references to assemblies loaded by <see cref="CurrentDomain_AssemblyResolve"/> (e.g. via <see cref="Constants"/>), or Mono will incorrectly show an assembly resolution error before assembly resolution is set up.</remarks> diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 7b5625d6..e62c8880 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -76,8 +76,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future /** * The base URL for SMAPI's web API, used to perform update checks. - * 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/", diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index c147e7dc..b99028da 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -3,13 +3,16 @@ <AssemblyName>StardewModdingAPI</AssemblyName> <RootNamespace>StardewModdingAPI</RootNamespace> <Description>The modding API for Stardew Valley.</Description> - <TargetFramework>net452</TargetFramework> - <PlatformTarget>x86</PlatformTarget> + <TargetFramework>net5.0</TargetFramework> + <PlatformTarget>x64</PlatformTarget> <OutputType>Exe</OutputType> <GenerateDocumentationFile>true</GenerateDocumentationFile> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <LargeAddressAware Condition="'$(OS)' == 'Windows_NT'">true</LargeAddressAware> <ApplicationIcon>icon.ico</ApplicationIcon> + + <!--copy dependency DLLs to bin folder so we can include them in installer bundle --> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <Import Project="..\..\build\common.targets" /> @@ -19,43 +22,22 @@ <PackageReference Include="Mono.Cecil" Version="0.11.4" /> <PackageReference Include="MonoMod.Common" Version="21.6.21.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="Platonymous.TMXTile" Version="1.5.8" /> + <PackageReference Include="Platonymous.TMXTile" Version="1.5.9" /> + <PackageReference Include="System.Reflection.Emit" Version="4.7.0" /> + <PackageReference Include="System.Runtime.Caching" Version="5.0.0" /> </ItemGroup> <ItemGroup> <Reference Include="..\..\build\0Harmony.dll" Private="True" /> - <Reference Include="$(GameExecutableName)" HintPath="$(GamePath)\$(GameExecutableName).exe" Private="False" /> + <Reference Include="Stardew Valley" HintPath="$(GamePath)\Stardew Valley.dll" Private="False" /> <Reference Include="StardewValley.GameData" HintPath="$(GamePath)\StardewValley.GameData.dll" Private="False" /> - <Reference Include="System.Numerics" Private="True" /> - <Reference Include="System.Runtime.Caching" Private="True" /> + <Reference Include="BmFont" HintPath="$(GamePath)\BmFont.dll" Private="False" /> <Reference Include="GalaxyCSharp" HintPath="$(GamePath)\GalaxyCSharp.dll" Private="False" /> <Reference Include="Lidgren.Network" HintPath="$(GamePath)\Lidgren.Network.dll" Private="False" /> + <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" 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="System.Windows.Forms" /> - </ItemGroup> - - <!-- Game framework --> - <Choose> - <When Condition="$(DefineConstants.Contains(SMAPI_FOR_XNA))"> - <ItemGroup> - <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" Private="False" /> - </ItemGroup> - </When> - <Otherwise> - <ItemGroup> - <Reference Include="MonoGame.Framework" HintPath="$(GamePath)\MonoGame.Framework.dll" Private="False" /> - </ItemGroup> - </Otherwise> - </Choose> - <ItemGroup> <ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" /> <ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" /> |