diff options
Diffstat (limited to 'src/SMAPI.Toolkit/Framework')
24 files changed, 434 insertions, 281 deletions
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 2f58a3f1..4fc4ea54 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// <summary>Metadata about a mod.</summary> @@ -7,15 +9,26 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// <summary>The mod's unique ID (if known).</summary> - public string ID { get; set; } + public string ID { get; } /// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary> - public ModEntryVersionModel SuggestedUpdate { get; set; } + public ModEntryVersionModel? SuggestedUpdate { get; set; } /// <summary>Optional extended data which isn't needed for update checks.</summary> - public ModExtendedMetadataModel Metadata { get; set; } + public ModExtendedMetadataModel? Metadata { get; set; } /// <summary>The errors that occurred while fetching update data.</summary> - public string[] Errors { get; set; } = new string[0]; + public string[] Errors { get; set; } = Array.Empty<string>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The mod's unique ID (if known).</param> + public ModEntryModel(string id) + { + this.ID = id; + } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs index 188db31d..a1e78986 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs @@ -11,19 +11,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi *********/ /// <summary>The version number.</summary> [JsonConverter(typeof(NonStandardSemanticVersionConverter))] - public ISemanticVersion Version { get; set; } + public ISemanticVersion Version { get; } /// <summary>The mod page URL.</summary> - public string Url { get; set; } + public string Url { get; } /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - public ModEntryVersionModel() { } - - /// <summary>Construct an instance.</summary> /// <param name="version">The version number.</param> /// <param name="url">The mod page URL.</param> public ModEntryVersionModel(ISemanticVersion version, string url) diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 5c2ce366..272a2063 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; @@ -17,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Mod info ****/ /// <summary>The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates).</summary> - public string[] ID { get; set; } = new string[0]; + public string[] ID { get; set; } = Array.Empty<string>(); /// <summary>The mod's display name.</summary> - public string Name { get; set; } + public string? Name { get; set; } /// <summary>The mod ID on Nexus.</summary> public int? NexusID { get; set; } @@ -32,31 +33,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public int? CurseForgeID { get; set; } /// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary> - public string CurseForgeKey { get; set; } + public string? CurseForgeKey { get; set; } /// <summary>The mod ID in the ModDrop mod repo.</summary> public int? ModDropID { get; set; } /// <summary>The GitHub repository in the form 'owner/repo'.</summary> - public string GitHubRepo { get; set; } + public string? GitHubRepo { get; set; } /// <summary>The URL to a non-GitHub source repo.</summary> - public string CustomSourceUrl { get; set; } + public string? CustomSourceUrl { get; set; } /// <summary>The custom mod page URL (if applicable).</summary> - public string CustomUrl { get; set; } + public string? CustomUrl { get; set; } /// <summary>The main version.</summary> - public ModEntryVersionModel Main { get; set; } + public ModEntryVersionModel? Main { get; set; } /// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary> - public ModEntryVersionModel Optional { get; set; } + public ModEntryVersionModel? Optional { get; set; } /// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary> - public ModEntryVersionModel Unofficial { get; set; } + public ModEntryVersionModel? Unofficial { get; set; } /// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</summary> - public ModEntryVersionModel UnofficialForBeta { get; set; } + public ModEntryVersionModel? UnofficialForBeta { get; set; } /**** ** Stable compatibility @@ -66,10 +67,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public WikiCompatibilityStatus? CompatibilityStatus { get; set; } /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary> - public string CompatibilitySummary { get; set; } + public string? CompatibilitySummary { get; set; } /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary> - public string BrokeIn { get; set; } + public string? BrokeIn { get; set; } /**** ** Beta compatibility @@ -79,22 +80,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } /// <summary>The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting.</summary> - public string BetaCompatibilitySummary { get; set; } + public string? BetaCompatibilitySummary { get; set; } /// <summary>The beta game or SMAPI version which broke this mod, if applicable.</summary> - public string BetaBrokeIn { get; set; } + public string? BetaBrokeIn { get; set; } /**** ** Version mappings ****/ /// <summary>A serialized change descriptor to apply to the local version during update checks (see <see cref="ChangeDescriptor"/>).</summary> - public string ChangeLocalVersions { get; set; } + public string? ChangeLocalVersions { get; set; } /// <summary>A serialized change descriptor to apply to the remote version during update checks (see <see cref="ChangeDescriptor"/>).</summary> - public string ChangeRemoteVersions { get; set; } + public string? ChangeRemoteVersions { get; set; } /// <summary>A serialized change descriptor to apply to the update keys during update checks (see <see cref="ChangeDescriptor"/>).</summary> - public string ChangeUpdateKeys { get; set; } + public string? ChangeUpdateKeys { get; set; } /********* @@ -110,7 +111,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <param name="optional">The latest optional version, if newer than <paramref name="main"/>.</param> /// <param name="unofficial">The latest unofficial version, if newer than <paramref name="main"/> and <paramref name="optional"/>.</param> /// <param name="unofficialForBeta">The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</param> - public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta) + public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial, ModEntryVersionModel? unofficialForBeta) { // versions this.Main = main; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index bf81e102..9c11e1db 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -1,3 +1,6 @@ +using System; +using System.Linq; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// <summary>Specifies the identifiers for a mod to match.</summary> @@ -7,37 +10,39 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// <summary>The unique mod ID.</summary> - public string ID { get; set; } + public string ID { get; } /// <summary>The namespaced mod update keys (if available).</summary> - public string[] UpdateKeys { get; set; } + public string[] UpdateKeys { get; private set; } /// <summary>The mod version installed by the local player. This is used for version mapping in some cases.</summary> - public ISemanticVersion InstalledVersion { get; set; } + public ISemanticVersion? InstalledVersion { get; } /// <summary>Whether the installed version is broken or could not be loaded.</summary> - public bool IsBroken { get; set; } + public bool IsBroken { get; } /********* ** Public methods *********/ - /// <summary>Construct an empty instance.</summary> - public ModSearchEntryModel() - { - // needed for JSON deserializing - } - /// <summary>Construct an instance.</summary> /// <param name="id">The unique mod ID.</param> /// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param> /// <param name="updateKeys">The namespaced mod update keys (if available).</param> /// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param> - public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false) + public ModSearchEntryModel(string id, ISemanticVersion? installedVersion, string[]? updateKeys, bool isBroken = false) { this.ID = id; this.InstalledVersion = installedVersion; - this.UpdateKeys = updateKeys ?? new string[0]; + this.UpdateKeys = updateKeys ?? Array.Empty<string>(); + this.IsBroken = isBroken; + } + + /// <summary>Add update keys for the mod.</summary> + /// <param name="updateKeys">The update keys to add.</param> + public void AddUpdateKeys(params string[] updateKeys) + { + this.UpdateKeys = this.UpdateKeys.Concat(updateKeys).ToArray(); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs index 73698173..3c74bab0 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -1,3 +1,5 @@ +using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Utilities; @@ -22,16 +24,23 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public ISemanticVersion GameVersion { get; set; } /// <summary>The OS on which the player plays.</summary> - public Platform? Platform { get; set; } + public Platform Platform { get; set; } /********* ** Public methods *********/ /// <summary>Construct an empty instance.</summary> + [Obsolete("This constructor only exists to support ASP.NET model binding, and shouldn't be used directly.")] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used by ASP.NET model binding.")] public ModSearchModel() { - // needed for JSON deserializing + // ASP.NET Web API needs a public empty constructor for top-level request models, and + // it'll fail if the other constructor is marked with [JsonConstructor]. Apparently + // it's fine with non-empty constructors in nested models like ModSearchEntryModel. + this.Mods = Array.Empty<ModSearchEntryModel>(); + this.ApiVersion = null!; + this.GameVersion = null!; } /// <summary>Construct an instance.</summary> diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index c2d906a0..d4282617 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -62,15 +62,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi private TResult Post<TBody, TResult>(string url, TBody content) { // note: avoid HttpClient for macOS compatibility - using WebClient client = new WebClient(); + using WebClient client = new(); - Uri fullUrl = new Uri(this.BaseUrl, url); + Uri fullUrl = new(this.BaseUrl, url); string data = JsonConvert.SerializeObject(content); client.Headers["Content-Type"] = "application/json"; client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings); + return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings) + ?? throw new InvalidOperationException($"Could not parse the response from POST {url}."); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs index f1feb44b..a2497dea 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki @@ -47,11 +48,19 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <summary>Apply the change descriptors to a comma-delimited field.</summary> /// <param name="rawField">The raw field text.</param> /// <returns>Returns the modified field.</returns> - public string ApplyToCopy(string rawField) +#if NET5_0_OR_GREATER + [return: NotNullIfNotNull("rawField")] +#endif + public string? ApplyToCopy(string? rawField) { // get list List<string> values = !string.IsNullOrWhiteSpace(rawField) - ? new List<string>(rawField.Split(',')) + ? new List<string>( + from field in rawField.Split(',') + let value = field.Trim() + where value.Length > 0 + select value + ) : new List<string>(); // apply changes @@ -73,12 +82,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { for (int i = values.Count - 1; i >= 0; i--) { - string value = this.FormatValue(values[i]?.Trim() ?? string.Empty); + string value = this.FormatValue(values[i].Trim()); if (this.Remove.Contains(value)) values.RemoveAt(i); - else if (this.Replace.TryGetValue(value, out string newValue)) + else if (this.Replace.TryGetValue(value, out string? newValue)) values[i] = newValue; } } @@ -86,7 +95,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki // add values if (this.Add.Any()) { - HashSet<string> curValues = new HashSet<string>(values.Select(p => p?.Trim() ?? string.Empty), StringComparer.OrdinalIgnoreCase); + HashSet<string> curValues = new HashSet<string>(values.Select(p => p.Trim()), StringComparer.OrdinalIgnoreCase); foreach (string add in this.Add) { if (!curValues.Contains(add)) @@ -119,7 +128,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <param name="descriptor">The raw change descriptor.</param> /// <param name="errors">The human-readable error message describing any invalid values that were ignored.</param> /// <param name="formatValue">Format a raw value into a normalized form if needed.</param> - public static ChangeDescriptor Parse(string descriptor, out string[] errors, Func<string, string> formatValue = null) + public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func<string, string>? formatValue = null) { // init formatValue ??= p => p; @@ -179,7 +188,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki errors = rawErrors.ToArray(); } else - errors = new string[0]; + errors = Array.Empty<string>(); // build model return new ChangeDescriptor( diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index f85e82e1..7f06d170 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -51,8 +51,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki doc.LoadHtml(html); // fetch game versions - string stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText; - string betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText; + string? stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText; + string? betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText; if (betaVersion == stableVersion) betaVersion = null; @@ -63,9 +63,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki if (modNodes == null) throw new InvalidOperationException("Can't parse wiki compatibility list, no mod data overrides section found."); - foreach (var entry in this.ParseOverrideEntries(modNodes)) + foreach (WikiDataOverrideEntry entry in this.ParseOverrideEntries(modNodes)) { - if (entry.Ids?.Any() != true || !entry.HasChanges) + if (entry.Ids.Any() != true || !entry.HasChanges) continue; foreach (string id in entry.Ids) @@ -83,18 +83,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } // build model - return new WikiModList - { - StableVersion = stableVersion, - BetaVersion = betaVersion, - Mods = mods - }; + return new WikiModList( + stableVersion: stableVersion, + betaVersion: betaVersion, + mods: mods + ); } /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } @@ -116,71 +115,68 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id"); - string curseForgeKey = this.GetAttribute(node, "data-curseforge-key"); + string? curseForgeKey = this.GetAttribute(node, "data-curseforge-key"); int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id"); - string githubRepo = this.GetAttribute(node, "data-github"); - string customSourceUrl = this.GetAttribute(node, "data-custom-source"); - string customUrl = this.GetAttribute(node, "data-url"); - string anchor = this.GetAttribute(node, "id"); - string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); - string devNote = this.GetAttribute(node, "data-dev-note"); - string pullRequestUrl = this.GetAttribute(node, "data-pr"); + string? githubRepo = this.GetAttribute(node, "data-github"); + string? customSourceUrl = this.GetAttribute(node, "data-custom-source"); + string? customUrl = this.GetAttribute(node, "data-url"); + string? anchor = this.GetAttribute(node, "id"); + string? contentPackFor = this.GetAttribute(node, "data-content-pack-for"); + string? devNote = this.GetAttribute(node, "data-dev-note"); + string? pullRequestUrl = this.GetAttribute(node, "data-pr"); // parse stable compatibility - WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo - { - Status = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok, - BrokeIn = this.GetAttribute(node, "data-broke-in"), - UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"), - UnofficialUrl = this.GetAttribute(node, "data-unofficial-url"), - Summary = this.GetInnerHtml(node, "mod-summary")?.Trim() - }; + WikiCompatibilityInfo compatibility = new( + status: this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok, + brokeIn: this.GetAttribute(node, "data-broke-in"), + unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"), + unofficialUrl: this.GetAttribute(node, "data-unofficial-url"), + summary: this.GetInnerHtml(node, "mod-summary")?.Trim() + ); // parse beta compatibility - WikiCompatibilityInfo betaCompatibility = null; + WikiCompatibilityInfo? betaCompatibility = null; { WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-beta-status"); if (betaStatus.HasValue) { - betaCompatibility = new WikiCompatibilityInfo - { - Status = betaStatus.Value, - BrokeIn = this.GetAttribute(node, "data-beta-broke-in"), - UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"), - UnofficialUrl = this.GetAttribute(node, "data-beta-unofficial-url"), - Summary = this.GetInnerHtml(node, "mod-beta-summary") - }; + betaCompatibility = new WikiCompatibilityInfo( + status: betaStatus.Value, + brokeIn: this.GetAttribute(node, "data-beta-broke-in"), + unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"), + unofficialUrl: this.GetAttribute(node, "data-beta-unofficial-url"), + summary: this.GetInnerHtml(node, "mod-beta-summary") + ); } } // find data overrides - WikiDataOverrideEntry overrides = ids + WikiDataOverrideEntry? overrides = ids .Select(id => overridesById.TryGetValue(id, out overrides) ? overrides : null) .FirstOrDefault(p => p != null); // yield model - yield return new WikiModEntry - { - ID = ids, - Name = names, - Author = authors, - NexusID = nexusID, - ChucklefishID = chucklefishID, - CurseForgeID = curseForgeID, - CurseForgeKey = curseForgeKey, - ModDropID = modDropID, - GitHubRepo = githubRepo, - CustomSourceUrl = customSourceUrl, - CustomUrl = customUrl, - ContentPackFor = contentPackFor, - Compatibility = compatibility, - BetaCompatibility = betaCompatibility, - Warnings = warnings, - PullRequestUrl = pullRequestUrl, - DevNote = devNote, - Overrides = overrides, - Anchor = anchor - }; + yield return new WikiModEntry( + id: ids, + name: names, + author: authors, + nexusId: nexusID, + chucklefishId: chucklefishID, + curseForgeId: curseForgeID, + curseForgeKey: curseForgeKey, + modDropId: modDropID, + githubRepo: githubRepo, + customSourceUrl: customSourceUrl, + customUrl: customUrl, + contentPackFor: contentPackFor, + compatibility: compatibility, + betaCompatibility: betaCompatibility, + warnings: warnings, + pullRequestUrl: pullRequestUrl, + devNote: devNote, + overrides: overrides, + anchor: anchor + ); } } @@ -194,10 +190,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { Ids = this.GetAttributeAsCsv(node, "data-id"), ChangeLocalVersions = this.GetAttributeAsChangeDescriptor(node, "data-local-version", - raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw + raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw ), ChangeRemoteVersions = this.GetAttributeAsChangeDescriptor(node, "data-remote-version", - raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw + raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw ), ChangeUpdateKeys = this.GetAttributeAsChangeDescriptor(node, "data-update-keys", @@ -210,7 +206,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <summary>Get an attribute value.</summary> /// <param name="element">The element whose attributes to read.</param> /// <param name="name">The attribute name.</param> - private string GetAttribute(HtmlNode element, string name) + private string? GetAttribute(HtmlNode element, string name) { string value = element.GetAttributeValue(name, null); if (string.IsNullOrWhiteSpace(value)) @@ -223,9 +219,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <param name="element">The element whose attributes to read.</param> /// <param name="name">The attribute name.</param> /// <param name="formatValue">Format an raw entry value when applying changes.</param> - private ChangeDescriptor GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func<string, string> formatValue) + private ChangeDescriptor? GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func<string, string> formatValue) { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); return raw != null ? ChangeDescriptor.Parse(raw, out _, formatValue) : null; @@ -236,10 +232,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <param name="name">The attribute name.</param> private string[] GetAttributeAsCsv(HtmlNode element, string name) { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); return !string.IsNullOrWhiteSpace(raw) ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() - : new string[0]; + : Array.Empty<string>(); } /// <summary>Get an attribute value and parse it as an enum value.</summary> @@ -248,7 +244,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <param name="name">The attribute name.</param> private TEnum? GetAttributeAsEnum<TEnum>(HtmlNode element, string name) where TEnum : struct { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); if (raw == null) return null; if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value)) @@ -259,10 +255,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <summary>Get an attribute value and parse it as a semantic version.</summary> /// <param name="element">The element whose attributes to read.</param> /// <param name="name">The attribute name.</param> - private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name) + private ISemanticVersion? GetAttributeAsSemanticVersion(HtmlNode element, string name) { - string raw = this.GetAttribute(element, name); - return SemanticVersion.TryParse(raw, out ISemanticVersion version) + string? raw = this.GetAttribute(element, name); + return SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version : null; } @@ -272,7 +268,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <param name="name">The attribute name.</param> private int? GetAttributeAsNullableInt(HtmlNode element, string name) { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); if (raw != null && int.TryParse(raw, out int value)) return value; return null; @@ -281,7 +277,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// <summary>Get the text of an element with the given class name.</summary> /// <param name="container">The metadata container.</param> /// <param name="className">The field name.</param> - private string GetInnerHtml(HtmlNode container, string className) + private string? GetInnerHtml(HtmlNode container, string className) { return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml; } @@ -291,8 +287,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] private class ResponseModel { + /********* + ** Accessors + *********/ /// <summary>The parse API results.</summary> - public ResponseParseModel Parse { get; set; } + public ResponseParseModel Parse { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="parse">The parse API results.</param> + public ResponseModel(ResponseParseModel parse) + { + this.Parse = parse; + } } /// <summary>The inner response model for the MediaWiki parse API.</summary> @@ -301,8 +311,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] private class ResponseParseModel { + /********* + ** Accessors + *********/ /// <summary>The parsed text.</summary> - public IDictionary<string, string> Text { get; set; } + public IDictionary<string, string> Text { get; } = new Dictionary<string, string>(); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs index 204acd2b..71c90d0c 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -7,18 +7,37 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki ** Accessors *********/ /// <summary>The compatibility status.</summary> - public WikiCompatibilityStatus Status { get; set; } + public WikiCompatibilityStatus Status { get; } /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary> - public string Summary { get; set; } + public string? Summary { get; } - /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary> - public string BrokeIn { get; set; } + /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary> + public string? BrokeIn { get; } /// <summary>The version of the latest unofficial update, if applicable.</summary> - public ISemanticVersion UnofficialVersion { get; set; } + public ISemanticVersion? UnofficialVersion { get; } /// <summary>The URL to the latest unofficial update, if applicable.</summary> - public string UnofficialUrl { get; set; } + public string? UnofficialUrl { get; } + + + /********* + ** Accessors + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="status">The compatibility status.</param> + /// <param name="summary">The human-readable summary of the compatibility status or workaround, without HTML formatting.</param> + /// <param name="brokeIn">The game or SMAPI version which broke this mod, if applicable.</param> + /// <param name="unofficialVersion">The version of the latest unofficial update, if applicable.</param> + /// <param name="unofficialUrl">The URL to the latest unofficial update, if applicable.</param> + public WikiCompatibilityInfo(WikiCompatibilityStatus status, string? summary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl) + { + this.Status = status; + this.Summary = summary; + this.BrokeIn = brokeIn; + this.UnofficialVersion = unofficialVersion; + this.UnofficialUrl = unofficialUrl; + } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs index 0587e09d..a6f5a88f 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs @@ -1,4 +1,4 @@ -#nullable enable +using System; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki ** Accessors *********/ /// <summary>The unique mod IDs for the mods to override.</summary> - public string[] Ids { get; set; } = new string[0]; + public string[] Ids { get; set; } = Array.Empty<string>(); /// <summary>Maps local versions to a semantic version for update checks.</summary> public ChangeDescriptor? ChangeLocalVersions { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 4e0104da..fc50125f 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// <summary>A mod entry in the wiki list.</summary> @@ -6,64 +8,114 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /********* ** Accessors *********/ - /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order.</summary> - public string[] ID { get; set; } + /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order.</summary> + public string[] ID { get; } /// <summary>The mod's display name. If the mod has multiple names, the first one is the most canonical name.</summary> - public string[] Name { get; set; } + public string[] Name { get; } - /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary> - public string[] Author { get; set; } + /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary> + public string[] Author { get; } /// <summary>The mod ID on Nexus.</summary> - public int? NexusID { get; set; } + public int? NexusID { get; } /// <summary>The mod ID in the Chucklefish mod repo.</summary> - public int? ChucklefishID { get; set; } + public int? ChucklefishID { get; } /// <summary>The mod ID in the CurseForge mod repo.</summary> - public int? CurseForgeID { get; set; } + public int? CurseForgeID { get; } /// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary> - public string CurseForgeKey { get; set; } + public string? CurseForgeKey { get; } /// <summary>The mod ID in the ModDrop mod repo.</summary> - public int? ModDropID { get; set; } + public int? ModDropID { get; } /// <summary>The GitHub repository in the form 'owner/repo'.</summary> - public string GitHubRepo { get; set; } + public string? GitHubRepo { get; } /// <summary>The URL to a non-GitHub source repo.</summary> - public string CustomSourceUrl { get; set; } + public string? CustomSourceUrl { get; } /// <summary>The custom mod page URL (if applicable).</summary> - public string CustomUrl { get; set; } + public string? CustomUrl { get; } /// <summary>The name of the mod which loads this content pack, if applicable.</summary> - public string ContentPackFor { get; set; } + public string? ContentPackFor { get; } /// <summary>The mod's compatibility with the latest stable version of the game.</summary> - public WikiCompatibilityInfo Compatibility { get; set; } + public WikiCompatibilityInfo Compatibility { get; } /// <summary>The mod's compatibility with the latest beta version of the game (if any).</summary> - public WikiCompatibilityInfo BetaCompatibility { get; set; } + public WikiCompatibilityInfo? BetaCompatibility { get; } /// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="BetaCompatibility"/> should be used for beta versions of SMAPI instead of <see cref="Compatibility"/>.</summary> +#if NET5_0_OR_GREATER + [MemberNotNullWhen(true, nameof(WikiModEntry.BetaCompatibility))] +#endif public bool HasBetaInfo => this.BetaCompatibility != null; /// <summary>The human-readable warnings for players about this mod.</summary> - public string[] Warnings { get; set; } + public string[] Warnings { get; } /// <summary>The URL of the pull request which submits changes for an unofficial update to the author, if any.</summary> - public string PullRequestUrl { get; set; } + public string? PullRequestUrl { get; } - /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary> - public string DevNote { get; set; } + /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests.</summary> + public string? DevNote { get; } /// <summary>The data overrides to apply to the mod's manifest or remote mod page data, if any.</summary> - public WikiDataOverrideEntry Overrides { get; set; } + public WikiDataOverrideEntry? Overrides { get; } /// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary> - public string Anchor { get; set; } + public string? Anchor { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order.</param> + /// <param name="name">The mod's display name. If the mod has multiple names, the first one is the most canonical name.</param> + /// <param name="author">The mod's author name. If the author has multiple names, the first one is the most canonical name.</param> + /// <param name="nexusId">The mod ID on Nexus.</param> + /// <param name="chucklefishId">The mod ID in the Chucklefish mod repo.</param> + /// <param name="curseForgeId">The mod ID in the CurseForge mod repo.</param> + /// <param name="curseForgeKey">The mod ID in the CurseForge mod repo.</param> + /// <param name="modDropId">The mod ID in the ModDrop mod repo.</param> + /// <param name="githubRepo">The GitHub repository in the form 'owner/repo'.</param> + /// <param name="customSourceUrl">The URL to a non-GitHub source repo.</param> + /// <param name="customUrl">The custom mod page URL (if applicable).</param> + /// <param name="contentPackFor">The name of the mod which loads this content pack, if applicable.</param> + /// <param name="compatibility">The mod's compatibility with the latest stable version of the game.</param> + /// <param name="betaCompatibility">The mod's compatibility with the latest beta version of the game (if any).</param> + /// <param name="warnings">The human-readable warnings for players about this mod.</param> + /// <param name="pullRequestUrl">The URL of the pull request which submits changes for an unofficial update to the author, if any.</param> + /// <param name="devNote">Special notes intended for developers who maintain unofficial updates or submit pull requests.</param> + /// <param name="overrides">The data overrides to apply to the mod's manifest or remote mod page data, if any.</param> + /// <param name="anchor">The link anchor for the mod entry in the wiki compatibility list.</param> + public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, WikiCompatibilityInfo? betaCompatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) + { + this.ID = id; + this.Name = name; + this.Author = author; + this.NexusID = nexusId; + this.ChucklefishID = chucklefishId; + this.CurseForgeID = curseForgeId; + this.CurseForgeKey = curseForgeKey; + this.ModDropID = modDropId; + this.GitHubRepo = githubRepo; + this.CustomSourceUrl = customSourceUrl; + this.CustomUrl = customUrl; + this.ContentPackFor = contentPackFor; + this.Compatibility = compatibility; + this.BetaCompatibility = betaCompatibility; + this.Warnings = warnings; + this.PullRequestUrl = pullRequestUrl; + this.DevNote = devNote; + this.Overrides = overrides; + this.Anchor = anchor; + } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs index 0d614f28..24548078 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs @@ -7,12 +7,27 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki ** Accessors *********/ /// <summary>The stable game version.</summary> - public string StableVersion { get; set; } + public string? StableVersion { get; } /// <summary>The beta game version (if any).</summary> - public string BetaVersion { get; set; } + public string? BetaVersion { get; } /// <summary>The mods on the wiki.</summary> - public WikiModEntry[] Mods { get; set; } + public WikiModEntry[] Mods { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="stableVersion">The stable game version.</param> + /// <param name="betaVersion">The beta game version (if any).</param> + /// <param name="mods">The mods on the wiki.</param> + public WikiModList(string? stableVersion, string? betaVersion, WikiModEntry[] mods) + { + this.StableVersion = stableVersion; + this.BetaVersion = betaVersion; + this.Mods = mods; + } } } diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 8d4198de..8e1538a5 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Xml.Linq; @@ -13,6 +14,7 @@ using Microsoft.Win32; namespace StardewModdingAPI.Toolkit.Framework.GameScanning { /// <summary>Finds installed game folders.</summary> + [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are valid game install paths.")] public class GameScanner { /********* @@ -39,13 +41,13 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning IEnumerable<string> paths = this .GetCustomInstallPaths() .Concat(this.GetDefaultInstallPaths()) - .Select(PathUtilities.NormalizePath) + .Select(path => PathUtilities.NormalizePath(path)) .Distinct(StringComparer.OrdinalIgnoreCase); // yield valid folders foreach (string path in paths) { - DirectoryInfo folder = new DirectoryInfo(path); + DirectoryInfo folder = new(path); if (this.LooksLikeGameFolder(folder)) yield return folder; } @@ -78,10 +80,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning return GameFolderType.NoGameFound; // get assembly version - Version version; + Version? version; try { version = AssemblyName.GetAssemblyName(executable.FullName).Version; + if (version == null) + return GameFolderType.InvalidUnknown; } catch { @@ -121,7 +125,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning case Platform.Linux: case Platform.Mac: { - string home = Environment.GetEnvironmentVariable("HOME"); + string home = Environment.GetEnvironmentVariable("HOME")!; // Linux yield return $"{home}/GOG Games/Stardew Valley/game"; @@ -146,13 +150,13 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning }; foreach (var pair in registryKeys) { - string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); + string? path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); if (!string.IsNullOrWhiteSpace(path)) yield return path; } // via Steam library path - string steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); + string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); if (steamPath != null) yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); #endif @@ -186,12 +190,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning private IEnumerable<string> GetCustomInstallPaths() { // get home path - string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME"); + string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME")!; if (string.IsNullOrWhiteSpace(homePath)) yield break; // get targets file - FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets")); + FileInfo file = new(Path.Combine(homePath, "stardewvalley.targets")); if (!file.Exists) yield break; @@ -208,7 +212,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning } // get install path - XElement element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace + XElement? element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace if (!string.IsNullOrWhiteSpace(element?.Value)) yield return element.Value.Trim(); } @@ -217,27 +221,27 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// <summary>Get the value of a key in the Windows HKLM registry.</summary> /// <param name="key">The full path of the registry key relative to HKLM.</param> /// <param name="name">The name of the value.</param> - private string GetLocalMachineRegistryValue(string key, string name) + private string? GetLocalMachineRegistryValue(string key, string name) { RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; - RegistryKey openKey = localMachine.OpenSubKey(key); + RegistryKey? openKey = localMachine.OpenSubKey(key); if (openKey == null) return null; using (openKey) - return (string)openKey.GetValue(name); + return (string?)openKey.GetValue(name); } /// <summary>Get the value of a key in the Windows HKCU registry.</summary> /// <param name="key">The full path of the registry key relative to HKCU.</param> /// <param name="name">The name of the value.</param> - private string GetCurrentUserRegistryValue(string key, string name) + private string? GetCurrentUserRegistryValue(string key, string name) { - RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; - RegistryKey openKey = currentuser.OpenSubKey(key); + RegistryKey currentUser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; + RegistryKey? openKey = currentUser.OpenSubKey(key); if (openKey == null) return null; using (openKey) - return (string)openKey.GetValue(name); + return (string?)openKey.GetValue(name); } #endif } diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index 8b6eb5fb..6978567e 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -57,11 +57,13 @@ namespace StardewModdingAPI.Toolkit.Framework #if SMAPI_FOR_WINDOWS try { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + string? result = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") .Get() .Cast<ManagementObject>() .Select(entry => entry.GetPropertyValue("Caption").ToString()) .FirstOrDefault(); + + return result ?? "Windows"; } catch { } #endif @@ -98,7 +100,7 @@ namespace StardewModdingAPI.Toolkit.Framework /// </remarks> private static bool IsRunningAndroid() { - using Process process = new Process + using Process process = new() { StartInfo = { @@ -135,7 +137,7 @@ namespace StardewModdingAPI.Toolkit.Framework buffer = Marshal.AllocHGlobal(8192); if (LowLevelEnvironmentUtility.uname(buffer) == 0) { - string os = Marshal.PtrToStringAnsi(buffer); + string? os = Marshal.PtrToStringAnsi(buffer); return os == "Darwin"; } return false; diff --git a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs index ef6d4dd9..da678ac9 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs @@ -9,6 +9,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Accessors ********/ /// <summary>Extra metadata about mods.</summary> - public IDictionary<string, ModDataModel> ModData { get; set; } + public IDictionary<string, ModDataModel> ModData { get; } = new Dictionary<string, ModDataModel>(); } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs index b02be3e4..9674d283 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -18,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData public bool IsDefault { get; } /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> - public ISemanticVersion LowerVersion { get; } + public ISemanticVersion? LowerVersion { get; } /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> - public ISemanticVersion UpperVersion { get; } + public ISemanticVersion? UpperVersion { get; } /********* @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <param name="isDefault">Whether this field should only be applied if it's not already set.</param> /// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param> /// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param> - public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion? lowerVersion, ISemanticVersion? upperVersion) { this.Key = key; this.Value = value; @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get whether this data field applies for the given manifest.</summary> /// <param name="manifest">The mod manifest.</param> - public bool IsMatch(IManifest manifest) + public bool IsMatch(IManifest? manifest) { return manifest?.Version != null // ignore invalid manifest diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index 2167d3e5..5912fb87 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Accessors *********/ /// <summary>The mod's current unique ID.</summary> - public string ID { get; set; } + public string ID { get; } /// <summary>The former mod IDs (if any).</summary> /// <remarks> @@ -23,14 +23,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// ID, if any. If the mod's ID changed over time, multiple variants can be separated by the /// <c>|</c> character. /// </remarks> - public string FormerIDs { get; set; } + public string? FormerIDs { get; } /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> - public ModWarning SuppressWarnings { get; set; } + public ModWarning SuppressWarnings { get; } /// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary> [JsonExtensionData] - public IDictionary<string, JToken> ExtensionData { get; set; } + public IDictionary<string, JToken> ExtensionData { get; } = new Dictionary<string, JToken>(); /// <summary>The versioned field data.</summary> /// <remarks> @@ -50,6 +50,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /********* ** Public methods *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The mod's current unique ID.</param> + /// <param name="formerIds">The former mod IDs (if any).</param> + /// <param name="suppressWarnings">The mod warnings to suppress, even if they'd normally be shown.</param> + public ModDataModel(string id, string? formerIds, ModWarning suppressWarnings) + { + this.ID = id; + this.FormerIDs = formerIds; + this.SuppressWarnings = suppressWarnings; + } + /// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary> public IEnumerable<ModDataField> GetFields() { @@ -59,8 +70,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData string packedKey = pair.Key; string value = pair.Value; bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; + ISemanticVersion? lowerVersion = null; + ISemanticVersion? upperVersion = null; // parse string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); @@ -111,11 +122,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData [OnDeserialized] private void OnDeserialized(StreamingContext context) { - if (this.ExtensionData != null) - { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; - } + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData.Clear(); } } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 5dd32acf..ab0e4377 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData public string[] FormerIDs { get; } /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> - public ModWarning SuppressWarnings { get; set; } + public ModWarning SuppressWarnings { get; } /// <summary>The versioned field data.</summary> public ModDataField[] Fields { get; } @@ -70,9 +70,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData } /// <summary>Get the default update key for this mod, if any.</summary> - public string GetDefaultUpdateKey() + public string? GetDefaultUpdateKey() { - string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + string? updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; return !string.IsNullOrWhiteSpace(updateKey) ? updateKey : null; @@ -80,9 +80,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get a parsed representation of the <see cref="ModDataRecord.Fields"/> which match a given manifest.</summary> /// <param name="manifest">The manifest to match.</param> - public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) + public ModDataRecordVersionedFields GetVersionedFields(IManifest? manifest) { - ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; + ModDataRecordVersionedFields parsed = new(this); foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) { switch (field.Key) diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 5aaabd51..65fa424e 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -7,24 +7,32 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Accessors *********/ /// <summary>The underlying data record.</summary> - public ModDataRecord DataRecord { get; set; } + public ModDataRecord DataRecord { get; } - /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary> - public string DisplayName { get; set; } - - /// <summary>The update key to apply.</summary> - public string UpdateKey { get; set; } + /// <summary>The update key to apply (if any).</summary> + public string? UpdateKey { get; set; } /// <summary>The predefined compatibility status.</summary> public ModStatus Status { get; set; } = ModStatus.None; /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary> - public string StatusReasonPhrase { get; set; } + public string? StatusReasonPhrase { get; set; } /// <summary>Technical details shown in TRACE logs for the <see cref="Status"/>, or <c>null</c> to omit it.</summary> - public string StatusReasonDetails { get; set; } + public string? StatusReasonDetails { get; set; } /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> - public ISemanticVersion StatusUpperVersion { get; set; } + public ISemanticVersion? StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="dataRecord">The underlying data record.</param> + public ModDataRecordVersionedFields(ModDataRecord dataRecord) + { + this.DataRecord = dataRecord; + } } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs index a9da884a..168b8aac 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData private readonly ModDataRecord[] Records; /// <summary>Get an update URL for an update key (if valid).</summary> - private readonly Func<string, string> GetUpdateUrl; + private readonly Func<string, string?> GetUpdateUrl; /********* @@ -22,12 +22,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData *********/ /// <summary>Construct an empty instance.</summary> public ModDatabase() - : this(new ModDataRecord[0], key => null) { } + : this(Array.Empty<ModDataRecord>(), _ => null) { } /// <summary>Construct an instance.</summary> /// <param name="records">The underlying mod data records indexed by default display name.</param> /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> - public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string> getUpdateUrl) + public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string?> getUpdateUrl) { this.Records = records.ToArray(); this.GetUpdateUrl = getUpdateUrl; @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get a mod data record.</summary> /// <param name="modID">The unique mod ID.</param> - public ModDataRecord Get(string modID) + public ModDataRecord? Get(string? modID) { return !string.IsNullOrWhiteSpace(modID) ? this.Records.FirstOrDefault(p => p.HasID(modID)) @@ -50,11 +50,11 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get the mod page URL for a mod (if available).</summary> /// <param name="id">The unique mod ID.</param> - public string GetModPageUrlFor(string id) + public string? GetModPageUrlFor(string? id) { // get update key - ModDataRecord record = this.Get(id); - ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + ModDataRecord? record = this.Get(id); + ModDataField? updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); if (updateKeyField == null) return null; diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 825b98e5..da2a3c85 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -22,13 +22,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning public ModType Type { get; } /// <summary>The mod manifest.</summary> - public Manifest Manifest { get; } + public Manifest? Manifest { get; } /// <summary>The error which occurred parsing the manifest, if any.</summary> public ModParseError ManifestParseError { get; set; } /// <summary>A human-readable message for the <see cref="ManifestParseError"/>, if any.</summary> - public string ManifestParseErrorText { get; set; } + public string? ManifestParseErrorText { get; set; } /********* @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <param name="manifest">The mod manifest.</param> /// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param> /// <param name="manifestParseErrorText">A human-readable message for the <paramref name="manifestParseError"/>, if any.</param> - public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, ModParseError manifestParseError, string manifestParseErrorText) + public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest? manifest, ModParseError manifestParseError, string? manifestParseErrorText) { // save info this.Directory = directory; @@ -59,9 +59,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning this.ManifestParseErrorText = manifestParseErrorText; // set display name - this.DisplayName = manifest?.Name; - if (string.IsNullOrWhiteSpace(this.DisplayName)) - this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); + this.DisplayName = !string.IsNullOrWhiteSpace(manifest?.Name) + ? manifest.Name + : PathUtilities.GetRelativePath(root.FullName, directory.FullName); } /// <summary>Get the update keys for a mod.</summary> diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index e6105f9c..24485620 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Serialization.Models; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.ModScanning { @@ -18,7 +19,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning private readonly JsonHelper JsonHelper; /// <summary>A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod.</summary> - private readonly HashSet<Regex> IgnoreFilesystemNames = new HashSet<Regex> + private readonly HashSet<Regex> IgnoreFilesystemNames = new() { new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // macOS @@ -26,7 +27,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// <summary>A list of file extensions to ignore when searching for mod files.</summary> - private readonly HashSet<string> IgnoreFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private readonly HashSet<string> IgnoreFileExtensions = new(StringComparer.OrdinalIgnoreCase) { // text ".doc", @@ -60,7 +61,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// <summary>The extensions for packed content files.</summary> - private readonly HashSet<string> StrictXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private readonly HashSet<string> StrictXnbModExtensions = new(StringComparer.OrdinalIgnoreCase) { ".xgs", ".xnb", @@ -69,7 +70,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning }; /// <summary>The extensions for files which an XNB mod may contain, in addition to <see cref="StrictXnbModExtensions"/>.</summary> - private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private readonly HashSet<string> PotentialXnbModExtensions = new(StringComparer.OrdinalIgnoreCase) { ".json", ".yaml" @@ -96,7 +97,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <param name="rootPath">The root folder containing mods.</param> public IEnumerable<ModFolder> GetModFolders(string rootPath) { - DirectoryInfo root = new DirectoryInfo(rootPath); + DirectoryInfo root = new(rootPath); return this.GetModFolders(root, root); } @@ -115,7 +116,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) { // find manifest.json - FileInfo manifestFile = this.FindManifest(searchFolder); + FileInfo? manifestFile = this.FindManifest(searchFolder); // set appropriate invalid-mod error if (manifestFile == null) @@ -137,7 +138,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); // SMAPI installer - if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on macOS.command" || p.Name == "install on Windows.bat")) + if (relevantFiles.Any(p => p.Name is "install on Linux.sh" or "install on macOS.command" or "install on Windows.bat")) return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file)."); // not a mod? @@ -145,13 +146,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } // read mod info - Manifest manifest = null; + Manifest? manifest = null; ModParseError error = ModParseError.None; - string errorText = null; + string? errorText = null; { try { - if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null) + if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest)) { error = ModParseError.ManifestInvalid; errorText = "its manifest is invalid."; @@ -169,14 +170,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - // normalize display fields - if (manifest != null) - { - manifest.Name = this.StripNewlines(manifest.Name); - manifest.Description = this.StripNewlines(manifest.Description); - manifest.Author = this.StripNewlines(manifest.Author); - } - // get mod type ModType type; { @@ -192,7 +185,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } // build result - return new ModFolder(root, manifestFile.Directory, type, manifest, error, errorText); + return new ModFolder(root, manifestFile.Directory!, type, manifest, error, errorText); } @@ -255,26 +248,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <summary>Find the manifest for a mod folder.</summary> /// <param name="folder">The folder to search.</param> - private FileInfo FindManifest(DirectoryInfo folder) + private FileInfo? FindManifest(DirectoryInfo folder) { - while (true) - { - // check for manifest in current folder - FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json")); - if (file.Exists) - return file; - - // check for single subfolder - FileSystemInfo[] entries = folder.EnumerateFileSystemInfos().Take(2).ToArray(); - if (entries.Length == 1 && entries[0] is DirectoryInfo subfolder) - { - folder = subfolder; - continue; - } + // check for conventional manifest in current folder + const string defaultName = "manifest.json"; + FileInfo file = new(Path.Combine(folder.FullName, defaultName)); + if (file.Exists) + return file; - // not found - return null; + // check for manifest with incorrect capitalization + { + CaseInsensitivePathLookup pathLookup = new(folder.FullName, SearchOption.TopDirectoryOnly); // don't use GetCachedFor, since we only need it temporarily + string realName = pathLookup.GetFilePath(defaultName); + if (realName != defaultName) + file = new(Path.Combine(folder.FullName, realName)); } + if (file.Exists) + return file; + + // not found + return null; } /// <summary>Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods).</summary> @@ -314,8 +307,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// <param name="entry">The file or folder.</param> private bool IsRelevant(FileSystemInfo entry) { - // ignored file extension - if (entry is FileInfo file && this.IgnoreFileExtensions.Contains(file.Extension)) + // ignored file extensions and any files starting with "." + if ((entry is FileInfo file) && (this.IgnoreFileExtensions.Contains(file.Extension) || file.Name.StartsWith("."))) return false; // ignored entry name @@ -363,12 +356,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return hasVortexMarker; } - - /// <summary>Strip newlines from a string.</summary> - /// <param name="input">The input to strip.</param> - private string StripNewlines(string input) - { - return input?.Replace("\r", "").Replace("\n", ""); - } } } diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs index 489e1c4d..939be771 100644 --- a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace StardewModdingAPI.Toolkit.Framework { /// <summary>Reads strings into a semantic version.</summary> @@ -16,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework /// <param name="prereleaseTag">An optional prerelease tag.</param> /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param> /// <returns>Returns whether the version was successfully parsed.</returns> - public static bool TryParse(string versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) + public static bool TryParse(string? versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string? prereleaseTag, out string? buildMetadata) { // init major = 0; @@ -103,7 +105,12 @@ namespace StardewModdingAPI.Toolkit.Framework /// <param name="raw">The raw characters to parse.</param> /// <param name="index">The index of the next character to read.</param> /// <param name="tag">The parsed tag.</param> - private static bool TryParseTag(char[] raw, ref int index, out string tag) + private static bool TryParseTag(char[] raw, ref int index, +#if NET5_0_OR_GREATER + [NotNullWhen(true)] +#endif + out string? tag + ) { // read tag length int length = 0; diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 077c0361..960caf96 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Toolkit.Framework.UpdateData { @@ -15,12 +16,15 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public ModSiteKey Site { get; } /// <summary>The mod ID within the repository.</summary> - public string ID { get; } + public string? ID { get; } /// <summary>If specified, a substring in download names/descriptions to match.</summary> - public string Subkey { get; } + public string? Subkey { get; } /// <summary>Whether the update key seems to be valid.</summary> +#if NET5_0_OR_GREATER + [MemberNotNullWhen(true, nameof(UpdateKey.ID))] +#endif public bool LooksValid { get; } @@ -32,9 +36,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="site">The mod site containing the mod.</param> /// <param name="id">The mod ID within the site.</param> /// <param name="subkey">If specified, a substring in download names/descriptions to match.</param> - public UpdateKey(string rawText, ModSiteKey site, string id, string subkey) + public UpdateKey(string? rawText, ModSiteKey site, string? id, string? subkey) { - this.RawText = rawText?.Trim(); + this.RawText = rawText?.Trim() ?? string.Empty; this.Site = site; this.ID = id?.Trim(); this.Subkey = subkey?.Trim(); @@ -47,19 +51,19 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="site">The mod site containing the mod.</param> /// <param name="id">The mod ID within the site.</param> /// <param name="subkey">If specified, a substring in download names/descriptions to match.</param> - public UpdateKey(ModSiteKey site, string id, string subkey) + public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } /// <summary>Parse a raw update key.</summary> /// <param name="raw">The raw update key to parse.</param> - public static UpdateKey Parse(string raw) + public static UpdateKey Parse(string? raw) { // extract site + ID - string rawSite; - string id; + string? rawSite; + string? id; { - string[] parts = raw?.Trim().Split(':'); - if (parts == null || parts.Length != 2) + string[]? parts = raw?.Trim().Split(':'); + if (parts?.Length != 2) return new UpdateKey(raw, ModSiteKey.Unknown, null, null); rawSite = parts[0].Trim(); @@ -69,7 +73,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData id = null; // extract subkey - string subkey = null; + string? subkey = null; if (id != null) { string[] parts = id.Split('@'); @@ -109,7 +113,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <summary>Indicates whether the current object is equal to another object of the same type.</summary> /// <param name="other">An object to compare with this object.</param> - public bool Equals(UpdateKey other) + public bool Equals(UpdateKey? other) { if (!this.LooksValid) { @@ -127,7 +131,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <summary>Determines whether the specified object is equal to the current object.</summary> /// <param name="obj">The object to compare with the current object.</param> - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is UpdateKey other && this.Equals(other); } @@ -143,7 +147,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="site">The mod site containing the mod.</param> /// <param name="id">The mod ID within the repository.</param> /// <param name="subkey">If specified, a substring in download names/descriptions to match.</param> - public static string GetString(ModSiteKey site, string id, string subkey = null) + public static string GetString(ModSiteKey site, string? id, string? subkey = null) { return $"{site}:{id}{subkey}".Trim(); } |