diff options
Diffstat (limited to 'src')
43 files changed, 1373 insertions, 447 deletions
diff --git a/src/.editorconfig b/src/.editorconfig index 4271803d..a5cdcf97 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -12,13 +12,16 @@ insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 +[*.csproj] +indent_size = 2 +insert_final_newline = false + [*.json] indent_size = 2 ########## ## C# formatting ## documentation: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference -## some undocumented settings from: https://github.com/dotnet/roslyn/blob/master/.editorconfig ########## [*.cs] @@ -47,6 +50,7 @@ dotnet_style_explicit_tuple_names = true:suggestion csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion # prefer method block bodies csharp_style_expression_bodied_methods = false:suggestion @@ -59,3 +63,6 @@ csharp_style_expression_bodied_accessors = true:suggestion # prefer inline out variables csharp_style_inlined_variable_declaration = true:warning + +# avoid superfluous braces +csharp_prefer_braces = false:suggestion diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index d9a01635..d496fbd1 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -1,6 +1,6 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.15.1.0")] -[assembly: AssemblyFileVersion("1.15.1.0")] +[assembly: AssemblyVersion("1.15.2.0")] +[assembly: AssemblyFileVersion("1.15.2.0")] diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index 7a12a8e9..8416bd51 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -68,6 +68,6 @@ <ItemGroup> <None Include="packages.config" /> </ItemGroup> - <Import Project="$(SolutionDir)\crossplatform.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(SolutionDir)\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 78d3d10e..01288f33 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -135,6 +135,33 @@ namespace StardewModdingApi.Installer /// </remarks> public void Run(string[] args) { +#if SMAPI_1_x + bool installArg = false; + bool uninstallArg = false; + string gamePathArg = null; +#else + /**** + ** 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]; + } +#endif + /**** ** collect details ****/ @@ -142,9 +169,17 @@ namespace StardewModdingApi.Installer Platform platform = this.DetectPlatform(); this.PrintDebug($"Platform: {(platform == Platform.Windows ? "Windows" : "Linux or Mac")}."); + // 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 = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", platform.ToString())); - DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform); DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); var paths = new { @@ -203,13 +238,19 @@ namespace StardewModdingApi.Installer /**** ** ask user what to do ****/ - Console.WriteLine("You can...."); - Console.WriteLine("[1] Install SMAPI."); - Console.WriteLine("[2] Uninstall SMAPI."); - Console.WriteLine(); - ScriptAction action; + + if (installArg) + action = ScriptAction.Install; + else if (uninstallArg) + action = ScriptAction.Uninstall; + else { + Console.WriteLine("You can...."); + Console.WriteLine("[1] Install SMAPI."); + Console.WriteLine("[2] Uninstall SMAPI."); + Console.WriteLine(); + string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2"); switch (choice) { @@ -222,8 +263,8 @@ namespace StardewModdingApi.Installer default: throw new InvalidOperationException($"Unexpected action key '{choice}'."); } + Console.WriteLine(); } - Console.WriteLine(); /**** ** Always uninstall old files @@ -513,13 +554,31 @@ namespace StardewModdingApi.Installer /// <summary>Interactively locate the game install path to update.</summary> /// <param name="platform">The current platform.</param> - private DirectoryInfo InteractivelyGetInstallPath(Platform platform) + /// <param name="specifiedPath">The path specified as a command-line argument (if any), which should override automatic path detection.</param> + private DirectoryInfo InteractivelyGetInstallPath(Platform platform, string specifiedPath) { // get executable name string executableFilename = platform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"; + // validate specified path + if (specifiedPath != null) + { + var dir = new DirectoryInfo(specifiedPath); + if (!dir.Exists) + { + this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't exist."); + return null; + } + if (!dir.EnumerateFiles(executableFilename).Any()) + { + this.PrintError($"You specified --game-path \"{specifiedPath}\", but that folder doesn't contain the Stardew Valley executable."); + return null; + } + return dir; + } + // get installed paths DirectoryInfo[] defaultPaths = ( diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index 765364dc..58ce519c 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -51,6 +51,6 @@ </Content> </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="$(SolutionDir)\crossplatform.targets" /> + <Import Project="$(SolutionDir)\common.targets" /> <Import Project="$(SolutionDir)\prepare-install-package.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index eda3a425..fc84ca29 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken }); + this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" }}); // act new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj index 9bfd7567..f3dbcdd4 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj @@ -30,17 +30,17 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> - <Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> - <HintPath>..\packages\Castle.Core.4.0.0\lib\net45\Castle.Core.dll</HintPath> + <Reference Include="Castle.Core, Version=4.1.1.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> + <HintPath>..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll</HintPath> </Reference> - <Reference Include="Moq, Version=4.7.10.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> - <HintPath>..\packages\Moq.4.7.10\lib\net45\Moq.dll</HintPath> + <Reference Include="Moq, Version=4.7.99.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> + <HintPath>..\packages\Moq.4.7.99\lib\net45\Moq.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> - <Reference Include="nunit.framework, Version=3.6.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> - <HintPath>..\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll</HintPath> + <Reference Include="nunit.framework, Version=3.7.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL"> + <HintPath>..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll</HintPath> </Reference> <Reference Include="System" /> </ItemGroup> @@ -64,6 +64,6 @@ <Name>StardewModdingAPI</Name> </ProjectReference> </ItemGroup> - <Import Project="$(SolutionDir)\crossplatform.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(SolutionDir)\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs b/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs index 95d0d74f..03cd26c9 100644 --- a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; using NUnit.Framework; +using StardewModdingAPI.Framework; namespace StardewModdingAPI.Tests.Utilities { @@ -206,6 +208,51 @@ namespace StardewModdingAPI.Tests.Utilities return version.IsBetween(lower, upper); } + /**** + ** Serialisable + ****/ + [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] + [TestCase("1.0")] + public void Serialisable(string versionStr) + { + // act + string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr)); + SemanticVersion after = JsonConvert.DeserializeObject<SemanticVersion>(json); + + // assert + Assert.IsNotNull(after, "The semantic version after deserialisation is unexpectedly null."); + Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialisation doesn't match the input version."); + } + + /**** + ** GameVersion + ****/ + [Test(Description = "Assert that the GameVersion subclass correctly parses legacy game versions.")] + [TestCase("1.0")] + [TestCase("1.01")] + [TestCase("1.02")] + [TestCase("1.03")] + [TestCase("1.04")] + [TestCase("1.05")] + [TestCase("1.051")] + [TestCase("1.051b")] + [TestCase("1.06")] + [TestCase("1.07")] + [TestCase("1.07a")] + [TestCase("1.1")] + [TestCase("1.11")] + [TestCase("1.2")] + [TestCase("1.2.15")] + public void GameVersion(string versionStr) + { + // act + GameVersion version = new GameVersion(versionStr); + + // assert + Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value."); + Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); + } + /********* ** Private methods diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/StardewModdingAPI.Tests/packages.config index ba954308..6f04e625 100644 --- a/src/StardewModdingAPI.Tests/packages.config +++ b/src/StardewModdingAPI.Tests/packages.config @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="Castle.Core" version="4.0.0" targetFramework="net45" /> - <package id="Moq" version="4.7.10" targetFramework="net45" /> + <package id="Castle.Core" version="4.1.1" targetFramework="net45" /> + <package id="Moq" version="4.7.99" targetFramework="net45" /> <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net45" /> - <package id="NUnit" version="3.6.1" targetFramework="net452" /> + <package id="NUnit" version="3.7.1" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 4d27e51b..9c3f18f8 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -1,7 +1,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.14 +VisualStudioVersion = 15.0.26430.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" EndProject @@ -12,8 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86 .editorconfig = .editorconfig ..\.gitattributes = ..\.gitattributes ..\.gitignore = ..\.gitignore + common.targets = common.targets ..\CONTRIBUTING.md = ..\CONTRIBUTING.md - crossplatform.targets = crossplatform.targets GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs ..\LICENSE = ..\LICENSE prepare-install-package.targets = prepare-install-package.targets diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 3bd31c2d..7a790f1a 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -9,6 +9,7 @@ using StardewModdingAPI.AssemblyRewriters.Finders; using StardewModdingAPI.AssemblyRewriters.Rewriters; using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework; using StardewValley; namespace StardewModdingAPI @@ -35,7 +36,7 @@ namespace StardewModdingAPI /// <summary>SMAPI's current semantic version.</summary> public static ISemanticVersion ApiVersion { get; } = #if SMAPI_1_x - new SemanticVersion(1, 15, 1); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} + new SemanticVersion(1, 15, 2); #else new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); #endif @@ -86,7 +87,7 @@ namespace StardewModdingAPI internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); /// <summary>The game's current semantic version.</summary> - internal static ISemanticVersion GameVersion { get; } = Constants.GetGameVersion(); + internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// <summary>The target game platform.</summary> internal static Platform TargetPlatform { get; } = @@ -98,7 +99,7 @@ namespace StardewModdingAPI /********* - ** Protected methods + ** Internal methods *********/ /// <summary>Get metadata for mapping assemblies to the current platform.</summary> /// <param name="targetPlatform">The target game platform.</param> @@ -179,15 +180,6 @@ namespace StardewModdingAPI new TypeFinder("StardewModdingAPI.Command"), new TypeFinder("StardewModdingAPI.Config"), new TypeFinder("StardewModdingAPI.Log"), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), @@ -198,6 +190,15 @@ namespace StardewModdingAPI new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), #endif /**** @@ -219,19 +220,10 @@ namespace StardewModdingAPI }; } - /// <summary>Get game current version as it should be displayed to players.</summary> - /// <param name="version">The semantic game version.</param> - internal static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) - { - switch (version.ToString()) - { - case "1.1.1": - return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 - default: - return version; - } - } + /********* + ** Private methods + *********/ /// <summary>Get the name of a save directory for the current player.</summary> private static string GetSaveFolderName() { @@ -239,20 +231,14 @@ namespace StardewModdingAPI return $"{prefix}_{Game1.uniqueIDForThisGame}"; } - /// <summary>Get the game's current semantic version.</summary> - private static ISemanticVersion GetGameVersion() + /// <summary>Get the game's current version string.</summary> + private static string GetGameVersion() { - // get raw version // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); if (field == null) throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); - string version = (string)field.GetValue(null); - - // get semantic version - if (version == "1.11") - version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks - return new SemanticVersion(version); + return (string)field.GetValue(null); } } } diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 6c5ae40e..119e14c8 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -16,8 +16,11 @@ namespace StardewModdingAPI /// <summary>Whether the player has loaded a save and the world has finished initialising.</summary> public static bool IsWorldReady { get; internal set; } - /// <summary>Whether the player is free to move around (e.g. save is loaded, no menu is displayed, no cutscene is in progress, etc).</summary> - public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.player.CanMove && !Game1.dialogueUp && !Game1.eventUp; + /// <summary>Whether <see cref="IsWorldReady"/> is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc).</summary> + public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && !Game1.eventUp; + + /// <summary>Whether <see cref="IsPlayerFree"/> is true and the player is free to move (e.g. not using a tool).</summary> + public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; /// <summary>Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use <see cref="GraphicsEvents.OnPostRenderEvent"/> to draw to the screen.</summary> public static bool IsInDrawLoop { get; internal set; } diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/StardewModdingAPI/Events/GraphicsEvents.cs index 25b976f1..fff51bed 100644 --- a/src/StardewModdingAPI/Events/GraphicsEvents.cs +++ b/src/StardewModdingAPI/Events/GraphicsEvents.cs @@ -51,11 +51,9 @@ namespace StardewModdingAPI.Events ****/ /// <summary>Raise a <see cref="Resize"/> event.</summary> /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="sender">The object which raised the event.</param> - /// <param name="e">The event arguments.</param> - internal static void InvokeResize(IMonitor monitor, object sender, EventArgs e) + internal static void InvokeResize(IMonitor monitor) { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList(), sender, e); + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); } /**** diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs new file mode 100644 index 00000000..ec9279f1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// <summary>An exception thrown when an assembly can't be loaded by SMAPI, with all the relevant details in the message.</summary> + internal class SAssemblyLoadFailedException : Exception + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="message">The error message.</param> + public SAssemblyLoadFailedException(string message) + : base(message) { } + } +} diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/StardewModdingAPI/Framework/GameVersion.cs new file mode 100644 index 00000000..48159f61 --- /dev/null +++ b/src/StardewModdingAPI/Framework/GameVersion.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework +{ + /// <summary>An implementation of <see cref="ISemanticVersion"/> that correctly handles the non-semantic versions used by older Stardew Valley releases.</summary> + internal class GameVersion : SemanticVersion + { + /********* + ** Private methods + *********/ + /// <summary>A mapping of game to semantic versions.</summary> + private static readonly IDictionary<string, string> VersionMap = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) + { + ["1.01"] = "1.0.1", + ["1.02"] = "1.0.2", + ["1.03"] = "1.0.3", + ["1.04"] = "1.0.4", + ["1.05"] = "1.0.5", + ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. + ["1.051b"] = "1.0.6-prelease2", + ["1.06"] = "1.0.6", + ["1.07"] = "1.0.7", + ["1.07a"] = "1.0.8-prerelease1", + ["1.11"] = "1.1.1" + }; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="version">The game version string.</param> + public GameVersion(string version) + : base(GameVersion.GetSemanticVersionString(version)) { } + + /// <summary>Get a string representation of the version.</summary> + public override string ToString() + { + return GameVersion.GetGameVersionString(base.ToString()); + } + + + /********* + ** Private methods + *********/ + /// <summary>Convert a game version string to a semantic version string.</summary> + /// <param name="gameVersion">The game version string.</param> + private static string GetSemanticVersionString(string gameVersion) + { + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; + } + + /// <summary>Convert a game version string to a semantic version string.</summary> + /// <param name="gameVersion">The game version string.</param> + private static string GetGameVersionString(string gameVersion) + { + foreach (var mapping in GameVersion.VersionMap) + { + if (mapping.Value.Equals(gameVersion, StringComparison.InvariantCultureIgnoreCase)) + return mapping.Key; + } + return gameVersion; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 5f72176e..ffa78ff6 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -33,10 +33,19 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>The friendly mod name for use in errors.</summary> private readonly string ModName; + /// <summary>Encapsulates monitoring and logging for a given module.</summary> + private readonly IMonitor Monitor; + /********* ** Accessors *********/ + /// <summary>The game's current locale code (like <c>pt-BR</c>).</summary> + public string CurrentLocale => this.ContentManager.GetLocale(); + + /// <summary>The game's current locale as an enum value.</summary> + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + /// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary> internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>(); @@ -44,10 +53,10 @@ namespace StardewModdingAPI.Framework.ModHelpers internal ObservableCollection<IAssetLoader> ObservableAssetLoaders { get; } = new ObservableCollection<IAssetLoader>(); /// <summary>Interceptors which provide the initial versions of matching content assets.</summary> - internal IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders; + public IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders; /// <summary>Interceptors which edit matching content assets after they're loaded.</summary> - internal IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors; + public IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors; /********* @@ -58,13 +67,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modFolderPath">The absolute path to the mod folder.</param> /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The friendly mod name for use in errors.</param> - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) + /// <param name="monitor">Encapsulates monitoring and logging.</param> + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.Monitor = monitor; } /// <summary>Load content from the game folder or mod 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> @@ -176,6 +187,25 @@ namespace StardewModdingAPI.Framework.ModHelpers } } + /// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary> + /// <param name="key">The asset key to invalidate in the content folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + /// <returns>Returns whether the given asset key was cached.</returns> + public bool InvalidateCache(string key) + { + this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); + return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); + } + + /// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary> + /// <typeparam name="T">The asset type to remove from the cache.</typeparam> + /// <returns>Returns whether any assets were invalidated.</returns> + public bool InvalidateCache<T>() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); + return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + } /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs index 9411a97a..14a339da 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -13,16 +13,21 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>The underlying reflection helper.</summary> private readonly Reflector Reflector; + /// <summary>The mod name for error messages.</summary> + private readonly string ModName; + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="modName">The mod name for error messages.</param> /// <param name="reflector">The underlying reflection helper.</param> - public ReflectionHelper(string modID, Reflector reflector) + public ReflectionHelper(string modID, string modName, Reflector reflector) : base(modID) { + this.ModName = modName; this.Reflector = reflector; } @@ -37,6 +42,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns> public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateField<TValue>(obj, name, required); } @@ -47,6 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateField<TValue>(type, name, required); } @@ -60,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private property is not found.</param> public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateProperty<TValue>(obj, name, required); } @@ -70,6 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private property is not found.</param> public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateProperty<TValue>(type, name, required); } @@ -89,6 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// </remarks> public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required); return field != null ? field.GetValue() @@ -107,6 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// </remarks> public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required); return field != null ? field.GetValue() @@ -122,6 +133,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, required); } @@ -131,6 +143,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, required); } @@ -144,6 +157,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); } @@ -154,7 +168,35 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); } + + + /********* + ** Private methods + *********/ + /// <summary>Assert that mods can use the reflection helper to access the given type.</summary> + /// <param name="type">The type being accessed.</param> + private void AssertAccessAllowed(Type type) + { +#if !SMAPI_1_x + // validate type namespace + if (type.Namespace != null) + { + string rootSmapiNamespace = typeof(Program).Namespace; + if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); + } +#endif + } + + /// <summary>Assert that mods can use the reflection helper to access the given type.</summary> + /// <param name="obj">The object being accessed.</param> + private void AssertAccessAllowed(object obj) + { + if (obj != null) + this.AssertAccessAllowed(obj.GetType()); + } } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs new file mode 100644 index 00000000..11be19fc --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// <summary>Indicates the result of an assembly load.</summary> + internal enum AssemblyLoadStatus + { + /// <summary>The assembly was loaded successfully.</summary> + Okay = 1, + + /// <summary>The assembly could not be loaded.</summary> + Failed = 2, + + /// <summary>The assembly is already loaded.</summary> + AlreadyLoaded = 3 + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 406d49e1..b14ae56f 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,6 +6,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.Framework.Exceptions; namespace StardewModdingAPI.Framework.ModLoading { @@ -65,16 +66,27 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); HashSet<string> visitedAssemblyNames = new HashSet<string>(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); - if (!assemblies.Any()) - throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); - resolver.Add(assemblies.Select(p => p.Definition).ToArray()); } + // validate load + if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) + { + throw new SAssemblyLoadFailedException(!File.Exists(assemblyPath) + ? $"Could not load '{assemblyPath}' because it doesn't exist." + : $"Could not load '{assemblyPath}'." + ); + } + if (assemblies[0].Status == AssemblyLoadStatus.AlreadyLoaded) + throw new SAssemblyLoadFailedException($"Could not load '{assemblyPath}' because it was already loaded. Do you have two copies of this mod?"); + // rewrite & load assemblies in leaf-to-root order bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; foreach (AssemblyParseResult assembly in assemblies) { + if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) + continue; + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); if (changed) { @@ -143,7 +155,7 @@ namespace StardewModdingAPI.Framework.ModLoading // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) - yield break; + yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); visitedAssemblyNames.Add(assembly.Name.Name); // yield referenced assemblies @@ -155,7 +167,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // yield assembly - yield return new AssemblyParseResult(file, assembly); + yield return new AssemblyParseResult(file, assembly, AssemblyLoadStatus.Okay); } /**** diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs index 69c99afe..b56a776c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The assembly definition.</summary> public readonly AssemblyDefinition Definition; + /// <summary>The result of the assembly load.</summary> + public AssemblyLoadStatus Status; + /********* ** Public methods @@ -22,10 +25,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Construct an instance.</summary> /// <param name="file">The original assembly file.</param> /// <param name="assembly">The assembly definition.</param> - public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) + /// <param name="status">The result of the assembly load.</param> + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status) { this.File = file; this.Definition = assembly; + this.Status = status; } } -}
\ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index b75453b7..6b19db5c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -71,9 +71,9 @@ namespace StardewModdingAPI.Framework.ModLoading compatibility = ( from mod in compatibilityRecords where - mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) - && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) - && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + mod.ID.Any(p => p.Matches(key, manifest)) + && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) + && !manifest.Version.IsNewerThan(mod.UpperVersion) select mod ).FirstOrDefault(); } @@ -109,15 +109,25 @@ namespace StardewModdingAPI.Framework.ModLoading ModCompatibility compatibility = mod.Compatibility; if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl); +#if SMAPI_1_x + bool hasOfficialUrl = mod.Compatibility.UpdateUrls.Length > 0; + bool hasUnofficialUrl = mod.Compatibility.UpdateUrls.Length > 1; string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:"; if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + error += !hasUnofficialUrl ? $" {compatibility.UpdateUrls[0]}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrls[0]}"; if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + error += $"{Environment.NewLine}- unofficial update: {compatibility.UpdateUrls[1]}"; +#else + string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string error = $"{reasonPhrase}. Please check for a "; + if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) + error += "newer version"; + else + error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; + error += " at " + string.Join(" or ", compatibility.UpdateUrls); +#endif mod.SetStatus(ModMetadataStatus.Failed, error); continue; @@ -161,7 +171,7 @@ namespace StardewModdingAPI.Framework.ModLoading #if !SMAPI_1_x { var duplicatesByID = mods - .GroupBy(mod => mod.Manifest.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) + .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 1b5c2646..29c3517e 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -21,18 +21,18 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// <summary>The mod version.</summary> - [JsonConverter(typeof(ManifestFieldConverter))] + [JsonConverter(typeof(SFieldConverter))] public ISemanticVersion Version { get; set; } /// <summary>The minimum SMAPI version required by this mod, if any.</summary> - [JsonConverter(typeof(ManifestFieldConverter))] + [JsonConverter(typeof(SFieldConverter))] public ISemanticVersion MinimumApiVersion { get; set; } /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> public string EntryDll { get; set; } /// <summary>The other mods that must be loaded before this mod.</summary> - [JsonConverter(typeof(ManifestFieldConverter))] + [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } /// <summary>The unique mod ID.</summary> diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index 90cbd237..d3a9c533 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -1,5 +1,5 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.Models { @@ -9,60 +9,32 @@ namespace StardewModdingAPI.Framework.Models /********* ** Accessors *********/ - /**** - ** From config - ****/ /// <summary>The unique mod IDs.</summary> - public string[] ID { get; set; } + [JsonConverter(typeof(SFieldConverter))] + public ModCompatibilityID[] ID { get; set; } /// <summary>The mod name.</summary> public string Name { get; set; } /// <summary>The oldest incompatible mod version, or <c>null</c> for all past versions.</summary> - public string LowerVersion { get; set; } + [JsonConverter(typeof(SFieldConverter))] + public ISemanticVersion LowerVersion { get; set; } /// <summary>The most recent incompatible mod version.</summary> - public string UpperVersion { get; set; } + [JsonConverter(typeof(SFieldConverter))] + public ISemanticVersion UpperVersion { get; set; } /// <summary>A label to show to the user instead of <see cref="UpperVersion"/>, when the manifest version differs from the user-facing version.</summary> public string UpperVersionLabel { get; set; } - /// <summary>The URL the user can check for an official updated version.</summary> - public string UpdateUrl { get; set; } - - /// <summary>The URL the user can check for an unofficial updated version.</summary> - public string UnofficialUpdateUrl { get; set; } + /// <summary>The URLs the user can check for a newer version.</summary> + public string[] UpdateUrls { get; set; } /// <summary>The reason phrase to show in the warning, or <c>null</c> to use the default value.</summary> /// <example>"this version is incompatible with the latest version of the game"</example> public string ReasonPhrase { get; set; } /// <summary>Indicates how SMAPI should consider the mod.</summary> - public ModCompatibilityType Compatibility { get; set; } - - - /**** - ** Injected - ****/ - /// <summary>The semantic version corresponding to <see cref="LowerVersion"/>.</summary> - [JsonIgnore] - public ISemanticVersion LowerSemanticVersion { get; set; } - - /// <summary>The semantic version corresponding to <see cref="UpperVersion"/>.</summary> - [JsonIgnore] - public ISemanticVersion UpperSemanticVersion { get; set; } - - - /********* - ** Private methods - *********/ - /// <summary>The method called when the model finishes deserialising.</summary> - /// <param name="context">The deserialisation context.</param> - [OnDeserialized] - private void OnDeserialized(StreamingContext context) - { - this.LowerSemanticVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; - this.UpperSemanticVersion = this.UpperVersion != null ? new SemanticVersion(this.UpperVersion) : null; - } + public ModCompatibilityType Compatibility { get; set; } = ModCompatibilityType.AssumeBroken; } } diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs new file mode 100644 index 00000000..98e70116 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs @@ -0,0 +1,57 @@ +using System; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.Models +{ + /// <summary>Uniquely identifies a mod for compatibility checks.</summary> + internal class ModCompatibilityID + { + /********* + ** Accessors + *********/ + /// <summary>The unique mod ID.</summary> + public string ID { get; set; } + + /// <summary>The mod name to disambiguate non-unique IDs, or <c>null</c> to ignore the mod name.</summary> + public string Name { get; set; } + + /// <summary>The author name to disambiguate non-unique IDs, or <c>null</c> to ignore the author.</summary> + public string Author { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ModCompatibilityID() { } + + /// <summary>Construct an instance.</summary> + /// <param name="data">The mod ID or a JSON string matching the <see cref="ModCompatibilityID"/> fields.</param> + public ModCompatibilityID(string data) + { + // JSON can be stuffed into the ID string as a convenience hack to keep JSON mod lists + // formatted readably. The tradeoff is that the format is a bit more magical, but that's + // probably acceptable since players aren't meant to edit it. It's also fairly clear what + // the JSON strings do, if not necessarily how. + if (data.StartsWith("{")) + JsonConvert.PopulateObject(data, this); + else + this.ID = data; + } + + /// <summary>Get whether this ID matches a given mod manifest.</summary> + /// <param name="id">The mod's unique ID, or a substitute ID if it isn't set in the manifest.</param> + /// <param name="manifest">The manifest to check.</param> + public bool Matches(string id, IManifest manifest) + { + return + this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) + && ( + this.Author == null + || this.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && this.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (this.Name == null || this.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 5ae24a73..c2c3a689 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -25,15 +25,7 @@ namespace StardewModdingAPI.Framework private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max(); /// <summary>The console text color for each log level.</summary> - private static readonly Dictionary<LogLevel, ConsoleColor> Colors = new Dictionary<LogLevel, ConsoleColor> - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; + private static readonly IDictionary<LogLevel, ConsoleColor> Colors = Monitor.GetConsoleColorScheme(); /// <summary>Propagates notification that SMAPI should exit.</summary> private readonly CancellationTokenSource ExitTokenSource; @@ -172,5 +164,56 @@ namespace StardewModdingAPI.Framework if (this.WriteToFile) this.LogFile.WriteLine(fullMessage); } + + /// <summary>Get the color scheme to use for the current console.</summary> + private static IDictionary<LogLevel, ConsoleColor> GetConsoleColorScheme() + { +#if !SMAPI_1_x + // scheme for dark console background + if (Monitor.IsDark(Console.BackgroundColor)) + { +#endif + return new Dictionary<LogLevel, ConsoleColor> + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warn] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.Magenta + }; +#if !SMAPI_1_x + } + + // scheme for light console background + return new Dictionary<LogLevel, ConsoleColor> + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.Black, + [LogLevel.Warn] = ConsoleColor.DarkYellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.DarkMagenta + }; +#endif + } + + /// <summary>Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'.</summary> + /// <param name="color">The color to check.</param> + private static bool IsDark(ConsoleColor color) + { + switch (color) + { + case ConsoleColor.Black: + case ConsoleColor.Blue: + case ConsoleColor.DarkBlue: + case ConsoleColor.DarkRed: + case ConsoleColor.Red: + return true; + + default: + return false; + } + } } } diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 669b0e7a..25775291 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -1,18 +1,17 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Metadata; using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Objects; -using StardewValley.Projectiles; namespace StardewModdingAPI.Framework { @@ -40,6 +39,15 @@ namespace StardewModdingAPI.Framework /// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary> private readonly IPrivateMethod GetKeyLocale; + /// <summary>The language codes used in asset keys.</summary> + private readonly IDictionary<string, LanguageCode> KeyLocales; + + /// <summary>Provides metadata for core game assets.</summary> + private readonly CoreAssets CoreAssets; + + /// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary> + private readonly ContextHash<string> AssetsBeingLoaded = new ContextHash<string>(); + /********* ** Accessors @@ -86,6 +94,11 @@ namespace StardewModdingAPI.Framework } else this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load<T> logic + + // get asset data + this.CoreAssets = new CoreAssets(this.NormaliseAssetName); + this.KeyLocales = this.GetKeyLocales(reflection); + } /// <summary>Normalise path separators in a file path. For asset keys, see <see cref="NormaliseAssetName"/> instead.</summary> @@ -130,11 +143,21 @@ namespace StardewModdingAPI.Framework // load asset T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load<T>(assetName); + } + else { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors<T>(info, asset); - data = (T)asset.Data; + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors<T>(info, asset); + return (T)asset.Data; + }); } // update cache & return data @@ -159,54 +182,90 @@ namespace StardewModdingAPI.Framework return this.GetKeyLocale.Invoke<string>(); } - /// <summary>Reset the asset cache and reload the game's static assets.</summary> - /// <remarks>This implementation is derived from <see cref="Game1.LoadContent"/>.</remarks> - public void Reset() + /// <summary>Get the cached asset keys.</summary> + public IEnumerable<string> GetAssetKeys() { - this.Monitor.Log("Resetting asset cache...", LogLevel.Trace); - this.Cache.Clear(); - - // from Game1.LoadContent - Game1.daybg = this.Load<Texture2D>("LooseSprites\\daybg"); - Game1.nightbg = this.Load<Texture2D>("LooseSprites\\nightbg"); - Game1.menuTexture = this.Load<Texture2D>("Maps\\MenuTiles"); - Game1.lantern = this.Load<Texture2D>("LooseSprites\\Lighting\\lantern"); - Game1.windowLight = this.Load<Texture2D>("LooseSprites\\Lighting\\windowLight"); - Game1.sconceLight = this.Load<Texture2D>("LooseSprites\\Lighting\\sconceLight"); - Game1.cauldronLight = this.Load<Texture2D>("LooseSprites\\Lighting\\greenLight"); - Game1.indoorWindowLight = this.Load<Texture2D>("LooseSprites\\Lighting\\indoorWindowLight"); - Game1.shadowTexture = this.Load<Texture2D>("LooseSprites\\shadow"); - Game1.mouseCursors = this.Load<Texture2D>("LooseSprites\\Cursors"); - Game1.controllerMaps = this.Load<Texture2D>("LooseSprites\\ControllerMaps"); - Game1.animations = this.Load<Texture2D>("TileSheets\\animations"); - Game1.achievements = this.Load<Dictionary<int, string>>("Data\\Achievements"); - Game1.NPCGiftTastes = this.Load<Dictionary<string, string>>("Data\\NPCGiftTastes"); - Game1.dialogueFont = this.Load<SpriteFont>("Fonts\\SpriteFont1"); - Game1.smallFont = this.Load<SpriteFont>("Fonts\\SmallFont"); - Game1.tinyFont = this.Load<SpriteFont>("Fonts\\tinyFont"); - Game1.tinyFontBorder = this.Load<SpriteFont>("Fonts\\tinyFontBorder"); - Game1.objectSpriteSheet = this.Load<Texture2D>("Maps\\springobjects"); - Game1.cropSpriteSheet = this.Load<Texture2D>("TileSheets\\crops"); - Game1.emoteSpriteSheet = this.Load<Texture2D>("TileSheets\\emotes"); - Game1.debrisSpriteSheet = this.Load<Texture2D>("TileSheets\\debris"); - Game1.bigCraftableSpriteSheet = this.Load<Texture2D>("TileSheets\\Craftables"); - Game1.rainTexture = this.Load<Texture2D>("TileSheets\\rain"); - Game1.buffsIcons = this.Load<Texture2D>("TileSheets\\BuffsIcons"); - Game1.objectInformation = this.Load<Dictionary<int, string>>("Data\\ObjectInformation"); - Game1.bigCraftablesInformation = this.Load<Dictionary<int, string>>("Data\\BigCraftablesInformation"); - FarmerRenderer.hairStylesTexture = this.Load<Texture2D>("Characters\\Farmer\\hairstyles"); - FarmerRenderer.shirtsTexture = this.Load<Texture2D>("Characters\\Farmer\\shirts"); - FarmerRenderer.hatsTexture = this.Load<Texture2D>("Characters\\Farmer\\hats"); - FarmerRenderer.accessoriesTexture = this.Load<Texture2D>("Characters\\Farmer\\accessories"); - Furniture.furnitureTexture = this.Load<Texture2D>("TileSheets\\furniture"); - SpriteText.spriteTexture = this.Load<Texture2D>("LooseSprites\\font_bold"); - SpriteText.coloredTexture = this.Load<Texture2D>("LooseSprites\\font_colored"); - Tool.weaponsTexture = this.Load<Texture2D>("TileSheets\\weapons"); - Projectile.projectileSheet = this.Load<Texture2D>("TileSheets\\Projectiles"); - - // from Farmer constructor - if (Game1.player != null) - Game1.player.FarmerRenderer = new FarmerRenderer(this.Load<Texture2D>("Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + IEnumerable<string> GetAllAssetKeys() + { + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string _); + yield return assetKey; + } + } + + return GetAllAssetKeys().Distinct(); + } + + /// <summary>Purge assets from the cache that match one of the interceptors.</summary> + /// <param name="editors">The asset editors for which to purge matching assets.</param> + /// <param name="loaders">The asset loaders for which to purge matching assets.</param> + /// <returns>Returns whether any cache entries were invalidated.</returns> + public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return false; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + + // invalidate matching keys + return this.InvalidateCache((assetName, assetType) => + { + // get asset metadata + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); + + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(assetType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { info }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(assetType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { info })); + }); + } + + /// <summary>Purge matched assets from the cache.</summary> + /// <param name="predicate">Matches the asset keys to invalidate.</param> + /// <returns>Returns whether any cache entries were invalidated.</returns> + public bool InvalidateCache(Func<string, Type, bool> predicate) + { + // find matching asset keys + HashSet<string> purgeCacheKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + HashSet<string> purgeAssetKeys = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out _); + Type type = this.Cache[cacheKey].GetType(); + if (predicate(assetKey, type)) + { + purgeAssetKeys.Add(assetKey); + purgeCacheKeys.Add(cacheKey); + } + } + + // purge from cache + foreach (string key in purgeCacheKeys) + this.Cache.Remove(key); + + // reload core game assets + int reloaded = 0; + foreach (string key in purgeAssetKeys) + { + if (this.CoreAssets.ReloadForKey(this, key)) + reloaded++; + } + + // report result + if (purgeCacheKeys.Any()) + { + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + return true; + } + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + return false; } @@ -221,6 +280,60 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset } + /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> + /// <param name="reflection">Simplifies access to private game code.</param> + private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField<LanguageCode> codeField = reflection.GetPrivateField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary<string, LanguageCode> map = new Dictionary<string, LanguageCode>(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke<string>()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; + } + + /// <summary>Parse a cache key into its component parts.</summary> + /// <param name="cacheKey">The input cache key.</param> + /// <param name="assetKey">The original asset key.</param> + /// <param name="localeCode">The asset locale code (or <c>null</c> if not localised).</param> + private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.KeyLocales.ContainsKey(suffix)) + { + assetKey = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetKey = cacheKey; + localeCode = null; + } + /// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary> /// <param name="info">The basic asset metadata.</param> /// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns> @@ -365,7 +478,8 @@ namespace StardewModdingAPI.Framework // can't know which assets are meant to be disposed. Here we remove current assets from // the cache, but don't dispose them to avoid crashing any code that still references // them. The garbage collector will eventually clean up any unused assets. - this.Reset(); + this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); + this.InvalidateCache((key, type) => true); } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index d6f1a05b..997e0c8c 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -19,7 +20,9 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; +#if SMAPI_1_x using SFarmer = StardewValley.Farmer; +#endif namespace StardewModdingAPI.Framework { @@ -54,10 +57,6 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the game is saving and SMAPI has already raised <see cref="SaveEvents.BeforeSave"/>.</summary> private bool IsBetweenSaveEvents; - /// <summary>Whether the game's zoom level is at 100% (i.e. nothing should be scaled).</summary> - public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f); - - /**** ** Game state ****/ @@ -76,7 +75,10 @@ namespace StardewModdingAPI.Framework /// <summary>The previous mouse position on the screen adjusted for the zoom level.</summary> private Point PreviousMousePosition; - /// <summary>The previous save ID at last check.</summary> + /// <summary>The window size value at last check.</summary> + private Point PreviousWindowSize; + + /// <summary>The save ID at last check.</summary> private ulong PreviousSaveID; /// <summary>A hash of <see cref="Game1.locations"/> at last check.</summary> @@ -318,6 +320,11 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) { +#if !SMAPI_1_x + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) +#endif + this.AfterLoadTimer--; + if (this.AfterLoadTimer == 0) { this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); @@ -329,7 +336,6 @@ namespace StardewModdingAPI.Framework #endif TimeEvents.InvokeAfterDayStarted(this.Monitor); } - this.AfterLoadTimer--; } /********* @@ -350,6 +356,20 @@ namespace StardewModdingAPI.Framework } /********* + ** Window events + *********/ + // Here we depend on the game's viewport instead of listening to the Window.Resize + // event because we need to notify mods after the game handles the resize, so the + // game's metadata (like Game1.viewport) are updated. That's a bit complicated + // since the game adds & removes its own handler on the fly. + if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) + { + Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); + GraphicsEvents.InvokeResize(this.Monitor); + this.PreviousWindowSize = size; + } + + /********* ** Input events (if window has focus) *********/ if (Game1.game1.IsActive) diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index 6947311b..11ffdccb 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -8,7 +8,7 @@ using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.Serialisation { /// <summary>Overrides how SMAPI reads and writes <see cref="ISemanticVersion"/> and <see cref="IManifestDependency"/> fields.</summary> - internal class ManifestFieldConverter : JsonConverter + internal class SFieldConverter : JsonConverter { /********* ** Accessors @@ -24,7 +24,10 @@ namespace StardewModdingAPI.Framework.Serialisation /// <param name="objectType">The object type.</param> public override bool CanConvert(Type objectType) { - return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]); + return + objectType == typeof(ISemanticVersion) + || objectType == typeof(IManifestDependency[]) + || objectType == typeof(ModCompatibilityID[]); } /// <summary>Reads the JSON representation of the object.</summary> @@ -83,6 +86,20 @@ namespace StardewModdingAPI.Framework.Serialisation return result.ToArray(); } + // mod compatibility ID + if (objectType == typeof(ModCompatibilityID[])) + { + List<ModCompatibilityID> result = new List<ModCompatibilityID>(); + foreach (JToken child in JArray.Load(reader).Children()) + { + result.Add(child is JValue value + ? new ModCompatibilityID(value.Value<string>()) + : child.ToObject<ModCompatibilityID>() + ); + } + return result.ToArray(); + } + // unknown throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); } diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs new file mode 100644 index 00000000..0d8487bb --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// <summary>A <see cref="HashSet{T}"/> wrapper meant for tracking recursive contexts.</summary> + /// <typeparam name="T">The key type.</typeparam> + internal class ContextHash<T> : HashSet<T> + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ContextHash() { } + + /// <summary>Construct an instance.</summary> + /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> implementation to use when comparing values in the set, or <c>null</c> to use the default comparer for the set type.</param> + public ContextHash(IEqualityComparer<T> comparer) + : base(comparer) { } + + /// <summary>Add a key while an action is in progress, and remove it when it completes.</summary> + /// <param name="key">The key to add.</param> + /// <param name="action">The action to perform.</param> + /// <exception cref="InvalidOperationException">The specified key is already added.</exception> + public void Track(T key, Action action) + { + if(this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + action(); + } + finally + { + this.Remove(key); + } + } + + /// <summary>Add a key while an action is in progress, and remove it when it completes.</summary> + /// <typeparam name="TResult">The value type returned by the method.</typeparam> + /// <param name="key">The key to add.</param> + /// <param name="action">The action to perform.</param> + public TResult Track<TResult>(T key, Func<TResult> action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + return action(); + } + finally + { + this.Remove(key); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Countdown.cs b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs index 25ca2546..921a35ce 100644 --- a/src/StardewModdingAPI/Framework/Countdown.cs +++ b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework +namespace StardewModdingAPI.Framework.Utilities { /// <summary>Counts down from a baseline value.</summary> internal class Countdown diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index 32a9ff19..b4557134 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -1,12 +1,35 @@ using System; +using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewValley; namespace StardewModdingAPI { /// <summary>Provides an API for loading content assets.</summary> public interface IContentHelper : IModLinked { + /********* + ** Accessors + *********/ +#if !SMAPI_1_x + /// <summary>Interceptors which provide the initial versions of matching content assets.</summary> + IList<IAssetLoader> AssetLoaders { get; } + + /// <summary>Interceptors which edit matching content assets after they're loaded.</summary> + IList<IAssetEditor> AssetEditors { get; } +#endif + + /// <summary>The game's current locale code (like <c>pt-BR</c>).</summary> + string CurrentLocale { get; } + + /// <summary>The game's current locale as an enum value.</summary> + LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } + + + /********* + ** Public methods + *********/ /// <summary>Load content from the game folder or mod 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> /// <typeparam name="T">The expected data type. The main supported types are <see cref="Texture2D"/> and dictionaries; other types may be supported by the game's content pipeline.</typeparam> /// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param> @@ -20,5 +43,18 @@ namespace StardewModdingAPI /// <param name="source">Where to search for a matching content asset.</param> /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); + +#if !SMAPI_1_x + /// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary> + /// <param name="key">The asset key to invalidate in the content folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + /// <returns>Returns whether the given asset key was cached.</returns> + bool InvalidateCache(string key); + + /// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary> + /// <typeparam name="T">The asset type to remove from the cache.</typeparam> + /// <returns>Returns whether any assets were invalidated.</returns> + bool InvalidateCache<T>(); +#endif } } diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/StardewModdingAPI/ISemanticVersion.cs index 27a2f67d..c1a4ca3a 100644 --- a/src/StardewModdingAPI/ISemanticVersion.cs +++ b/src/StardewModdingAPI/ISemanticVersion.cs @@ -4,6 +4,9 @@ namespace StardewModdingAPI { /// <summary>A semantic version with an optional release tag.</summary> public interface ISemanticVersion : IComparable<ISemanticVersion> +#if !SMAPI_1_x + , IEquatable<ISemanticVersion> +#endif { /********* ** Accessors diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/StardewModdingAPI/Metadata/CoreAssets.cs new file mode 100644 index 00000000..24f23af7 --- /dev/null +++ b/src/StardewModdingAPI/Metadata/CoreAssets.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; +using StardewValley.Locations; +using StardewValley.Objects; +using StardewValley.Projectiles; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Metadata +{ + /// <summary>Provides metadata about core assets in the game.</summary> + internal class CoreAssets + { + /********* + ** Properties + *********/ + /// <summary>Normalises an asset key to match the cache key.</summary> + protected readonly Func<string, string> GetNormalisedPath; + + /// <summary>Setters which update static or singleton texture fields indexed by normalised asset key.</summary> + private readonly IDictionary<string, Action<SContentManager, string>> SingletonSetters; + + + /********* + ** Public methods + *********/ + /// <summary>Initialise the core asset data.</summary> + /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param> + public CoreAssets(Func<string, string> getNormalisedPath) + { + this.GetNormalisedPath = getNormalisedPath; + this.SingletonSetters = + new Dictionary<string, Action<SContentManager, string>> + { + // from Game1.loadContent + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load<Texture2D>(key), + ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load<Texture2D>(key), + ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load<Texture2D>(key), + ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load<Texture2D>(key), + ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load<Texture2D>(key), + ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load<Texture2D>(key), + ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load<Texture2D>(key), + ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load<Texture2D>(key), + ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load<Texture2D>(key), + ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load<Texture2D>(key), + ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load<Texture2D>(key), + ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load<Texture2D>(key), + ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load<Dictionary<int, string>>(key), + ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key), + ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load<SpriteFont>(key), + ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load<SpriteFont>(key), + ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load<SpriteFont>(key), + ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load<SpriteFont>(key), + ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load<Texture2D>(key), + ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load<Texture2D>(key), + ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load<Texture2D>(key), + ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load<Texture2D>(key), + ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key), + ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load<Texture2D>(key), + ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load<Texture2D>(key), + ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load<Dictionary<int, string>>(key), + ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key), + ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key), + ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key), + ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load<Texture2D>(key), + ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key), + ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load<Texture2D>(key), + ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load<Texture2D>(key), + ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load<Texture2D>(key), + ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load<Texture2D>(key), + ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load<Texture2D>(key), + + // from Game1.ResetToolSpriteSheet + ["TileSheets\\tools"] = (content, key) => Game1.ResetToolSpriteSheet(), + + // from Bush + ["TileSheets\\bushes"] = (content, key) => Bush.texture = content.Load<Texture2D>(key), + + // from Critter + ["TileSheets\\critters"] = (content, key) => Critter.critterTexture = content.Load<Texture2D>(key), + + // from Farm + ["Buildings\\houses"] = (content, key) => + { + Farm farm = Game1.getFarm(); + if (farm != null) + farm.houseTextures = content.Load<Texture2D>(key); + }, + + // from Farmer + ["Characters\\Farmer\\farmer_base"] = (content, key) => + { + if (Game1.player != null && Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key)); + }, + ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + { + if (Game1.player != null && !Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key)); + }, + + // from Flooring + ["TerrainFeatures\\Flooring"] = (content, key) => Flooring.floorsTexture = content.Load<Texture2D>(key), + + // from FruitTree + ["TileSheets\\fruitTrees"] = (content, key) => FruitTree.texture = content.Load<Texture2D>(key), + + // from HoeDirt + ["TerrainFeatures\\hoeDirt"] = (content, key) => HoeDirt.lightTexture = content.Load<Texture2D>(key), + ["TerrainFeatures\\hoeDirtDark"] = (content, key) => HoeDirt.darkTexture = content.Load<Texture2D>(key), + ["TerrainFeatures\\hoeDirtSnow"] = (content, key) => HoeDirt.snowTexture = content.Load<Texture2D>(key), + + // from Wallpaper + ["Maps\\walls_and_floors"] = (content, key) => Wallpaper.wallpaperTexture = content.Load<Texture2D>(key) + } + .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value); + } + + /// <summary>Reload one of the game's core assets (if applicable).</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 an asset was reloaded.</returns> + public bool ReloadForKey(SContentManager content, string key) + { + // static assets + if (this.SingletonSetters.TryGetValue(key, out Action<SContentManager, string> reload)) + { + reload(content, key); + return true; + } + + // building textures + if (key.StartsWith(this.GetNormalisedPath("Buildings\\"))) + { + Building[] buildings = this.GetAllBuildings().Where(p => key == this.GetNormalisedPath($"Buildings\\{p.buildingType}")).ToArray(); + if (buildings.Any()) + { + Texture2D texture = content.Load<Texture2D>(key); + foreach (Building building in buildings) + building.texture = texture; + return true; + } + return false; + } + + return false; + } + + + /********* + ** Private methods + *********/ + /// <summary>Get all player-constructed buildings in the world.</summary> + private IEnumerable<Building> GetAllBuildings() + { + return Game1.locations + .OfType<BuildableGameLocation>() + .SelectMany(p => p.buildings); + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 08bd2b84..108e9273 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -15,6 +15,7 @@ using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; @@ -125,7 +126,7 @@ namespace StardewModdingAPI try { // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); #if SMAPI_1_x this.Monitor.Log("Preparing SMAPI..."); @@ -138,13 +139,13 @@ namespace StardewModdingAPI // validate game version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.GetGameDisplayVersion(Constants.MaximumGameVersion)}. Please check for a newer version of SMAPI.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } @@ -185,14 +186,13 @@ namespace StardewModdingAPI ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); - this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}"; + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; } catch (Exception ex) { @@ -315,7 +315,7 @@ namespace StardewModdingAPI if (Type.GetType($"StardewValley.LocalizedContentManager+LanguageCode, {gameAssemblyName}", throwOnError: false) == null) { PrintErrorAndExit(Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion) - ? $"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI." + ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." : "Oops! SMAPI doesn't seem to be compatible with your game. Make sure you're running the latest version of Stardew Valley and SMAPI." ); } @@ -445,8 +445,8 @@ namespace StardewModdingAPI // update window titles int modsLoaded = this.ModRegistry.GetMods().Count(); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods"; + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; // start SMAPI console new Thread(this.RunConsoleLoop).Start(); @@ -649,7 +649,16 @@ namespace StardewModdingAPI } catch (IncompatibleInstructionException ex) { +#if SMAPI_1_x TrackSkip(metadata, $"it's not compatible with the latest version of the game or SMAPI (detected {ex.NounPhrase}). Please check for a newer version of the mod."); +#else + TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); +#endif + continue; + } + catch (SAssemblyLoadFailedException ex) + { + TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}"); continue; } catch (Exception ex) @@ -703,15 +712,16 @@ namespace StardewModdingAPI // inject data { + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); - mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); + mod.Monitor = monitor; #if SMAPI_1_x mod.PathOnDisk = metadata.DirectoryPath; #endif @@ -791,6 +801,9 @@ namespace StardewModdingAPI // raise deprecation warning for old Entry() methods if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.PendingRemoval)); +#else + if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(IModHelper) })) + this.Monitor.Log($"{metadata.DisplayName} doesn't implement Entry() and may not work correctly.", LogLevel.Error); #endif } catch (Exception ex) @@ -799,8 +812,8 @@ namespace StardewModdingAPI } } - // reset cache when needed - // only register listeners after Entry to avoid repeatedly reloading assets during load + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) @@ -808,16 +821,30 @@ namespace StardewModdingAPI helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) - this.ContentManager.Reset(); + { + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]); + } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) - this.ContentManager.Reset(); + { + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray()); + } }; } } - this.ContentManager.Reset(); + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => ((ContentHelper)p.Mod.Helper.Content).AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => ((ContentHelper)p.Mod.Helper.Content).AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(editors, loaders); + } } /// <summary>Reload translations for all mods.</summary> diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs index 4b27c819..e448eae1 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Text.RegularExpressions; +using Newtonsoft.Json; namespace StardewModdingAPI { @@ -17,7 +18,7 @@ namespace StardewModdingAPI /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); /// - doesn't allow '+build' suffixes. /// </remarks> - private static readonly Regex Regex = new Regex(@"^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)(\.(?<patch>0|[1-9]\d*))?(?:-(?<prerelease>([a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex Regex = new Regex(@"^(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); /********* @@ -40,15 +41,16 @@ namespace StardewModdingAPI ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="major">The major version incremented for major API changes.</param> - /// <param name="minor">The minor version incremented for backwards-compatible changes.</param> - /// <param name="patch">The patch version for backwards-compatible bug fixes.</param> + /// <param name="majorVersion">The major version incremented for major API changes.</param> + /// <param name="minorVersion">The minor version incremented for backwards-compatible changes.</param> + /// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param> /// <param name="build">An optional build tag.</param> - public SemanticVersion(int major, int minor, int patch, string build = null) + [JsonConstructor] + public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) { - this.MajorVersion = major; - this.MinorVersion = minor; - this.PatchVersion = patch; + this.MajorVersion = majorVersion; + this.MinorVersion = minorVersion; + this.PatchVersion = patchVersion; this.Build = this.GetNormalisedTag(build); } @@ -117,8 +119,7 @@ namespace StardewModdingAPI { // compare numerically if possible { - int curNum, otherNum; - if (int.TryParse(curParts[i], out curNum) && int.TryParse(otherParts[i], out otherNum)) + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) return curNum.CompareTo(otherNum); } @@ -178,6 +179,16 @@ namespace StardewModdingAPI return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); } +#if !SMAPI_1_x + /// <summary>Indicates whether the current object is equal to another object of the same type.</summary> + /// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns> + /// <param name="other">An object to compare with this object.</param> + public bool Equals(ISemanticVersion other) + { + return other != null && this.CompareTo(other) == 0; + } +#endif + /// <summary>Get a string representation of the version.</summary> public override string ToString() { diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 4e871636..d393f5a9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -32,20 +32,41 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "DisabledMods": [ { + "Name": "Animal Mood Fix", + "ID": [ "GPeters-AnimalMoodFix" ], + "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + { + "Name": "Colored Chests", + "ID": [ "4befde5c-731c-4853-8e4b-c5cdf946805f" ], + "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + { "Name": "Modder Serialization Utility", "ID": [ "SerializerUtils-0-1" ], - "ReasonPhrase": "it's no longer used by any mods, and is no longer maintained." + "ReasonPhrase": "it's no longer maintained or used." + }, + { + "Name": "No Debug Mode", + "ID": [ "NoDebugMode" ], + "ReasonPhrase": "debug mode was removed in SMAPI 1.0." }, { "Name": "StarDustCore", "ID": [ "StarDustCore" ], - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere (which no longer uses it), and is no longer maintained." + "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, + { + "Name": "XmlSerializerRetool", + "ID": [ "XmlSerializerRetool.dll" ], + "ReasonPhrase": "it's no longer maintained or used." } ], /** * A list of mod versions SMAPI should consider compatible or broken regardless of whether it - * detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. + * detects incompatible code. The default for each record is to assume broken; to force SMAPI to + * load a mod regardless of compatibility checks, add a "Compatibility": "AssumeCompatible" field. * Changing this field is not recommended and may destabilise your game. */ "ModCompatibility": [ @@ -53,382 +74,417 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "AccessChestAnywhere", "ID": [ "AccessChestAnywhere" ], "UpperVersion": "1.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", - "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Needs update for SDV 1.1." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], + "Notes": "Broke in SDV 1.1." + }, + { + "Name": "AdjustArtisanPrices", + "ID": [ "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc" ], + "UpperVersion": "0.0", + "UpperVersionLabel": "0.01", + "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SMAPI 1.9." + }, + { + "Name": "Advanced Location Loader", + "ID": [ "Entoarox.AdvancedLocationLoader" ], + "UpperVersion": "1.2.10", + "UpdateUrls": [ "http://community.playstarbound.com/resources/3619" ], + "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, { "Name": "Almighty Tool", "ID": [ "AlmightyTool.dll" ], "UpperVersion": "1.1.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Better Sprinklers", "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], "UpperVersion": "2.3.1-pathoschild-update", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Birthday Mail", "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], "UpperVersion": "1.2.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/276", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Chest Label System", "ID": [ "SPDChestLabel" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.1." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.1." }, { "Name": "Chest Pooling", "ID": [ "ChestPooling.dll" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/111988", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Chests Anywhere", "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ], "UpperVersion": "1.9-beta", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "CJB Automation", "ID": [ "CJBAutomation" ], "UpperVersion": "1.4", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", - "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "CJB Cheats Menu", "ID": [ "CJBCheatsMenu" ], "UpperVersion": "1.12", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", - "Notes": "Needs update for SDV 1.1." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/4" ], + "Notes": "Broke in SDV 1.1." }, { "Name": "CJB Item Spawner", "ID": [ "CJBItemSpawner" ], "UpperVersion": "1.5", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", - "Notes": "Needs update for SDV 1.1." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], + "Notes": "Broke in SDV 1.1." }, { "Name": "CJB Show Item Sell Price", "ID": [ "CJBShowItemSellPrice" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Cooking Skill", "ID": [ "CookingSkill" ], "UpperVersion": "1.0.3", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", - "Notes": "Needs update for SDV 1.2." - }, - { - "Name": "Cooking Skill Prestige Adapter", - "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], - "UpperVersion": "1.0.4", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/569", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Enemy Health Bars", "ID": [ "SPDHealthBar" ], "UpperVersion": "1.7", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193", - "UnofficialUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Entoarox Framework", "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], - "UpperVersion": "1.7.5", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4228", - "Notes": "Needs update for SDV 1.2." + "UpperVersion": "1.7.10", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], + "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, { "Name": "Extended Fridge", "ID": [ "Mystra007ExtendedFridge" ], "UpperVersion": "1.0", "UpperVersionLabel": "0.94", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", - "Notes": "Needs update for SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485" ], + "Notes": "Broke in SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." }, { - "Name": "FarmAutomation.ItemCollector", - "ID": [ "FarmAutomation.ItemCollector.dll", /*since 0.4*/ "Maddy99.FarmAutomation.ItemCollector" ], + "Name": "Farm Automation: Item Collector", + "ID": [ "FarmAutomation.ItemCollector.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://community.playstarbound.com/threads/125172" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Farm Automation Unofficial: Item Collector", + "ID": [ "Maddy99.FarmAutomation.ItemCollector" ], "UpperVersion": "0.4", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/125172", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/threads/125172" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Instant Geode", "ID": [ "InstantGeode" ], "UpperVersion": "1.12", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/109038", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Gate Opener", "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ], "UpperVersion": "1.0.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/111988", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Get Dressed", "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], "UpperVersion": "3.3", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Gift Taste Helper", "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ], "UpperVersion": "2.3.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/229", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Lookup Anything", "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], "UpperVersion": "1.10.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Makeshift Multiplayer", "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], - "Compatibility": "AssumeBroken", "UpperVersion": "0.3.3", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "More Pets", + "ID": [ "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS", /* since 1.3 */ "Entoarox.MorePets" ], + "UpperVersion": "1.3.2", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4288" ], + "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, { "Name": "NoSoilDecay", "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], "UpperVersion": "0.5", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." }, { "Name": "NPC Map Locations", "ID": [ "NPCMapLocationsMod" ], "LowerVersion": "1.42", "UpperVersion": "1.43", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", - "ReasonPhrase": "These versions have an update check error which crash the game." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], + "ReasonPhrase": "this version has an update check error which crashes the game." }, { "Name": "Part of the Community", "ID": [ "SB_PotC" ], "UpperVersion": "1.0.8", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/923", - "ReasonPhrase": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], + "ReasonPhrase": "Broke in SDV 1.2." + }, + { + "Name": "Persival's BundleMod", + "ID": [ "BundleMod.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.1." }, { "Name": "Point-and-Plant", "ID": [ "PointAndPlant.dll" ], "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "PrairieKingMadeEasy", "ID": [ "PrairieKingMadeEasy.dll" ], - "UpperVersion": "1.0.0", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/3594", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Rush Orders", "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], "UpperVersion": "1.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/605", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Save Anywhere", - "ID": [ "SaveAnywhere" ], + "ID": [ "{'ID':'SaveAnywhere', 'Name':'Save Anywhere'}" ], // disambiguate from Night Owl "UpperVersion": "2.3", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Seasonal Immersion", + "ID": [ "EntoaroxSeasonalHouse", /*since 1.1.0*/ "EntoaroxSeasonalBuildings", /* since 1.6 or earlier*/ "EntoaroxSeasonalImmersion", /*since 1.7*/ "Entoarox.SeasonalImmersion" ], + "UpperVersion": "1.8.2", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4262" ], + "Notes": "Overhauled for SMAPI 1.11+ compatibility." + }, + { + "Name": "Shop Expander", + "ID": [ /*since at least 1.4*/ "821ce8f6-e629-41ad-9fde-03b54f68b0b6", /*since 1.5*/ "EntoaroxShopExpander", /*since 1.5.2*/ "Entoarox.ShopExpander" ], + "UpperVersion": "1.5.3", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4381" ], + "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, { "Name": "Simple Sprinklers", "ID": [ "SimpleSprinkler.dll" ], "UpperVersion": "1.4", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/76", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Siv's Marriage Mod", + "ID": [ "6266959802" ], + "UpperVersion": "1.2.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SMAPI 1.9 (has multiple Mod instances)." + }, + { + "Name": "Skill Prestige: Cooking Adapter", + "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], + "UpperVersion": "1.0.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Sprint and Dash", "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.0", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/3531", - "UnofficialUpdateUrl": "http://community.playstarbound.com/resources/4201", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://community.playstarbound.com/resources/4201" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Sprint and Dash Redux", "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4201", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Sprinting Mod", + "ID": [ "a10d3097-b073-4185-98ba-76b586cba00c" ], + "UpperVersion": "2.1", + "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "StackSplitX", "ID": [ "StackSplitX.dll" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Tainted Cellar", + "ID": [ "TaintedCellar.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.1 or 1.11." }, { "Name": "Teleporter", "ID": [ "Teleporter" ], "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4374", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/resources/4374" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Three-heart Dance Partner", + "ID": [ "ThreeHeartDancePartner" ], + "UpperVersion": "1.0.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "TimeSpeed", - "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "4108e859-333c-4fec-a1a7-d2e18c1019fe", /*since 2.1*/ "community.TimeSpeed" ], + "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'}", /* since 2.0.3 */ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'}", /*since 2.1*/ "community.TimeSpeed" ], // disambiguate other mods by Alpha_Omegasis "UpperVersion": "2.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/169", - "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "UiModSuite", "ID": [ "Demiacle.UiModSuite" ], - "UpperVersion": "0.5", - "UpperVersionLabel": "1.0", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1023", - "Notes": "Needs update for SDV 1.2. Actual upper version is 1.0, but mod incorrectly sets it to 0.5 in the manifest." + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Weather Controller", "ID": [ "WeatherController.dll" ], "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/111526", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Wonderful Farm Life", + "ID": [ "WonderfulFarmLife.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://community.playstarbound.com/threads/132096" ], + "Notes": "Broke in SDV 1.1 or 1.11." + }, + { + "Name": "Xnb Loader", + "ID": [ "Entoarox.XnbLoader" ], + "UpperVersion": "1.0.6", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4506" ], + "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, { "Name": "zDailyIncrease", "ID": [ "zdailyincrease" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4247", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoom Out Extreme", "ID": [ "ZoomMod" ], "UpperVersion": "0.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/115028", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Better RNG", "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Calendar Anywhere", "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Health Bars", "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Junimo Deposit Anywhere", "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ], "UpperVersion": "1.7", - "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Movement Mod", "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Regen Mod", "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", - "Notes": "Needs update for SDV 1.2." + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Notes": "Broke in SDV 1.2." } ] } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index f778660d..8c7279a1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,6 +91,10 @@ <Link>Properties\GlobalAssemblyInfo.cs</Link> </Compile> <Compile Include="Command.cs" /> + <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> + <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> + <Compile Include="Framework\Utilities\ContextHash.cs" /> + <Compile Include="Metadata\CoreAssets.cs" /> <Compile Include="ContentSource.cs" /> <Compile Include="Events\ContentEvents.cs" /> <Compile Include="Events\EventArgsInput.cs" /> @@ -125,9 +129,11 @@ <Compile Include="Events\EventArgsStringChanged.cs" /> <Compile Include="Events\GameEvents.cs" /> <Compile Include="Events\GraphicsEvents.cs" /> - <Compile Include="Framework\Countdown.cs" /> + <Compile Include="Framework\Utilities\Countdown.cs" /> + <Compile Include="Framework\GameVersion.cs" /> <Compile Include="Framework\IModMetadata.cs" /> <Compile Include="Framework\Models\DisabledMod.cs" /> + <Compile Include="Framework\Models\ModCompatibilityID.cs" /> <Compile Include="Framework\ModHelpers\BaseHelper.cs" /> <Compile Include="Framework\ModHelpers\CommandHelper.cs" /> <Compile Include="Framework\ModHelpers\ContentHelper.cs" /> @@ -159,7 +165,7 @@ <Compile Include="Framework\Exceptions\SParseException.cs" /> <Compile Include="Framework\Serialisation\JsonHelper.cs" /> <Compile Include="Framework\Serialisation\SelectiveStringEnumConverter.cs" /> - <Compile Include="Framework\Serialisation\ManifestFieldConverter.cs" /> + <Compile Include="Framework\Serialisation\SFieldConverter.cs" /> <Compile Include="IAssetEditor.cs" /> <Compile Include="IAssetInfo.cs" /> <Compile Include="IAssetLoader.cs" /> @@ -256,36 +262,6 @@ <Name>StardewModdingAPI.AssemblyRewriters</Name> </ProjectReference> </ItemGroup> - <Import Project="$(SolutionDir)\crossplatform.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <PropertyGroup> - <PostBuildEvent> - </PostBuildEvent> - </PropertyGroup> - <!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors --> - <Target Name="BeforeBuild"> - <Error Condition="!Exists('$(GamePath)')" Text="Failed to find the game install path automatically; edit the *.csproj file and manually add a <GamePath> setting with the full directory path containing the Stardew Valley executable." /> - </Target> - - <!-- copy files into game directory and enable debugging (only in debug mode) --> - <Target Name="AfterBuild" Condition="$(Configuration) == 'Debug'"> - <Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" /> - <Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)" /> - <Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" /> - <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" /> - <Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" /> - <Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" /> - <Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" /> - </Target> - - <!-- launch SMAPI on debug --> - <PropertyGroup Condition="$(Configuration) == 'Debug'"> - <StartAction>Program</StartAction> - <StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram> - <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory> - </PropertyGroup> - - <!-- Somehow this makes Visual Studio for Mac recognise the previous section. --> - <!-- Nobody knows why. --> - <PropertyGroup Condition="'$(RunConfiguration)' == 'Default'" /> + <Import Project="$(SolutionDir)\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/StardewModdingAPI/Utilities/SDate.cs index e0613491..d7631598 100644 --- a/src/StardewModdingAPI/Utilities/SDate.cs +++ b/src/StardewModdingAPI/Utilities/SDate.cs @@ -32,6 +32,11 @@ namespace StardewModdingAPI.Utilities /// <summary>The year.</summary> public int Year { get; } +#if !SMAPI_1_x + /// <summary>The day of week.</summary> + public DayOfWeek DayOfWeek { get; } +#endif + /********* ** Public methods @@ -64,6 +69,9 @@ namespace StardewModdingAPI.Utilities this.Day = day; this.Season = season; this.Year = year; +#if !SMAPI_1_x + this.DayOfWeek = this.GetDayOfWeek(); +#endif } /// <summary>Get the current in-game date.</summary> @@ -198,6 +206,30 @@ namespace StardewModdingAPI.Utilities /********* ** Private methods *********/ + /// <summary>Get the day of week for the current date.</summary> + private DayOfWeek GetDayOfWeek() + { + switch (this.Day % 7) + { + case 0: + return DayOfWeek.Sunday; + case 1: + return DayOfWeek.Monday; + case 2: + return DayOfWeek.Tuesday; + case 3: + return DayOfWeek.Wednesday; + case 4: + return DayOfWeek.Thursday; + case 5: + return DayOfWeek.Friday; + case 6: + return DayOfWeek.Saturday; + default: + return 0; + } + } + /// <summary>Get the current season index.</summary> /// <exception cref="InvalidOperationException">The current season wasn't recognised.</exception> private int GetSeasonIndex() diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh index 39fd4f29..70f1873a 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/StardewModdingAPI/unix-launcher.sh @@ -64,6 +64,8 @@ else # open SMAPI in terminal if $COMMAND x-terminal-emulator 2>/dev/null; then x-terminal-emulator -e "$LAUNCHER" + elif $COMMAND xfce4-terminal 2>/dev/null; then + xfce4-terminal -e "$LAUNCHER" elif $COMMAND gnome-terminal 2>/dev/null; then gnome-terminal -e "$LAUNCHER" elif $COMMAND xterm 2>/dev/null; then diff --git a/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs b/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs index 121ad9a6..ad79b4af 100644 --- a/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs +++ b/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs @@ -1,4 +1,5 @@ -using StardewModdingAPI; +#if SMAPI_1_x +using StardewModdingAPI; using StardewValley; using StardewValley.Menus; @@ -26,3 +27,4 @@ namespace TrainerMod.Framework.Commands.Saves } } } +#endif
\ No newline at end of file diff --git a/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs b/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs index 5f6941e9..ea2bd6a8 100644 --- a/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs +++ b/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs @@ -1,4 +1,5 @@ -using StardewModdingAPI; +#if SMAPI_1_x +using StardewModdingAPI; using StardewValley; namespace TrainerMod.Framework.Commands.Saves @@ -25,3 +26,4 @@ namespace TrainerMod.Framework.Commands.Saves } } } +#endif
\ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 57c8a907..73b40050 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -99,15 +99,6 @@ </None> <None Include="packages.config" /> </ItemGroup> - <Import Project="$(SolutionDir)\crossplatform.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <PropertyGroup> - <PostBuildEvent> - </PostBuildEvent> - </PropertyGroup> - <Target Name="AfterBuild" Condition="$(Configuration) == 'Debug'"> - <Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\TrainerMod" /> - <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\TrainerMod" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" /> - <Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\TrainerMod" /> - </Target> + <Import Project="$(SolutionDir)\common.targets" /> </Project>
\ No newline at end of file diff --git a/src/crossplatform.targets b/src/common.targets index 66e15abd..ee138524 100644 --- a/src/crossplatform.targets +++ b/src/common.targets @@ -1,6 +1,9 @@ -<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <!-- load dev settings --> <Import Condition="$(OS) != 'Windows_NT' AND Exists('$(HOME)\stardewvalley.targets')" Project="$(HOME)\stardewvalley.targets" /> <Import Condition="$(OS) == 'Windows_NT' AND Exists('$(USERPROFILE)\stardewvalley.targets')" Project="$(USERPROFILE)\stardewvalley.targets" /> + + <!-- find game path --> <PropertyGroup> <!-- Linux paths --> <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/GOG Games/Stardew Valley/game</GamePath> @@ -15,6 +18,8 @@ <GamePath Condition="!Exists('$(GamePath)') AND '$(OS)' == 'Windows_NT'">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath> <GamePath Condition="!Exists('$(GamePath)') AND '$(OS)' == 'Windows_NT'">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath> </PropertyGroup> + + <!-- add references--> <Choose> <When Condition="$(OS) == 'Windows_NT'"> <PropertyGroup> @@ -65,4 +70,38 @@ </ItemGroup> </Otherwise> </Choose> -</Project>
\ No newline at end of file + + <!-- if game path is invalid, show one user-friendly error instead of a slew of reference errors --> + <Target Name="BeforeBuild"> + <Error Condition="!Exists('$(GamePath)')" Text="Failed to find the game install path automatically; edit the *.csproj file and manually add a <GamePath> setting with the full directory path containing the Stardew Valley executable." /> + </Target> + + <!-- copy files into game directory and enable debugging (only in debug mode) --> + <Target Name="AfterBuild"> + <CallTarget Targets="CopySMAPI;CopyTrainerMod" Condition="'$(Configuration)' == 'Debug'" /> + </Target> + <Target Name="CopySMAPI" Condition="'$(MSBuildProjectName)' == 'StardewModdingAPI'"> + <Copy SourceFiles="$(TargetDir)\$(TargetName).exe" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).config.json" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" /> + <Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" /> + </Target> + <Target Name="CopyTrainerMod" Condition="'$(MSBuildProjectName)' == 'TrainerMod'"> + <Copy SourceFiles="$(TargetDir)\$(TargetName).dll" DestinationFolder="$(GamePath)\Mods\TrainerMod" /> + <Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)\Mods\TrainerMod" Condition="Exists('$(TargetDir)\$(TargetName).pdb')" /> + <Copy SourceFiles="$(TargetDir)\manifest.json" DestinationFolder="$(GamePath)\Mods\TrainerMod" /> + </Target> + + <!-- launch SMAPI on debug --> + <PropertyGroup Condition="$(Configuration) == 'Debug'"> + <StartAction>Program</StartAction> + <StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram> + <StartWorkingDirectory>$(GamePath)</StartWorkingDirectory> + </PropertyGroup> + + <!-- Somehow this makes Visual Studio for Mac recognise the previous section. Nobody knows why. --> + <PropertyGroup Condition="'$(RunConfiguration)' == 'Default'" /> +</Project> |