diff options
Diffstat (limited to 'src')
38 files changed, 330 insertions, 168 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 5a6aa747..d00a5df4 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -206,7 +206,7 @@ namespace StardewModdingApi.Installer Console.WriteLine(); // handle choice - string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }); + string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }, printLine: Console.WriteLine); switch (choice) { case "1": @@ -453,6 +453,7 @@ namespace StardewModdingApi.Installer } // find target folder + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract -- avoid error if the Mods folder has invalid mods, since they're not validated yet ModFolder? targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true); DirectoryInfo defaultTargetFolder = new(Path.Combine(paths.ModsPath, sourceMod.Directory.Name)); DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder; @@ -628,22 +629,22 @@ namespace StardewModdingApi.Installer } /// <summary>Interactively ask the user to choose a value.</summary> - /// <param name="print">A callback which prints a message to the console.</param> + /// <param name="printLine">A callback which prints a message to the console.</param> /// <param name="message">The message to print.</param> /// <param name="options">The allowed options (not case sensitive).</param> /// <param name="indent">The indentation to prefix to output.</param> - private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? print = null) + private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? printLine = null) { - print ??= this.PrintInfo; + printLine ??= this.PrintInfo; while (true) { - print(indent + message); + printLine(indent + message); Console.Write(indent); string? input = Console.ReadLine()?.Trim().ToLowerInvariant(); if (input == null || !options.Contains(input)) { - print($"{indent}That's not a valid option."); + printLine($"{indent}That's not a valid option."); continue; } return input; diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index c7026ee1..88412d92 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -227,8 +227,7 @@ namespace StardewModdingAPI.ModBuildConfig string fromPath = entry.Value.FullName; string toPath = Path.Combine(modFolderPath, entry.Key); - // ReSharper disable once AssignNullToNotNullAttribute -- not applicable in this context - Directory.CreateDirectory(Path.GetDirectoryName(toPath)); + Directory.CreateDirectory(Path.GetDirectoryName(toPath)!); File.Copy(fromPath, toPath, overwrite: true); } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 3722e155..88ddfe6b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework /// <summary>Get all spawnable items.</summary> /// <param name="itemTypes">The item types to fetch (or null for any type).</param> /// <param name="includeVariants">Whether to include flavored variants like "Sunflower Honey".</param> - [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")] + [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = $"{nameof(ItemRepository.TryCreate)} invokes the lambda immediately.")] public IEnumerable<SearchableItem> GetAll(ItemType[]? itemTypes = null, bool includeVariants = true) { // diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 300de9d2..3c2dec19 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.15.0", + "Version": "3.15.1", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.15.0" + "MinimumApiVersion": "3.15.1" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 15a1e0f3..28b4b149 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.15.0", + "Version": "3.15.1", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.15.0" + "MinimumApiVersion": "3.15.1" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 1a11742c..1944575b 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.15.0", + "Version": "3.15.1", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.15.0" + "MinimumApiVersion": "3.15.1" } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 7f06d170..3bdd145a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -283,8 +283,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } /// <summary>The response model for the MediaWiki parse API.</summary> - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialization.")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialization.")] private class ResponseModel { /********* @@ -306,9 +306,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } /// <summary>The inner response model for the MediaWiki parse API.</summary> - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialization.")] + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local", Justification = "Used via JSON deserialization.")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialization.")] private class ResponseParseModel { /********* diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index 6978567e..f464f4bb 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -21,7 +21,8 @@ namespace StardewModdingAPI.Toolkit.Framework /// <summary>Get the OS name from the system uname command.</summary> /// <param name="buffer">The buffer to fill with the resulting string.</param> [DllImport("libc")] - static extern int uname(IntPtr buffer); + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "This is the actual external command name.")] + private static extern int uname(IntPtr buffer); /********* @@ -51,7 +52,6 @@ namespace StardewModdingAPI.Toolkit.Framework /// <summary>Get the human-readable OS name and version.</summary> /// <param name="platform">The current platform.</param> - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] public static string GetFriendlyPlatformName(string platform) { #if SMAPI_FOR_WINDOWS @@ -65,7 +65,10 @@ namespace StardewModdingAPI.Toolkit.Framework return result ?? "Windows"; } - catch { } + catch + { + // fallback to default behavior + } #endif string name = Environment.OSVersion.ToString(); diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 0df75a31..55b9bdd8 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Toolkit /// <param name="metadataPath">The file path for the SMAPI metadata file.</param> public ModDatabase GetModDatabase(string metadataPath) { - MetadataModel metadata = JsonConvert.DeserializeObject<MetadataModel>(File.ReadAllText(metadataPath)); + MetadataModel metadata = JsonConvert.DeserializeObject<MetadataModel>(File.ReadAllText(metadataPath)) ?? new MetadataModel(); ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); return new ModDatabase(records, this.GetUpdateUrl); } diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs index c32c3185..913d54e0 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -48,7 +48,12 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters return this.ReadObject(JObject.Load(reader)); case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value<string>(), path); + { + string? value = JToken.Load(reader).Value<string>(); + return value is not null + ? this.ReadString(value, path) + : null; + } default: throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs index 1c59f5e7..cdf2ed77 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs @@ -42,7 +42,12 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters return this.ReadObject(JObject.Load(reader), path); case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value<string>(), path); + { + string? value = JToken.Load(reader).Value<string>(); + return value is not null + ? this.ReadString(value, path) + : null; + } default: throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs index c60b2c90..f5a5f930 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop /// <summary>Get update check info about a mod.</summary> /// <param name="id">The mod ID.</param> - [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this method.")] + [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The nullability is validated in this method.")] public async Task<IModPage?> GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 7fc8f958..0efa62c5 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -200,6 +200,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing log.GameVersion = match.Groups["gameVersion"].Value; log.OperatingSystem = match.Groups["os"].Value; smapiMod.OverrideVersion(log.ApiVersion); + + log.ApiVersionParsed = smapiMod.GetParsedVersion(); } // mod path line diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs index 4b80a830..557f08ff 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs @@ -1,4 +1,6 @@ +using System; using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework.LogParsing.Models { @@ -6,6 +8,13 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models public class LogModInfo { /********* + ** Private fields + *********/ + /// <summary>The parsed mod version, if valid.</summary> + private Lazy<ISemanticVersion?> ParsedVersionImpl; + + + /********* ** Accessors *********/ /// <summary>The mod name.</summary> @@ -68,7 +77,6 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models { this.Name = name; this.Author = author; - this.Version = version; this.Description = description; this.UpdateVersion = updateVersion; this.UpdateLink = updateLink; @@ -82,6 +90,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models this.IsContentPack = !string.IsNullOrWhiteSpace(this.ContentPackFor); this.IsCodeMod = !this.IsContentPack; } + + this.OverrideVersion(version); } /// <summary>Add an update alert for this mod.</summary> @@ -95,9 +105,29 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// <summary>Override the version number, for cases like SMAPI itself where the version is only known later during parsing.</summary> /// <param name="version">The new mod version.</param> + [MemberNotNull(nameof(LogModInfo.Version), nameof(LogModInfo.ParsedVersionImpl))] public void OverrideVersion(string version) { this.Version = version; + this.ParsedVersionImpl = new Lazy<ISemanticVersion?>(this.ParseVersion); + } + + /// <summary>Get the semantic version for this mod, if it's valid.</summary> + public ISemanticVersion? GetParsedVersion() + { + return this.ParsedVersionImpl.Value; + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the semantic version for this mod, if it's valid.</summary> + public ISemanticVersion? ParseVersion() + { + return !string.IsNullOrWhiteSpace(this.Version) && SemanticVersion.TryParse(this.Version, out ISemanticVersion? version) + ? version + : null; } } } diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs index 6951e434..3f649199 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -28,6 +28,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// <summary>The SMAPI version.</summary> public string? ApiVersion { get; set; } + /// <summary>The parsed SMAPI version, if it's valid.</summary> + public ISemanticVersion? ApiVersionParsed { get; set; } + /// <summary>The game version.</summary> public string? GameVersion { get; set; } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 33239a2b..57e26ace 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -8,24 +8,30 @@ @{ ViewData["Title"] = "SMAPI log parser"; + // get log info ParsedLog? log = Model!.ParsedLog; - IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod(); + ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>()); + + // detect suggested fixes + LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? Array.Empty<LogModInfo>(); + LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler"); + bool hasOlderErrorHandler = errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion()); + bool isPyTkCompatibilityMode = log?.ApiVersionParsed?.IsOlderThan("3.15.0") is false && log.Mods.Any(p => p.IsCodeMod && p.Name == "PyTK" && p.GetParsedVersion()?.IsOlderThan("1.23.1") is true); + + // get filters IDictionary<string, bool> defaultFilters = Enum .GetValues<LogLevel>() .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); - IDictionary<int, string> logLevels = Enum .GetValues<LogLevel>() .ToDictionary(level => (int)level, level => level.ToString().ToLower()); - IDictionary<int, string> logSections = Enum .GetValues<LogSection>() .ToDictionary(section => (int)section, section => section.ToString()); + // get form string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true)!; - - ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>()); } @section Head { @@ -34,7 +40,7 @@ <meta name="robots" content="noindex" /> } <link rel="stylesheet" href="~/Content/css/file-upload.css" /> - <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20220409" /> + <link rel="stylesheet" href="~/Content/css/log-parser.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" /> <script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script> @@ -69,7 +75,7 @@ </text> } </script> - + <script> $(function() { smapi.logParser( @@ -158,7 +164,7 @@ else if (log?.IsValid == true) <div id="os-instructions"> <div> <ul data-tabs> - @foreach (Platform platform in new[] {Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows}) + @foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows }) { @if (platform == Platform.Windows) { @@ -237,55 +243,70 @@ else if (log?.IsValid == true) @if (log?.IsValid == true) { <div id="output"> - @if (log.Mods.Any(mod => mod.HasUpdate)) + @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode) { <h2>Suggested fixes</h2> <ul id="fix-list"> - <li> - Consider updating these mods to fix problems: + @if (errorHandler is null) + { + <li class="important">You don't have the <strong>Error Handler</strong> mod installed. This automatically prevents many game or mod errors. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to re-add it.</li> + } + @if (hasOlderErrorHandler) + { + <li>Your <strong>Error Handler</strong> mod is older than SMAPI. You may be missing some game/mod error fixes. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to update it.</li> + } + @if (isPyTkCompatibilityMode) + { + <li>PyTK 1.23.0 or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.</li> + } + @if (outdatedMods.Any()) + { + <li> + Consider updating these mods to fix problems: - <table id="updates" class="table"> - @foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate)))) - { - <tr class="mod-entry"> - <td> - <strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong> - @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList)) - { - <div class="content-packs"> - @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate)) - { - <text>+ @contentPack.Name</text><br /> - } - </div> - } - </td> - <td> - @if (mod.HasUpdate) - { - <a href="@mod.UpdateLink" target="_blank"> - @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}") - </a> - } - else - { - <text> </text> - } + <table id="updates" class="table"> + @foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate)))) + { + <tr class="mod-entry"> + <td> + <strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong> + @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList)) + { + <div class="content-packs"> + @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate)) + { + <text>+ @contentPack.Name</text><br /> + } + </div> + } + </td> + <td> + @if (mod.HasUpdate) + { + <a href="@mod.UpdateLink" target="_blank"> + @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}") + |
