summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-08-14 12:21:40 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-08-14 12:21:40 -0400
commit4f28ea33bd7cc65485402c5e85259083e86b49e1 (patch)
tree86c4d8f9272de9a715cfcbf4008f0c09f5a59a21 /src
parent60b41195778af33fd609eab66d9ae3f1d1165e8f (diff)
parent4dd4efc96fac6a7ab66c14edead10e4fa988040d (diff)
downloadSMAPI-4f28ea33bd7cc65485402c5e85259083e86b49e1.tar.gz
SMAPI-4f28ea33bd7cc65485402c5e85259083e86b49e1.tar.bz2
SMAPI-4f28ea33bd7cc65485402c5e85259083e86b49e1.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/Framework/InstallerPaths.cs61
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs463
-rw-r--r--src/SMAPI.Installer/StardewModdingAPI.Installer.csproj1
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs5
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs29
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json5
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json5
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs41
-rw-r--r--src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json21
-rw-r--r--src/SMAPI/Constants.cs15
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs10
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs31
-rw-r--r--src/SMAPI/Framework/ContentPack.cs4
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs10
-rw-r--r--src/SMAPI/Framework/SGame.cs26
-rw-r--r--src/SMAPI/Framework/SGameConstructorHack.cs37
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs84
-rw-r--r--src/SMAPI/Program.cs198
-rw-r--r--src/SMAPI/SButton.cs6
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj1
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs44
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs5
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs3
-rw-r--r--src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs22
-rw-r--r--src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs22
27 files changed, 722 insertions, 433 deletions
diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs
new file mode 100644
index 00000000..d212876a
--- /dev/null
+++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs
@@ -0,0 +1,61 @@
+using System.IO;
+
+namespace StardewModdingAPI.Installer.Framework
+{
+ /// <summary>Manages paths for the SMAPI installer.</summary>
+ internal class InstallerPaths
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The directory containing the installer files for the current platform.</summary>
+ public DirectoryInfo PackageDir { get; }
+
+ /// <summary>The directory containing the installed game.</summary>
+ public DirectoryInfo GameDir { get; }
+
+ /// <summary>The directory into which to install mods.</summary>
+ public DirectoryInfo ModsDir { get; }
+
+ /// <summary>The full path to the directory containing the installer files for the current platform.</summary>
+ public string PackagePath => this.PackageDir.FullName;
+
+ /// <summary>The full path to the directory containing the installed game.</summary>
+ public string GamePath => this.GameDir.FullName;
+
+ /// <summary>The full path to the directory into which to install mods.</summary>
+ public string ModsPath => this.ModsDir.FullName;
+
+ /// <summary>The full path to the installed SMAPI executable file.</summary>
+ public string ExecutablePath { get; }
+
+ /// <summary>The full path to the vanilla game launcher on Linux/Mac.</summary>
+ public string UnixLauncherPath { get; }
+
+ /// <summary>The full path to the installed SMAPI launcher on Linux/Mac before it's renamed.</summary>
+ public string UnixSmapiLauncherPath { get; }
+
+ /// <summary>The full path to the vanilla game launcher on Linux/Mac after SMAPI is installed.</summary>
+ public string UnixBackupLauncherPath { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="packageDir">The directory path containing the installer files for the current platform.</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 packageDir, DirectoryInfo gameDir, string gameExecutableName)
+ {
+ this.PackageDir = packageDir;
+ this.GameDir = gameDir;
+ this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods"));
+
+ 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");
+ }
+ }
+}
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index f39486e1..0aac1da2 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -7,6 +7,7 @@ using System.Reflection;
using System.Threading;
using Microsoft.Win32;
using StardewModdingApi.Installer.Enums;
+using StardewModdingAPI.Installer.Framework;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.ConsoleWriting;
@@ -135,7 +136,7 @@ namespace StardewModdingApi.Installer
}
/// <summary>Handles writing color-coded text to the console.</summary>
- private readonly ColorfulConsoleWriter ConsoleWriter;
+ private ColorfulConsoleWriter ConsoleWriter;
/*********
@@ -168,6 +169,9 @@ namespace StardewModdingApi.Installer
/// </remarks>
public void Run(string[] args)
{
+ /*********
+ ** Step 1: initial setup
+ *********/
/****
** Get platform & set window title
****/
@@ -175,6 +179,9 @@ namespace StardewModdingApi.Installer
Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}";
Console.WriteLine();
+ /****
+ ** Check if correct installer
+ ****/
#if SMAPI_FOR_WINDOWS
if (platform == Platform.Linux || platform == Platform.Mac)
{
@@ -182,78 +189,17 @@ namespace StardewModdingApi.Installer
Console.ReadLine();
return;
}
-#endif
-
- /****
- ** read command-line arguments
- ****/
- // get action from CLI
- bool installArg = args.Contains("--install");
- bool uninstallArg = args.Contains("--uninstall");
- if (installArg && uninstallArg)
- {
- this.PrintError("You can't specify both --install and --uninstall command-line flags.");
- Console.ReadLine();
- return;
- }
-
- // get game path from CLI
- string gamePathArg = null;
- {
- int pathIndex = Array.LastIndexOf(args, "--game-path") + 1;
- if (pathIndex >= 1 && args.Length >= pathIndex)
- gamePathArg = args[pathIndex];
- }
-
- /****
- ** collect details
- ****/
- // get game path
- DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg);
- if (installDir == null)
- {
- this.PrintError("Failed finding your game path.");
- Console.ReadLine();
- return;
- }
-
- // get folders
- DirectoryInfo packageDir = platform.IsMono()
- ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono
- : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows"));
- DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods"));
- var paths = new
- {
- executable = Path.Combine(installDir.FullName, EnvironmentUtility.GetExecutableName(platform)),
- unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"),
- unixLauncher = Path.Combine(installDir.FullName, "StardewValley"),
- unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original")
- };
-
- // show output
- this.PrintInfo($"Your game folder: {installDir}.");
-
- /****
- ** validate assumptions
- ****/
- if (!packageDir.Exists)
- {
- this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip")
- ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder."
- : $"The 'internal/{packageDir.Name}' package folder is missing (should be at {packageDir})."
- );
- Console.ReadLine();
- return;
- }
- if (!File.Exists(paths.executable))
+#else
+ if (platform == Platform.Windows)
{
- this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
+ this.PrintError($"This is the installer for Linux/Mac. Run the 'install on Windows.exe' file instead.");
Console.ReadLine();
return;
}
+#endif
/****
- ** validate Windows dependencies
+ ** Check Windows dependencies
****/
if (platform == Platform.Windows)
{
@@ -276,159 +222,324 @@ namespace StardewModdingApi.Installer
}
}
- Console.WriteLine();
-
/****
- ** ask user what to do
+ ** read command-line arguments
****/
- ScriptAction action;
+ // get action from CLI
+ bool installArg = args.Contains("--install");
+ bool uninstallArg = args.Contains("--uninstall");
+ if (installArg && uninstallArg)
+ {
+ this.PrintError("You can't specify both --install and --uninstall command-line flags.");
+ Console.ReadLine();
+ return;
+ }
- if (installArg)
- action = ScriptAction.Install;
- else if (uninstallArg)
- action = ScriptAction.Uninstall;
- else
+ // get game path from CLI
+ string gamePathArg = null;
+ {
+ int pathIndex = Array.LastIndexOf(args, "--game-path") + 1;
+ if (pathIndex >= 1 && args.Length >= pathIndex)
+ gamePathArg = args[pathIndex];
+ }
+
+
+ /*********
+ ** Step 2: choose a theme (can't auto-detect on Linux/Mac)
+ *********/
+ MonitorColorScheme scheme = MonitorColorScheme.AutoDetect;
+ if (platform == Platform.Linux || platform == Platform.Mac)
{
- this.PrintInfo("You can....");
- this.PrintInfo("[1] Install SMAPI.");
- this.PrintInfo("[2] Uninstall SMAPI.");
+ /****
+ ** print header
+ ****/
+ this.PrintPlain("Hi there! I'll help you install or remove SMAPI. Just a few questions first.");
+ this.PrintPlain("----------------------------------------------------------------------------");
+ Console.WriteLine();
+
+ /****
+ ** show theme selector
+ ****/
+ // get theme writers
+ var lightBackgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.LightBackground);
+ var darkDarkgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.DarkBackground);
+
+ // print question
+ this.PrintPlain("Which text looks more readable?");
+ Console.WriteLine();
+ Console.Write(" [1] ");
+ lightBackgroundWriter.WriteLine("Dark text on light background", ConsoleLogLevel.Info);
+ Console.Write(" [2] ");
+ darkDarkgroundWriter.WriteLine("Light text on dark background", ConsoleLogLevel.Info);
Console.WriteLine();
- string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2");
+ // handle choice
+ string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" });
switch (choice)
{
case "1":
- action = ScriptAction.Install;
+ scheme = MonitorColorScheme.LightBackground;
+ this.ConsoleWriter = lightBackgroundWriter;
break;
case "2":
- action = ScriptAction.Uninstall;
+ scheme = MonitorColorScheme.DarkBackground;
+ this.ConsoleWriter = darkDarkgroundWriter;
break;
default:
throw new InvalidOperationException($"Unexpected action key '{choice}'.");
}
- Console.WriteLine();
}
+ Console.Clear();
- /****
- ** Always uninstall old files
- ****/
- // restore game launcher
- if (platform.IsMono() && File.Exists(paths.unixLauncherBackup))
- {
- this.PrintDebug("Removing SMAPI launcher...");
- this.InteractivelyDelete(paths.unixLauncher);
- File.Move(paths.unixLauncherBackup, paths.unixLauncher);
- }
- // remove old files
- string[] removePaths = this.GetUninstallPaths(installDir, modsDir)
- .Where(path => Directory.Exists(path) || File.Exists(path))
- .ToArray();
- if (removePaths.Any())
+ /*********
+ ** Step 3: find game folder
+ *********/
+ InstallerPaths paths;
{
- this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files...");
- foreach (string path in removePaths)
- this.InteractivelyDelete(path);
+ /****
+ ** print header
+ ****/
+ this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just a few questions first.");
+ this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}");
+ this.PrintDebug("----------------------------------------------------------------------------");
+ Console.WriteLine();
+
+ /****
+ ** collect details
+ ****/
+ // get game path
+ this.PrintInfo("Where is your game folder?");
+ DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg);
+ if (installDir == null)
+ {
+ this.PrintError("Failed finding your game path.");
+ Console.ReadLine();
+ return;
+ }
+
+ // get folders
+ DirectoryInfo packageDir = platform.IsMono()
+ ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono
+ : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows"));
+ paths = new InstallerPaths(packageDir, installDir, EnvironmentUtility.GetExecutableName(platform));
}
+ Console.Clear();
- /****
- ** Install new files
- ****/
- if (action == ScriptAction.Install)
+
+ /*********
+ ** Step 4: validate assumptions
+ *********/
{
- // copy SMAPI files to game dir
- this.PrintDebug("Adding SMAPI files...");
- foreach (FileInfo sourceFile in packageDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ if (!paths.PackageDir.Exists)
{
- if (sourceFile.Name == this.InstallerFileName)
- continue;
+ this.PrintError(platform == Platform.Windows && paths.PackagePath.Contains(Path.GetTempPath()) && paths.PackagePath.Contains(".zip")
+ ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder."
+ : $"The 'internal/{paths.PackageDir.Name}' package folder is missing (should be at {paths.PackagePath})."
+ );
+ Console.ReadLine();
+ return;
+ }
- string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
- this.InteractivelyDelete(targetPath);
- sourceFile.CopyTo(targetPath);
+ if (!File.Exists(paths.ExecutablePath))
+ {
+ this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
+ Console.ReadLine();
+ return;
}
+ }
+
+
+ /*********
+ ** Step 5: ask what to do
+ *********/
+ ScriptAction action;
+ {
+ /****
+ ** print header
+ ****/
+ this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just one question first.");
+ this.PrintDebug($"Game path: {paths.GamePath}");
+ this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}");
+ this.PrintDebug("----------------------------------------------------------------------------");
+ Console.WriteLine();
- // replace mod launcher (if possible)
- if (platform.IsMono())
+ /****
+ ** ask what to do
+ ****/
+ if (installArg)
+ action = ScriptAction.Install;
+ else if (uninstallArg)
+ action = ScriptAction.Uninstall;
+ else
{
- this.PrintDebug("Safely replacing game launcher...");
- if (File.Exists(paths.unixLauncher))
+ this.PrintInfo("What do you want to do?");
+ Console.WriteLine();
+ this.PrintInfo("[1] Install SMAPI.");
+ this.PrintInfo("[2] Uninstall SMAPI.");
+ Console.WriteLine();
+
+ string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" });
+ switch (choice)
{
- if (!File.Exists(paths.unixLauncherBackup))
- File.Move(paths.unixLauncher, paths.unixLauncherBackup);
- else
- this.InteractivelyDelete(paths.unixLauncher);
+ case "1":
+ action = ScriptAction.Install;
+ break;
+ case "2":
+ action = ScriptAction.Uninstall;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected action key '{choice}'.");
}
+ }
+ }
+ Console.Clear();
+
- File.Move(paths.unixSmapiLauncher, paths.unixLauncher);
+ /*********
+ ** Step 6: apply
+ *********/
+ {
+ /****
+ ** print header
+ ****/
+ this.PrintInfo($"That's all I need! I'll {action.ToString().ToLower()} SMAPI now.");
+ this.PrintDebug($"Game path: {paths.GamePath}");
+ this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}");
+ this.PrintDebug("----------------------------------------------------------------------------");
+ Console.WriteLine();
+
+ /****
+ ** Always uninstall old files
+ ****/
+ // restore game launcher
+ if (platform.IsMono() && File.Exists(paths.UnixBackupLauncherPath))
+ {
+ this.PrintDebug("Removing SMAPI launcher...");
+ this.InteractivelyDelete(paths.UnixLauncherPath);
+ File.Move(paths.UnixBackupLauncherPath, paths.UnixLauncherPath);
}
- // create mods directory (if needed)
- if (!modsDir.Exists)
+ // remove old files
+ string[] removePaths = this.GetUninstallPaths(paths.GameDir, paths.ModsDir)
+ .Where(path => Directory.Exists(path) || File.Exists(path))
+ .ToArray();
+ if (removePaths.Any())
{
- this.PrintDebug("Creating mods directory...");
- modsDir.Create();
+ this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files...");
+ foreach (string path in removePaths)
+ this.InteractivelyDelete(path);
}
- // add or replace bundled mods
- modsDir.Create();
- DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods"));
- if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
+ /****
+ ** Install new files
+ ****/
+ if (action == ScriptAction.Install)
{
- this.PrintDebug("Adding bundled mods...");
+ // copy SMAPI files to game dir
+ this.PrintDebug("Adding SMAPI files...");
+ foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ {
+ if (sourceFile.Name == this.InstallerFileName)
+ continue;
+
+ string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name);
+ this.InteractivelyDelete(targetPath);
+ sourceFile.CopyTo(targetPath);
+ }
- // special case: rename Omegasis' SaveBackup mod
+ // replace mod launcher (if possible)
+ if (platform.IsMono())
{
- DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup"));
- DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "AdvancedSaveBackup"));
- FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json"));
- if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1))
+ this.PrintDebug("Safely replacing game launcher...");
+ if (File.Exists(paths.UnixLauncherPath))
{
- this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}...");
- this.Move(oldFolder, newFolder.FullName);
+ if (!File.Exists(paths.UnixBackupLauncherPath))
+ File.Move(paths.UnixLauncherPath, paths.UnixBackupLauncherPath);
+ else
+ this.InteractivelyDelete(paths.UnixLauncherPath);
}
+
+ File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath);
}
- // add bundled mods
- foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
+ // create mods directory (if needed)
+ if (!paths.ModsDir.Exists)
{
- this.PrintDebug($" adding {sourceDir.Name}...");
+ this.PrintDebug("Creating mods directory...");
+ paths.ModsDir.Create();
+ }
+
+ // add or replace bundled mods
+ DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(paths.PackageDir.FullName, "Mods"));
+ if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
+ {
+ this.PrintDebug("Adding bundled mods...");
- // init/clear target dir
- DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name));
- if (targetDir.Exists)
+ // special case: rename Omegasis' SaveBackup mod
{
- this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles);
- foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos())
+ DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "SaveBackup"));
+ DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "AdvancedSaveBackup"));
+ FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json"));
+ if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1))
{
- if (protectedFiles == null || !protectedFiles.Contains(entry.Name))
- this.InteractivelyDelete(entry.FullName);
+ this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}...");
+ this.Move(oldFolder, newFolder.FullName);
}
}
- else
- targetDir.Create();
- // copy files
- foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
- sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
+ // add bundled mods
+ foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
+ {
+ this.PrintDebug($" adding {sourceDir.Name}...");
+
+ // init/clear target dir
+ DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, sourceDir.Name));
+ if (targetDir.Exists)
+ {
+ this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles);
+ foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos())
+ {
+ if (protectedFiles == null || !protectedFiles.Contains(entry.Name))
+ this.InteractivelyDelete(entry.FullName);
+ }
+ }
+ else
+ targetDir.Create();
+
+ // copy files
+ foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
+ }
+
+ // set SMAPI's color scheme if defined
+ if (scheme != MonitorColorScheme.AutoDetect)
+ {
+ string configPath = Path.Combine(paths.GamePath, "StardewModdingAPI.config.json");
+ string text = File
+ .ReadAllText(configPath)
+ .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}""");
+ File.WriteAllText(configPath, text);
+ }
}
- }
- // remove obsolete appdata mods
- this.InteractivelyRemoveAppDataMods(modsDir, packagedModsDir);
+ // remove obsolete appdata mods
+ this.InteractivelyRemoveAppDataMods(paths.ModsDir, packagedModsDir);
+ }
}
Console.WriteLine();
Console.WriteLine();
- /****
- ** final instructions
- ****/
+
+ /*********
+ ** Step 7: final instructions
+ *********/
if (platform == Platform.Windows)
{
if (action == ScriptAction.Install)
{
this.PrintSuccess("SMAPI is installed! If you use Steam, set your launch options to enable achievements (see smapi.io/install):");
- this.PrintSuccess($" \"{Path.Combine(installDir.FullName, "StardewModdingAPI.exe")}\" %command%");
+ this.PrintSuccess($" \"{Path.Combine(paths.GamePath, "StardewModdingAPI.exe")}\" %command%");
Console.WriteLine();
this.PrintSuccess("If you don't use Steam, launch StardewModdingAPI.exe in your game folder to play with mods.");
}
@@ -460,6 +571,23 @@ namespace StardewModdingApi.Installer
return str;
}
+ /// <summary>Get the display text for a color scheme.</summary>
+ /// <param name="scheme">The color scheme.</param>
+ private string GetDisplayText(MonitorColorScheme scheme)
+ {
+ switch (scheme)
+ {
+ case MonitorColorScheme.AutoDetect:
+ return "auto-detect";
+ case MonitorColorScheme.DarkBackground:
+ return "light text on dark background";
+ case MonitorColorScheme.LightBackground:
+ return "dark text on light background";
+ default:
+ return scheme.ToString();
+ }
+ }
+
/// <summary>Get the value of a key in the Windows HKLM registry.</summary>
/// <param name="key">The full path of the registry key relative to HKLM.</param>
/// <param name="name">The name of the value.</param>
@@ -486,6 +614,10 @@ namespace StardewModdingApi.Installer
return (string)openKey.GetValue(name);
}
+ /// <summary>Print a message without formatting.</summary>
+ /// <param name="text">The text to print.</param>
+ private void PrintPlain(string text) => Console.WriteLine(text);
+
/// <summary>Print a debug message.</summary>
/// <param name="text">The text to print.</param>
private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
@@ -594,17 +726,22 @@ namespace StardewModdingApi.Installer
}
/// <summary>Interactively ask the user to choose a value.</summary>
+ /// <param name="print">A callback which prints a message to the console.</param>
/// <param name="message">The message to print.</param>
/// <param name="options">The allowed options (not case sensitive).</param>
- private string InteractivelyChoose(string message, params string[] options)
+ /// <param name="indent">The indentation to prefix to output.</param>
+ private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
{
+ print = print ?? this.PrintInfo;
+
while (true)
{
- this.PrintInfo(message);
+ print(indent + message);
+ Console.Write(indent);
string input = Console.ReadLine()?.Trim().ToLowerInvariant();
if (!options.Contains(input))
{
- this.PrintInfo("That's not a valid option.");
+ print($"{indent}That's not a valid option.");
continue;
}
return input;
diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
index 2ad7e82a..e82c6093 100644
--- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
+++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
@@ -40,6 +40,7 @@
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Enums\ScriptAction.cs" />
+ <Compile Include="Framework\InstallerPaths.cs" />
<Compile Include="InteractiveInstaller.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs
index 797d4650..7ee662d0 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs
@@ -1,4 +1,4 @@
-namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
+namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
{
/// <summary>An item type that can be searched and added to the player through the console.</summary>
internal enum ItemType
@@ -9,9 +9,6 @@
/// <summary>A <see cref="Boots"/> item.</summary>
Boots,
- /// <summary>A fish item.</summary>
- Fish,
-
/// <summary>A <see cref="Wallpaper"/> flooring item.</summary>
Flooring,
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
index e678d057..7a3d8694 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
@@ -37,14 +37,15 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset, new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 1, new Shears());
yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 2, new Pan());
+ yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 3, new Wand());
// wallpapers
for (int id = 0; id < 112; id++)
- yield return new SearchableItem(ItemType.Wallpaper, id, new Wallpaper(id));
+ yield return new SearchableItem(ItemType.Wallpaper, id, new Wallpaper(id) { Category = SObject.furnitureCategory });
// flooring
for (int id = 0; id < 40; id++)
- yield return new SearchableItem(ItemType.Flooring, id, new Wallpaper(id, isFloor: true));
+ yield return new SearchableItem(ItemType.Flooring, id, new Wallpaper(id, isFloor: true) { Category = SObject.furnitureCategory });
// equipment
foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\Boots").Keys)
@@ -75,10 +76,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
yield return new SearchableItem(ItemType.Furniture, id, new Furniture(id, Vector2.Zero));
}
- // fish
- foreach (int id in Game1.content.Load<Dictionary<int, string>>("Data\\Fish").Keys)
- yield return new SearchableItem(ItemType.Fish, id, new SObject(id, 999));
-
// craftables
foreach (int id in Game1.bigCraftablesInformation.Keys)
yield return new SearchableItem(ItemType.BigCraftable, id, new SObject(Vector2.Zero, id));
@@ -103,16 +100,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
yield return new SearchableItem(ItemType.Object, id, item);
// fruit products
- if (item.category == SObject.FruitsCategory)
+ if (item.Category == SObject.FruitsCategory)
{
// wine
SObject wine = new SObject(348, 1)
{
Name = $"{item.Name} Wine",
- Price = item.price * 3
+ Price = item.Price * 3
};
wine.preserve.Value = SObject.PreserveType.Wine;
- wine.preservedParentSheetIndex.Value = item.parentSheetIndex;
+ wine.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, wine);
// jelly
@@ -122,21 +119,21 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
Price = 50 + item.Price * 2
};
jelly.preserve.Value = SObject.PreserveType.Jelly;
- jelly.preservedParentSheetIndex.Value = item.parentSheetIndex;
+ jelly.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, jelly);
}
// vegetable products
- else if (item.category == SObject.VegetableCategory)
+ else if (item.Category == SObject.VegetableCategory)
{
// juice
SObject juice = new SObject(350, 1)
{
Name = $"{item.Name} Juice",
- Price = (int)(item.price * 2.25d)
+ Price = (int)(item.Price * 2.25d)
};
juice.preserve.Value = SObject.PreserveType.Juice;
- juice.preservedParentSheetIndex.Value = item.parentSheetIndex;
+ juice.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, juice);
// pickled
@@ -146,16 +143,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
Price = 50 + item.Price * 2
};
pickled.preserve.Value = SObject.PreserveType.Pickle;
- pickled.preservedParentSheetIndex.Value = item.parentSheetIndex;
+ pickled.preservedParentSheetIndex.Value = item.ParentSheetIndex;
yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, pickled);
}
// flower honey
- else if (item.category == SObject.flowersCategory)
+ else if (item.Category == SObject.flowersCategory)
{
// get honey type
SObject.HoneyType? type = null;
- switch (item.parentSheetIndex)
+ switch (item.ParentSheetIndex)
{
case 376:
type = SObject.HoneyType.Poppy;
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index f89049c6..a6c5cd88 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,8 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "2.6.0",
+ "Version": "2.7.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
- "EntryDll": "ConsoleCommands.dll"
+ "EntryDll": "ConsoleCommands.dll",
+ "MinimumApiVersion": "2.7.0"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index ee0f2abb..e973b449 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,8 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "2.6.0",
+ "Version": "2.7.0",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
- "EntryDll": "SaveBackup.dll"
+ "EntryDll": "SaveBackup.dll",
+ "MinimumApiVersion": "2.7.0"
}
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index b500e19d..18d55665 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -83,31 +83,25 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Fetch version metadata for the given mods.</summary>
/// <param name="model">The mod search criteria.</param>
[HttpPost]
- public async Task<object> PostAsync([FromBody] ModSearchModel model)
+ public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model)
{
- // parse request data
- ISemanticVersion apiVersion = this.GetApiVersion();
- ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray();
+ if (model?.Mods == null)
+ return new ModEntryModel[0];
// fetch wiki data
WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync();
-
- // fetch data
IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
- foreach (ModSearchEntryModel mod in searchMods)
+ foreach (ModSearchEntryModel mod in model.Mods)
{
if (string.IsNullOrWhiteSpace(mod.ID))
continue;
ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata);
- result.SetBackwardsCompatibility(apiVersion);
mods[mod.ID] = result;
}
- // return in expected structure
- return apiVersion.IsNewerThan("2.6-beta.18")
- ? mods.Values
- : (object)mods;
+ // return data
+ return mods.Values;
}
@@ -231,29 +225,6 @@ namespace StardewModdingAPI.Web.Controllers
return current != null && (other == null || other.IsOlderThan(current));
}
- /// <summary>Get the mods for which the API should return data.</summary>
- /// <param name="model">The search model.</param>
- /// <param name="apiVersion">The requested API version.</param>
- private IEnumerable<ModSearchEntryModel> GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion)
- {
- if (model == null)
- yield break;
-
- // yield standard entries
- if (model.Mods != null)
- {
- foreach (ModSearchEntryModel mod in model.Mods)
- yield return mod;
- }
-
- // yield mod update keys if backwards compatible
- if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17"))
- {
- foreach (string updateKey in model.ModKeys.Distinct())
- yield return new ModSearchEntryModel(updateKey, new[] { updateKey });
- }
- }
-
/// <summary>Get mod data from the wiki compatibility list.</summary>
private async Task<WikiCompatibilityEntry[]> GetWikiDataAsync()
{
diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
index e72efb39..c95abe75 100644
--- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
@@ -51,6 +51,23 @@
* mod is no longer compatible.
*/
"ModData": {
+ /*********
+ ** Content packs
+ *********/
+ "Canon-Friendly Dialogue Expansion": {
+ "ID": "gizzymo.canonfriendlyexpansion",
+ "~1.1.1 | Status": "AssumeBroken" // causes a save crash on certain dates
+ },
+
+ "Everytime Submarine": {
+ "ID": "MustafaDemirel.EverytimeSubmarine",
+ "~1.0.0 | Status": "AssumeBroken" // breaks player saves if their beach bridge is fixed
+ },
+
+
+ /*********
+ ** Mods
+ *********/
"AccessChestAnywhere": {
"ID": "AccessChestAnywhere",
"MapLocalVersions": { "1.1-1078": "1.1" },
@@ -822,7 +839,7 @@
},
"Level Extender": {
- "ID": "DevinLematty.LevelExtender",
+ "ID": "DevinLematty.LevelExtender",
"FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3
"Default | UpdateKey": "Nexus:1471"
},
@@ -1046,7 +1063,7 @@
},
"One Click Shed": {
- "ID": "BitwiseJonMods.OneClickShedReloader",
+ "ID": "BitwiseJonMods.OneClickShedReloader",
"Default | UpdateKey": "Nexus:2052"
},
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index a6cddbe4..bd512fb1 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -29,10 +29,10 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.6.0");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.7.0");
/// <summary>The minimum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.27");
+ public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.28");
/// <summary>The maximum supported version of Stardew Valley.</summary>
public static ISemanticVersion MaximumGameVersion { get; } = null;
@@ -70,11 +70,14 @@ namespace StardewModdingAPI
/// <summary>The file path for the SMAPI metadata file.</summary>
internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json");
- /// <summary>The filename prefix for SMAPI log files.</summary>
- internal static string LogNamePrefix { get; } = "SMAPI-latest";
+ /// <summary>The filename prefix used for all SMAPI logs.</summary>
+ internal static string LogNamePrefix { get; } = "SMAPI-";
+
+ /// <summary>The filename for SMAPI's main log, excluding the <see cref="LogExtension"/>.</summary>
+ internal static string LogFilename { get; } = $"{Constants.LogNamePrefix}latest";
/// <summary>The filename extension for SMAPI log files.</summary>
- internal static string LogNameExtension { get; } = "txt";
+ internal static string LogExtension { get; } = "txt";
/// <summary>A copy of the log leading up to the previous fatal crash, if any.</summary>
internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt");
@@ -86,7 +89,7 @@ namespace StardewModdingAPI
internal static string UpdateMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.update.marker");
/// <summary>The full path to the folder containing mods.</summary>
- internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods");
+ internal static string DefaultModsPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods");
/// <summary>The game's current semantic version.</summary>
internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion());
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index d9b2109a..9eb7b5f9 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -9,6 +9,7 @@ using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Metadata;
+using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@@ -32,6 +33,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Simplifies access to private code.</summary>
private readonly Reflector Reflection;
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ private readonly JsonHelper JsonHelper;
+
/// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
private readonly IList<IContentManager> ContentManagers = new List<IContentManager>();
@@ -67,10 +71,12 @@ namespace StardewModdingAPI.Framework
/// <param name="currentCulture">The current culture for which to localise content.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private code.</param>
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection)
+ /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper)
{
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Reflection = reflection;
+ this.JsonHelper = jsonHelper;
this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory);
this.ContentManagers.Add(
this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing)
@@ -92,7 +98,7 @@ namespace StardewModdingAPI.Framework
/// <param name="rootDirectory">The root directory to search for content (or <c>null</c> for the default).</param>
public ModContentManager CreateModContentManager(string name, string rootDirectory)
{
- ModContentManager manager = new ModContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing);
+ ModContentManager manager = new ModContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.JsonHelper, this.OnDisposing);
this.ContentManagers.Add(manager);
return manager;
}
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 80bf37e9..24ce69ea 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Toolkit.Serialisation;
using StardewValley;
namespace StardewModdingAPI.Framework.ContentManagers
@@ -13,6 +14,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
internal class ModContentManager : BaseContentManager
{
/*********
+ ** Properties
+ *********/
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ private readonly JsonHelper JsonHelper;
+
+
+ /*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
@@ -23,9 +31,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="coordinator">The central coordinator which manages content managers.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private code.</param>
+ /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) { }
+ public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true)
+ {
+ this.JsonHelper = jsonHelper;
+ }
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
/// <typeparam name="T">The type of asset to load.</typeparam>
@@ -95,9 +107,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
case ".xnb":
return base.Load<T>(relativePath, language);
- // unpacked map
- case ".tbin":
- throw GetContentError($"can't read unpacked map file directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper.");
+ // unpacked data
+ case ".json":
+ {
+ if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T data))
+ throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above
+
+ return data;
+ }
// unpacked image
case ".png":
@@ -114,6 +131,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
return (T)(object)texture;
}
+ // unpacked map
+ case ".tbin":
+ throw GetContentError($"can't read unpacked map file directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper.");
+
default:
throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'.");
}
diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index 4a4adb90..62d8b80d 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -54,7 +54,9 @@ namespace StardewModdingAPI.Framework
public TModel ReadJsonFile<TModel>(string path) where TModel : class
{
path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path));
- return this.JsonHelper.ReadJsonFile<TModel>(path);
+ return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model)
+ ? model
+ : null;
}
/// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 671dc21e..a8b24a13 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -259,7 +259,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
// get seasonal name (if applicable)
string seasonalImageSource = null;
- if (Game1.currentSeason != null)
+ if (Context.IsSaveLoaded && Game1.currentSeason != null)
{
string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null.");
bool hasSeasonalPrefix =
diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
index d9498e83..0ba258b4 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -138,7 +138,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
where TModel : class
{
path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path));
- return this.JsonHelper.ReadJsonFile<TModel>(path);
+ return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data)
+ ? data
+ : null;
}
/// <summary>Save to a JSON file.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
index cf5a3175..3a26660f 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
@@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
// method reference
MethodReference methodReference = RewriteHelper.AsMethodReference(instruction);
- if (methodReference != null && this.ShouldValidate(methodReference.DeclaringType))
+ if (methodReference != null && !this.IsUnsupported(methodReference) && this.ShouldValidate(methodReference.DeclaringType))
{
// get potential targets
MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray();
@@ -106,6 +106,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name);
}
+ /// <summary>Get whether a method reference is a special case that's not currently supported (e.g. array methods).</summary>
+ /// <param name="method">The method reference.</param>
+ private bool IsUnsupported(MethodReference method)
+ {
+ return
+ method.DeclaringType.Name.Contains("["); // array methods
+ }
+
/// <summary>Get a shorter type name for display.</summary>
/// <param name="type">The type reference.</param>
private string GetFriendlyTypeName(TypeReference type)
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 05fedc3d..83e8c9a7 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -17,6 +17,7 @@ using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.StateTracking;
using StardewModdingAPI.Framework.Utilities;
+using StardewModdingAPI.Toolkit.Serialisation;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Buildings;
@@ -37,16 +38,6 @@ namespace StardewModdingAPI.Framework
** Properties
*********/
/****
- ** Constructor hack
- ****/
- /// <summary>A static instance of <see cref="Monitor"/> to use while <see cref="Game1"/> is initialising, which happens before the <see cref="SGame"/> constructor runs.</summary>
- internal static IMonitor MonitorDuringInitialisation;
-
- /// <summary>A static instance of <see cref="Reflection"/> to use while <see cref="Game1"/> is initialising, which happens before the <see cref="SGame"/> constructor runs.</summary>
- internal static Reflector ReflectorDuringInitialisation;
-
-
- /****
** SMAPI state
****/
/// <summary>Encapsulates monitoring and logging.</summary>
@@ -83,6 +74,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection;
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ private readonly JsonHelper JsonHelper;
+
/****
** Game state
****/
@@ -105,6 +99,9 @@ namespace StardewModdingAPI.Framework
/*********
** Accessors
*********/
+ /// <summary>Static state to use while <see cref="Game1"/> is initialising, which happens before the <see cref="SGame"/> constructor runs.</summary>
+ internal static SGameConstructorHack ConstructorHack { get; set; }
+
/// <summary>SMAPI's content manager.</summary>
public ContentCoordinator ContentCore { get; private set; }
@@ -132,10 +129,13 @@ namespace StardewModdingAPI.Framework
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private game code.</param>
/// <param name="eventManager">Manages SMAPI events for mods.</param>
+ /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onGameInitialised">A callback to invoke after the game finishes initialising.</param>
/// <param name="onGameExiting">A callback to invoke when the game exits.</param>
- internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, Action onGameInitialised, Action onGameExiting)
+ internal SGame(IMonitor monitor, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, Action onGameInitialised, Action onGameExiting)
{
+ SGame.ConstructorHack = null;
+
// check expectations
if (this.ContentCore == null)
throw new InvalidOperationException($"The game didn't initialise its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change.");
@@ -147,6 +147,7 @@ namespace StardewModdingAPI.Framework
this.Monitor = monitor;
this.Events = eventManager;
this.Reflection = reflection;
+ this.JsonHelper = jsonHelper;
this.OnGameInitialised = onGameInitialised;
this.OnGameExiting = onGameExiting;
Game1.input = new SInputState();
@@ -191,8 +192,7 @@ namespace StardewModdingAPI.Framework
// NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialised at this point.
if (this.ContentCore == null)
{
- this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation);
- SGame.MonitorDuringInitialisation = null;
+ this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper);
this.NextContentManagerIsMain = true;
return this.ContentCore.CreateGameContentManager("Game1._temporaryContent");
}
diff --git a/src/SMAPI/Framework/SGameConstructorHack.cs b/src/SMAPI/Framework/SGameConstructorHack.cs
new file mode 100644
index 00000000..494bab99
--- /dev/null
+++ b/src/SMAPI/Framework/SGameConstructorHack.cs
@@ -0,0 +1,37 @@
+using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>The static state to use while <see cref="Game1"/> is initialising, which happens before the <see cref="SGame"/> constructor runs.</summary>
+ internal class SGameConstructorHack
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Encapsulates monitoring and logging.</summary>
+ public IMonitor Monitor { get; }
+
+ /// <summary>Simplifies access to private game code.</summary>
+ public Reflector Reflection { get; }
+
+ /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
+ public JsonHelper JsonHelper { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="monitor">Encapsulates monitoring and logging.</param>
+ /// <param name="reflection">Simplifies access to private game code.</param>
+ /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
+ public SGameConstructorHack(IMonitor monitor, Reflector reflection, JsonHelper jsonHelper)
+ {
+ this.Monitor = monitor;
+ this.Reflection = reflection;
+ this.JsonHelper = jsonHelper;
+ }
+ }
+}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 12abeb10..8487b6be 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -13,6 +13,7 @@ using StardewValley.Menus;
using StardewValley.Objects;
using StardewValley.Projectiles;
using StardewValley.TerrainFeatures;
+using xTile.Tiles;
namespace StardewModdingAPI.Metadata
{
@@ -63,6 +64,23 @@ namespace StardewModdingAPI.Metadata
/// <returns>Returns any non-null value to indicate an asset was loaded.</returns>
private object PropagateImpl(LocalizedContentManager content, string key)
{
+ /****
+ ** Special case: current map tilesheet
+ ** We only need to do this for the current location, since tilesheets are reloaded when you enter a location.
+ ** Just in case, we should still propagate by key even if a tilesheet is matched.
+ ****/
+ if (Game1.currentLocation?.map?.TileSheets != null)
+ {
+ foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
+ {
+ if (this.GetNormalisedPath(tilesheet.ImageSource) == key)
+ Game1.mapDisplayDevice.LoadTileSheet(tilesheet);
+ }
+ }
+
+ /****
+ ** Propagate by key
+ ****/
Reflector reflection = this.Reflection;
switch (key.ToLower().Replace("/", "\\")) // normalised key so we can compare statically
{
@@ -313,21 +331,21 @@ namespace StardewModdingAPI.Metadata
if (this.IsInFolder(key, "Buildings"))
return this.ReloadBuildings(content, key);
- if (this.IsInFolder(key, "Characters"))
- return this.ReloadNpcSprites(content, key, monster: false);
-
- if (this.IsInFolder(key, "Characters\\Monsters"))
- return this.ReloadNpcSprites(content, key, monster: true);
+ if (this.IsInFolder(key, "Characters") || this.IsInFolder(key, "Characters\\Monsters"))
+ return this.ReloadNpcSprites(content, key);
- if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"), StringComparison.InvariantCultureIgnoreCase))
- return this.ReloadFenceTextures(content, key);
+ if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
+ return this.ReloadFenceTextures(key);
if (this.IsInFolder(key, "Portraits"))
return this.ReloadNpcPortraits(content, key);
// dynamic data
+ if (this.IsInFolder(key, "Characters\\Dialogue"))
+ return this.ReloadNpcDialogue(key);
+
if (this.IsInFolder(key, "Characters\\schedules"))
- return this.ReloadNpcSchedules(content, key);
+ return this.ReloadNpcSchedules(key);
return false;
}
@@ -416,10 +434,9 @@ namespace StardewModdingAPI.Metadata
}
/// <summary>Reload the sprites for a fence type.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
/// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadFenceTextures(LocalizedContentManager content, string key)
+ private bool ReloadFenceTextures(string key)
{
// get fence type
if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType))
@@ -446,13 +463,13 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload the sprites for matching NPCs.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
- /// <param name="monster">Whether to match monsters (<c>true</c>) or non-monsters (<c>false</c>).</param>
/// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadNpcSprites(LocalizedContentManager content, string key, bool monster)
+ private bool ReloadNpcSprites(LocalizedContentManager content, string key)
{
// get NPCs
- string name = this.GetNpcNameFromFileName(Path.GetFileName(key));
- NPC[] characters = this.GetCharacters().Where(npc => npc.Name == name && npc.IsMonster == monster).ToArray();
+ NPC[] characters = this.GetCharacters()
+ .Where(npc => this.GetNormalisedPath(npc.Sprite.textureName.Value) == key)
+ .ToArray();
if (!characters.Any())
return false;
@@ -470,15 +487,20 @@ namespace StardewModdingAPI.Metadata
private bool ReloadNpcPortraits(LocalizedContentManager content, string key)
{
// get NPCs
- string name = this.GetNpcNameFromFileName(Path.GetFileName(key));
- NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
+ NPC[] villagers = this.GetCharacters()
+ .Where(npc => npc.isVillager() && this.GetNormalisedPath($"Portraits\\{this.Reflection.GetMethod(npc, "getTextureName").Invoke<string>()}") == key)
+ .ToArray();
if (!villagers.Any())
return false;
// update portrait
Texture2D texture = content.Load<Texture2D>(key);
foreach (NPC villager in villagers)
+ {
+ villager.resetPortrait();
villager.Portrait = texture;
+ }
+
return true;
}
@@ -508,11 +530,27 @@ namespace StardewModdingAPI.Metadata
/****
** Reload data methods
****/
+ /// <summary>Reload the dialogue data for matching NPCs.</summary>
+ /// <param name="key">The asset key to reload.</param>
+ /// <returns>Returns whether any assets were reloaded.</returns>
+ private bool ReloadNpcDialogue(string key)
+ {
+ // get NPCs
+ string name = Path.GetFileName(key);
+ NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
+ if (!villagers.Any())
+ return false;
+
+ // update dialogue
+ foreach (NPC villager in villagers)
+ villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue
+ return true;
+ }
+
/// <summary>Reload the schedules for matching NPCs.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
/// <returns>Returns whether any assets were reloaded.</returns>
- private bool ReloadNpcSchedules(LocalizedContentManager content, string key)
+ private bool ReloadNpcSchedules(string key)
{
// get NPCs
string name = Path.GetFileName(key);
@@ -607,6 +645,14 @@ namespace StardewModdingAPI.Metadata
}
}
+ /// <summary>Get whether a key starts with a substring after the substring is normalised.</summary>
+ /// <param name="key">The key to check.</param>
+ /// <param name="rawSubstring">The substring to normalise and find.</param>
+ private bool KeyStartsWith(string key, string rawSubstring)
+ {
+ return key.StartsWith(this.GetNormalisedPath(rawSubstring), StringComparison.InvariantCultureIgnoreCase);
+ }
+
/// <summary>Get whether a normalised asset key is in the given folder.</summary>
/// <param name="key">The normalised asset key (like <c>Animals/cat</c>).</param>
/// <param name="folder">The key folder (like <c>Animals</c>); doesn't need to be normalised.</param>
@@ -614,7 +660,7 @@ namespace StardewModdingAPI.Metadata
private bool IsInFolder(string key, string folder, bool allowSubfolders = false)
{
return
- key.StartsWith(this.GetNormalisedPath($"{folder}\\"), StringComparison.InvariantCultureIgnoreCase)
+ this.KeyStartsWith(key, $"{folder}\\")
&& (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1);
}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index 6012b15a..634c5066 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -11,7 +11,6 @@ using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
-using Microsoft.Xna.Framework.Input;
#if SMAPI_FOR_WINDOWS
using System.Windows.Forms;
#endif
@@ -32,10 +31,8 @@ using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Serialisation;
-using StardewModdingAPI.Toolkit.Serialisation.Converters;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
-using Keys = Microsoft.Xna.Framework.Input.Keys;
using Monitor = StardewModdingAPI.Framework.Monitor;
using SObject = StardewValley.Object;
using ThreadState = System.Threading.ThreadState;
@@ -103,6 +100,9 @@ namespace StardewModdingAPI
/// <summary>The mod toolkit used for generic mod interactions.</summary>
private readonly ModToolkit Toolkit = new ModToolkit();
+ /// <summary>The path to search for mods.</summary>
+ private readonly string ModsPath;
+
/*********
** Public methods
@@ -116,18 +116,34 @@ namespace StardewModdingAPI
// get flags from arguments
bool writeToConsole = !args.Contains("--no-terminal");
+ // get mods path from arguments
+ string modsPath = null;
+ {
+ int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1;
+ if (pathIndex >= 1 && args.Length >= pathIndex)
+ {
+ modsPath = args[pathIndex];
+ if (!string.IsNullOrWhiteSpace(modsPath) && !Path.IsPathRooted(modsPath))
+ modsPath = Path.Combine(Constants.ExecutionPath, modsPath);
+ }
+ if (string.IsNullOrWhiteSpace(modsPath))
+ modsPath = Constants.DefaultModsPath;
+ }
+
// load SMAPI
- using (Program program = new Program(writeToConsole))
+ using (Program program = new Program(modsPath, writeToConsole))
program.RunInteractively();
}
/// <summary>Construct an instance.</summary>
+ /// <param name="modsPath">The path to search for mods.</param>
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
- public Program(bool writeToConsole)
+ public Program(string modsPath, bool writeToConsole)
{
// init paths
- this.VerifyPath(Constants.ModPath);
+ this.VerifyPath(modsPath);
this.VerifyPath(Constants.LogDir);
+ this.ModsPath = modsPath;
// init log file
this.PurgeLogFiles();
@@ -146,7 +162,9 @@ namespace StardewModdingAPI
// init logging
this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
- this.Monitor.Log($"Mods go here: {Constants.ModPath}");
+ this.Monitor.Log($"Mods go here: {modsPath}");
+ if (modsPath != Constants.DefaultModsPath)
+ this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace);
this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace);
// validate game version
@@ -193,9 +211,6 @@ namespace StardewModdingAPI
// init JSON parser
JsonConverter[] converters = {
- new StringEnumConverter<Buttons>(),
- new StringEnumConverter<Keys>(),
- new StringEnumConverter<SButton>(),
new ColorConverter(),
new PointConverter(),
new RectangleConverter()
@@ -214,9 +229,8 @@ namespace StardewModdingAPI
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name);
// override game
- SGame.MonitorDuringInitialisation = this.Monitor;
- SGame.ReflectorDuringInitialisation = this.Reflection;
- this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart, this.Dispose);
+ SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper);
+ this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.InitialiseAfterGameStart, this.Dispose);
StardewValley.Program.gamePtr = this.GameInstance;
// add exit handler
@@ -418,7 +432,7 @@ namespace StardewModdingAPI
ModResolver resolver = new ModResolver();
// load manifests
- IModMetadata[] mods = resolver.ReadManifests(toolkit, Constants.ModPath, modDatabase).ToArray();
+ IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray();
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
// process dependencies
@@ -435,10 +449,10 @@ namespace StardewModdingAPI
Exported = DateTime.UtcNow.ToString("O"),
ApiVersion = Constants.ApiVersion.ToString(),
GameVersion = Constants.GameVersion.ToString(),
- ModFolderPath = Constants.ModPath,
+ ModFolderPath = this.ModsPath,
Mods = mods
};
- this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export);
+ this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export);
}
// check for updates
@@ -746,7 +760,7 @@ namespace StardewModdingAPI
// load content packs
foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack))
{
- this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)})...", LogLevel.Trace);
+ this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)})...", LogLevel.Trace);
// show warning for missing update key
if (metadata.HasManifest() && !metadata.HasUpdateKeys())
@@ -791,7 +805,7 @@ namespace StardewModdingAPI
// get basic info
IManifest manifest = metadata.Manifest;
this.Monitor.Log(metadata.Manifest?.EntryDll != null
- ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(Constants.ModPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid
+ ? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid
: $" {metadata.DisplayName}...", LogLevel.Trace);
// show warnings
@@ -884,33 +898,15 @@ namespace StardewModdingAPI
}
IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray();
- // log skipped mods
- this.Monitor.Newline();
- if (skippedMods.Any())
- {
- this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error);
- foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName))
- {
- IModMetadata mod = pair.Key;
- string[] reason = pair.Value;
-
- this.Monitor.Log($" {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error);
- if (reason[1] != null)
- this.Monitor.Log($" {reason[1]}", LogLevel.Trace);
- }
- this.Monitor.Newline();
- }
-
// log loaded mods
this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info);
-
foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName))
{
IManifest manifest = metadata.Manifest;
this.Monitor.Log(
$" {metadata.DisplayName} {manifest.Version}"
- + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "")
- + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""),
+ + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "")
+ + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""),
LogLevel.Info
);
}
@@ -936,27 +932,8 @@ namespace StardewModdingAPI
this.Monitor.Newline();
}
- // log warnings
- {
- IModMetadata[] modsWithWarnings = this.ModRegistry.GetAll().Where(p => p.Warnings != ModWarning.None).ToArray();
- if (modsWithWarnings.Any())
- {
- this.Monitor.Log($"Found issues with {modsWithWarnings.Length} mods:", LogLevel.Warn);
- foreach (IModMetadata metadata in modsWithWarnings)
- {
- string[] warnings = this.GetWarningText(metadata.Warnings).ToArray();
- if (warnings.Length == 1)
- this.Monitor.Log($" {metadata.DisplayName} {warnings[0]}", LogLevel.Warn);
- else
- {
- this.Monitor.Log($" {metadata.DisplayName}:", LogLevel.Warn);
- foreach (string warning in warnings)
- this.Monitor.Log(" - " + warning, LogLevel.Warn);
- }
- }
- this.Monitor.Newline();
- }
- }
+ // log mod warnings
+ this.LogModWarnings(this.ModRegistry.GetAll().ToArray(), skippedMods);
// initialise translations
this.ReloadTranslations(loadedMods);
@@ -1047,23 +1024,87 @@ namespace StardewModdingAPI
this.ModRegistry.AreAllModsInitialised = true;
}
- /// <summary>Get the warning text for a mod warning bit mask.</summary>
- /// <param name="mask">The mod warning bit mask.</param>
- private IEnumerable<string> GetWarningText(ModWarning mask)
+ /// <summary>Write a summary of mod warnings to the console and log.</summary>
+ /// <param name="mods">The loaded mods.</param>
+ /// <param name="skippedMods">The mods which were skipped, along with the friendly and developer reasons.</param>
+ private void LogModWarnings(IModMetadata[] mods, IDictionary<IModMetadata, string[]> skippedMods)
{
- if (mask.HasFlag(ModWarning.BrokenCodeLoaded))
- yield return "has broken code, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.";
- if (mask.HasFlag(ModWarning.ChangesSaveSerialiser))
- yield return "accesses the save serialiser and may break your saves.";
- if (mask.HasFlag(ModWarning.PatchesGame))
- yield return "patches the game. This may cause errors or bugs in-game. If you have issues, try removing this mod first.";
- if (mask.HasFlag(ModWarning.UsesUnvalidatedUpdateTick))
- yield return "bypasses normal SMAPI event protections. This may cause errors or save corruption. If you have issues, try removing this mod first.";
- if (mask.HasFlag(ModWarning.UsesDynamic))
- yield return "uses the 'dynamic' keyword. This won't work on Linux/Mac.";
- if (mask.HasFlag(ModWarning.NoUpdateKeys))
- yield return "has no update keys in its manifest. SMAPI won't show update alerts for this mod.";
+ // get mods with warnings
+ IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray();
+ if (!modsWithWarnings.Any() && !skippedMods.Any())
+ return;
+
+ // log intro
+ {
+ int count = modsWithWarnings.Union(skippedMods.Keys).Count();
+ this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info);
+ }
+
+ // log skipped mods
+ if (skippedMods.Any())
+ {
+ this.Monitor.Log(" Skipped mods", LogLevel.Error);
+ this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error);
+ this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error);
+ this.Monitor.Newline();
+ foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName))
+ {
+ IModMetadata mod = pair.Key;
+ string[] reason = pair.Value;
+
+ this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error);
+ if (reason[1] != null)
+ this.Monitor.Log($" ({reason[1]})", LogLevel.Trace);
+ }
+ this.Monitor.Newline();
+ }
+
+ // log warnings
+ if (modsWithWarnings.Any())
+ {
+ // issue block format logic
+ void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb)
+ {
+ IModMetadata[] matches = modsWithWarnings.Where(p => p.Warnings.HasFlag(warning)).ToArray();
+ if (!matches.Any())
+ return;
+
+ this.Monitor.Log(" " + heading, logLevel);
+ this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel);
+ foreach (string line in blurb)
+ this.Monitor.Log(" " + line, logLevel);
+ this.Monitor.Newline();
+ foreach (IModMetadata match in matches)
+ this.Monitor.Log($" - {match.DisplayName}", logLevel);
+ this.Monitor.Newline();
+ }
+
+ // supported issues
+ LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods",
+ "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,",
+ "errors, or crashes in-game."
+ );
+ LogWarningGroup(ModWarning.ChangesSaveSerialiser, LogLevel.Warn, "Changed save serialiser",
+ "These mods change the save serialiser. They may corrupt your save files, or make them unusable if",
+ "you uninstall these mods."
+ );
+ LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code",
+ "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if",
+ "your game has issues, try removing these first. Otherwise you can ignore this warning."
+ );
+ LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks",
+ "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save",
+ "corruption. If your game has issues, try removing these first."
+ );
+ LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys",
+ "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these",
+ "mods. Consider notifying the mod authors about this problem."
+ );
+ LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform",
+ "These mods use the 'dynamic' keyword, and won't work on Linux/Mac."
+ );
+ }
}
/// <summary>Load a mod's entry class.</summary>
@@ -1118,7 +1159,10 @@ namespace StardewModdingAPI
string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim());
try
{
- translations[locale] = jsonHelper.ReadJsonFile<IDictionary<string, string>>(file.FullName);
+ if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary<string, string> data))
+ translations[locale] = data;
+ else
+ metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed.");
}
catch (Exception ex)
{
@@ -1260,7 +1304,7 @@ namespace StardewModdingAPI
{
// default path
{
- FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.{Constants.LogNameExtension}"));
+ FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}"));
if (!defaultFile.Exists)
return defaultFile.FullName;
}
@@ -1268,7 +1312,7 @@ namespace StardewModdingAPI
// get first disambiguated path
for (int i = 2; i < int.MaxValue; i++)
{
- FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.player-{i}.{Constants.LogNameExtension}"));
+ FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}"));
if (!file.Exists)
return file.FullName;
}
diff --git a/src/SMAPI/SButton.cs b/src/SMAPI/SButton.cs
index 3f95169a..bc76c91d 100644
--- a/src/SMAPI/SButton.cs
+++ b/src/SMAPI/SButton.cs
@@ -604,21 +604,21 @@ namespace StardewModdingAPI
*********/
/// <summary>Get the <see cref="SButton"/> equivalent for the given button.</summary>
/// <param name="key">The keyboard button to convert.</param>
- internal static SButton ToSButton(this Keys key)
+ public static SButton ToSButton(this Keys key)
{
return (SButton)key;
}
/// <summary>Get the <see cref="SButton"/> equivalent for the given button.</summary>
/// <param name="key">The controller button to convert.</param>
- internal static SButton ToSButton(this Buttons key)
+ public static SButton ToSButton(this Buttons key)
{
return (SButton)(SButtonExtensions.ControllerOffset + key);
}
/// <summary>Get the <see cref="SButton"/> equivalent for the given button.</summary>
/// <param name="input">The Stardew Valley button to convert.</param>
- internal static SButton ToSButton(this InputButton input)
+ public static SButton ToSButton(this InputButton input)
{
// derived from InputButton constructors
if (input.mouseLeft)
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 0d0a5fe9..fc2d45ba 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -109,6 +109,7 @@
<Compile Include="Events\WorldBuildingListChangedEventArgs.cs" />
<Compile Include="Events\WorldLocationListChangedEventArgs.cs" />
<Compile Include="Events\WorldObjectListChangedEventArgs.cs" />
+ <Compile Include="Framework\SGameConstructorHack.cs" />
<Compile Include="Framework\ContentManagers\BaseContentManager.cs" />
<Compile Include="Framework\ContentManagers\GameContentManager.cs" />
<Compile Include="Framework\ContentManagers\IContentManager.cs" />
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
index f3f22b93..2aafe199 100644
--- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
@@ -1,6 +1,3 @@
-using System;
-using Newtonsoft.Json;
-
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
/// <summary>Metadata about a mod.</summary>
@@ -26,46 +23,5 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The errors that occurred while fetching update data.</summary>
public string[] Errors { get; set; } = new string[0];
-
- /****
- ** Backwards-compatible fields
- ****/
- /// <summary>The mod's latest version number.</summary>
- [Obsolete("Use " + nameof(ModEntryModel.Main))]
- [JsonProperty]
- internal string Version { get; private set; }
-
- /// <summary>The mod's web URL.</summary>
- [Obsolete("Use " + nameof(ModEntryModel.Main))]
- [JsonProperty]
- internal string Url { get; private set; }
-
- /// <summary>The mod's latest optional release, if newer than <see cref="Version"/>.</summary>
- [Obsolete("Use " + nameof(ModEntryModel.Optional))]
- [JsonProperty]
- internal string PreviewVersion { get; private set; }
-
- /// <summary>The web URL to the mod's latest optional release, if newer than <see cref="Version"/>.</summary>
- [Obsolete("Use " + nameof(ModEntryModel.Optional))]
- [JsonProperty]
- internal string PreviewUrl { get; private set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Set backwards-compatible fields.</summary>
- /// <param name="version">The requested API version.</param>
- public void SetBackwardsCompatibility(ISemanticVersion version)
- {
- if (version.IsOlderThan("2.6-beta.19"))
- {
- this.Version = this.Main?.Version?.ToString();
- this.Url = this.Main?.Url;
-
- this.PreviewVersion = this.Optional?.Version?.ToString();
- this.PreviewUrl = this.Optional?.Url;
- }
- }
}
}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs
index df0d8457..e352e1cc 100644
--- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs
+++ b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs
@@ -1,4 +1,3 @@
-using System;
using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
@@ -9,10 +8,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/*********
** Accessors
*********/
- /// <summary>The namespaced mod keys to search.</summary>
- [Obsolete]
- public string[] ModKeys { get; set; }
-
/// <summary>The mods for which to find data.</summary>
public ModSearchEntryModel[] Mods { get; set; }
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index de8d0f02..f1cce4a4 100644
--- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -51,8 +51,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{
try
{
- manifest = this.JsonHelper.ReadJsonFile<Manifest>(manifestFile.FullName);
- if (manifest == null)
+ if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest))
manifestError = "its manifest is invalid.";
}
catch (SParseException ex)
diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs
deleted file mode 100644
index 13e6e3a1..00000000
--- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/StringEnumConverter.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using Newtonsoft.Json.Converters;
-
-namespace StardewModdingAPI.Toolkit.Serialisation.Converters
-{
- /// <summary>A variant of <see cref="StringEnumConverter"/> which only converts a specified enum.</summary>
- /// <typeparam name="T">The enum type.</typeparam>
- internal class StringEnumConverter<T> : StringEnumConverter
- {
- /*********
- ** Public methods
- *********/
- /// <summary>Get whether this instance can convert the specified object type.</summary>
- /// <param name="type">The object type.</param>
- public override bool CanConvert(Type type)
- {
- return
- base.CanConvert(type)
- && (Nullable.GetUnderlyingType(type) ?? type) == typeof(T);
- }
- }
-}
diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs
index 00f334ad..cc8eeb73 100644
--- a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs
+++ b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
using StardewModdingAPI.Toolkit.Serialisation.Converters;
namespace StardewModdingAPI.Toolkit.Serialisation
@@ -17,7 +18,11 @@ namespace StardewModdingAPI.Toolkit.Serialisation
{
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
- Converters = new List<JsonConverter> { new SemanticVersionConverter() }
+ Converters = new List<JsonConverter>
+ {
+ new SemanticVersionConverter(),
+ new StringEnumConverter()
+ }
};
@@ -27,10 +32,11 @@ namespace StardewModdingAPI.Toolkit.Serialisation
/// <summary>Read a JSON file.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="fullPath">The absolete file path.</param>
- /// <returns>Returns the deserialised model, or <c>null</c> if the file doesn't exist or is empty.</returns>
- /// <exception cref="InvalidOperationException">The given path is empty or invalid.</exception>
- public TModel ReadJsonFile<TModel>(string fullPath)
- where TModel : class
+ /// <param name="result">The parsed content model.</param>
+ /// <returns>Returns false if the file doesn't exist, else true.</returns>
+ /// <exception cref="ArgumentException">The given <paramref name="fullPath"/> is empty or invalid.</exception>
+ /// <exception cref="JsonReaderException">The file contains invalid JSON.</exception>
+ public bool ReadJsonFileIfExists<TModel>(string fullPath, out TModel result)
{
// validate
if (string.IsNullOrWhiteSpace(fullPath))
@@ -44,13 +50,15 @@ namespace StardewModdingAPI.Toolkit.Serialisation
}
catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException)
{
- return null;
+ result = default(TModel);
+ return false;
}
// deserialise model
try
{
- return this.Deserialise<TModel>(json);
+ result = this.Deserialise<TModel>(json);
+ return true;
}
catch (Exception ex)
{