From 6d4eed56b1d80d02db773b0cd2f372baec6b2d1b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 22 Sep 2022 23:03:26 -0400 Subject: [PATCH 01/84] refactor UpdateKey parsing, move responsibility for subkey matching UpdateKey parsing now allows multiple : and @ inside the update key, splitting on the first occurence of each Subkey matching is moved into IModDownload / GenericModDownload, in preparation for some Mod Sites using something less error-prone than substring matching. --- .../Framework/UpdateData/UpdateKey.cs | 45 +++++++++++-------- .../Framework/Clients/GenericModDownload.cs | 14 ++++++ src/SMAPI.Web/Framework/IModDownload.cs | 5 +++ src/SMAPI.Web/Framework/ModSiteManager.cs | 12 ++--- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 960caf96e..7c2cc73cc 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -54,35 +54,44 @@ public UpdateKey(string? rawText, ModSiteKey site, string? id, string? subkey) public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } + /// + /// Split a string into two at a delimiter. If the delimiter does not appear in the string then the second + /// value of the returned tuple is null. Both returned strings are trimmed of whitespace. + /// + /// The string to split. + /// The character on which to split. + /// + /// If true then the second string returned will include the delimiter character + /// (provided that the string is not null) + /// + /// + /// A pair containing the string consisting of all characters in before the first + /// occurrence of , and a string consisting of all characters in + /// after the first occurrence of or null if the delimiter does not + /// exist in s. Both strings are trimmed of whitespace. + /// + private static (string, string?) Bifurcate(string s, char delimiter, bool keepDelimiter = false) { + int pos = s.IndexOf(delimiter); + if (pos < 0) + return (s.Trim(), null); + return (s.Substring(0, pos).Trim(), s.Substring(pos + (keepDelimiter ? 0 : 1)).Trim()); + } + /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string? raw) { + if (raw is null) + return new UpdateKey(raw, ModSiteKey.Unknown, null, null); // extract site + ID - string? rawSite; - string? id; - { - string[]? parts = raw?.Trim().Split(':'); - if (parts?.Length != 2) - return new UpdateKey(raw, ModSiteKey.Unknown, null, null); - - rawSite = parts[0].Trim(); - id = parts[1].Trim(); - } + (string rawSite, string? id) = Bifurcate(raw, ':'); if (string.IsNullOrWhiteSpace(id)) id = null; // extract subkey string? subkey = null; if (id != null) - { - string[] parts = id.Split('@'); - if (parts.Length == 2) - { - id = parts[0].Trim(); - subkey = $"@{parts[1]}".Trim(); - } - } + (id, subkey) = Bifurcate(id, '@', true); // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 548f17c39..b37e5cdae 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Web.Framework.Clients { /// Generic metadata about a file download on a mod page. @@ -29,5 +31,17 @@ public GenericModDownload(string name, string? description, string? version) this.Description = description; this.Version = version; } + + /// + /// Return true if the subkey matches this download. A subkey matches if it appears as + /// a substring in the name or description. + /// + /// the subkey + /// true if matches this download, otherwise false + /// + public bool MatchesSubkey(string subkey) { + return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true + || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; + } } } diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index fe1717858..13739f8f0 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -14,5 +14,10 @@ internal interface IModDownload /// The download's file version. string? Version { get; } + + /// Return true iff the subkey matches this download + /// the subkey + /// true if matches this download, otherwise false + bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 674b9ffc9..70070d631 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -119,7 +119,7 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt preview = null; // parse all versions from the mod page - IEnumerable<(string? name, string? description, ISemanticVersion? version)> GetAllVersions() + IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() { if (mod != null) { @@ -132,14 +132,14 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt // get mod version ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); if (modVersion != null) - yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version)); + yield return (download: null, version: ParseAndMapVersion(mod.Version)); // get file versions foreach (IModDownload download in mod.Downloads) { ISemanticVersion? cur = ParseAndMapVersion(download.Version); if (cur != null) - yield return (download.Name, download.Description, cur); + yield return (download, cur); } } } @@ -148,13 +148,13 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(string? name, string? description, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; // get latest main + preview version - foreach ((string? name, string? description, ISemanticVersion? version) entry in versions) + foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) { if (entry.version is null || filter?.Invoke(entry) == false) continue; @@ -178,7 +178,7 @@ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out I } if (subkey is not null) - TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true); + TryGetVersions(out main, out preview, entry => entry.download?.MatchesSubkey(subkey) == true); if (main is null) TryGetVersions(out main, out preview); From 83aec980b3ee739fb4bc251217556b3ae44f741b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 1 Oct 2022 21:09:39 -0400 Subject: [PATCH 02/84] Add UpdateManifest site type. Adds the UpdateManifest site key and associated client. This required some additional features in the existing update machinery. Each "version" can now (optionally) have its own download URL. Mod Page objects can now specify that subkey matching (for that page) should be "strict". A strict subkey match does not fall back to matching with no subkey if a subkey was provided but produced no versions. It also strips the leading '@' from the subkey. IModDownload objects are now responsible for deciding whether a subkey matches or not. The default behavior is unchanged, but this allows different mod sites to have different rules for subkey matching (which the UpdateManifest mod site uses to force exact matches). --- .../Framework/UpdateData/ModSiteKey.cs | 5 +- .../Controllers/ModsApiController.cs | 14 ++-- .../Framework/Clients/GenericModDownload.cs | 15 +++-- .../Framework/Clients/GenericModPage.cs | 16 +++++ .../UpdateManifest/IUpdateManifestClient.cs | 9 +++ .../TextAsJsonMediaTypeFormatter.cs | 15 +++++ .../UpdateManifest/UpdateManifestClient.cs | 55 ++++++++++++++++ .../UpdateManifestModDownload.cs | 27 ++++++++ .../UpdateManifest/UpdateManifestModModel.cs | 26 ++++++++ .../UpdateManifest/UpdateManifestModPage.cs | 58 +++++++++++++++++ .../UpdateManifest/UpdateManifestModel.cs | 23 +++++++ .../UpdateManifestVersionModel.cs | 26 ++++++++ src/SMAPI.Web/Framework/IModDownload.cs | 7 +- src/SMAPI.Web/Framework/IModPage.cs | 17 +++++ src/SMAPI.Web/Framework/ModInfoModel.cs | 11 +++- src/SMAPI.Web/Framework/ModSiteManager.cs | 64 ++++++++++++------- src/SMAPI.Web/SMAPI.Web.csproj | 4 ++ src/SMAPI.Web/Startup.cs | 3 + 18 files changed, 359 insertions(+), 36 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs index 47cd3f7e4..d1dd90497 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -19,6 +19,9 @@ public enum ModSiteKey ModDrop, /// The Nexus Mods mod repository. - Nexus + Nexus, + + /// An arbitrary URL for an update manifest file. + UpdateManifest } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 71fb42c24..1c34f2afc 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -22,6 +22,7 @@ using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest; using StardewModdingAPI.Web.Framework.ConfigModels; namespace StardewModdingAPI.Web.Controllers @@ -63,14 +64,15 @@ internal class ModsApiController : Controller /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + /// The UpdateManifest client. + public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); this.WikiCache = wikiCache; this.ModCache = modCache; this.Config = config; - this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus }); + this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus, updateManifest }); } /// Fetch version metadata for the given mods. @@ -161,18 +163,22 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod // if there's only a prerelease version (e.g. from GitHub), don't override the main version ISemanticVersion? curMain = data.Version; + string? curMainUrl = data.MainVersionUrl; ISemanticVersion? curPreview = data.PreviewVersion; + string? curPreviewUrl = data.PreviewVersionUrl; if (curPreview == null && curMain?.IsPrerelease() == true) { curPreview = curMain; + curPreviewUrl = curMainUrl; curMain = null; + curMainUrl = null; } // handle versions if (this.IsNewer(curMain, main?.Version)) - main = new ModEntryVersionModel(curMain, data.Url!); + main = new ModEntryVersionModel(curMain, curMainUrl ?? data.Url!); if (this.IsNewer(curPreview, optional?.Version)) - optional = new ModEntryVersionModel(curPreview, data.Url!); + optional = new ModEntryVersionModel(curPreview, curPreviewUrl ?? data.Url!); } // get unofficial version diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index b37e5cdae..5cc03aba9 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -17,6 +17,10 @@ internal class GenericModDownload : IModDownload /// The download's file version. public string? Version { get; } + /// + /// The URL for this download, if it has one distinct from the mod page's URL. + /// + public string? Url { get; } /********* ** Public methods @@ -25,21 +29,22 @@ internal class GenericModDownload : IModDownload /// The download's display name. /// The download's description. /// The download's file version. - public GenericModDownload(string name, string? description, string? version) + /// The download's URL (if different from the mod page's URL). + public GenericModDownload(string name, string? description, string? version, string? url = null) { this.Name = name; this.Description = description; this.Version = version; + this.Url = url; } /// - /// Return true if the subkey matches this download. A subkey matches if it appears as + /// Return if the subkey matches this download. A subkey matches if it appears as /// a substring in the name or description. /// /// the subkey - /// true if matches this download, otherwise false - /// - public bool MatchesSubkey(string subkey) { + /// if matches this download, otherwise + public virtual bool MatchesSubkey(string subkey) { return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 5353c7e1d..4c66e1a0c 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,6 +40,8 @@ internal class GenericModPage : IModPage [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; + /// Whether to use strict subkey matching or not. + public bool IsSubkeyStrict { get; set; } = false; /********* ** Public methods @@ -79,5 +81,19 @@ public IModPage SetError(RemoteModStatus status, string error) return this; } + + /// Returns the mod page name. + /// ignored + /// The mod page name. + public virtual string? GetName(string? subkey) { + return this.Name; + } + + /// Returns the mod page URL. + /// ignored + /// The mod page URL. + public virtual string? GetUrl(string? subkey) { + return this.Url; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs new file mode 100644 index 000000000..36d018c76 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -0,0 +1,9 @@ +// Copyright 2022 Jamie Taylor +using System; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// An HTTP client for fetching an update manifest from an arbitrary URL. + internal interface IUpdateManifestClient : IModSiteClient, IDisposable { } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs new file mode 100644 index 000000000..48e3c2948 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs @@ -0,0 +1,15 @@ +// Copyright 2022 Jamie Taylor +using System.Net.Http.Formatting; +using System.Net.Http.Headers; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// + /// A that can parse from content of type text/plain. + /// + internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter { + /// Construct a new + public TextAsJsonMediaTypeFormatter() { + this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs new file mode 100644 index 000000000..d7cf49455 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -0,0 +1,55 @@ +// Copyright 2022 Jamie Taylor +using System; +using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using System.Threading.Tasks; +using System.Net; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// An HTTP client for fetching an update manifest from an arbitrary URL. + internal class UpdateManifestClient : IUpdateManifestClient { + /********* + ** Fields + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.UpdateManifest; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the API client. + public UpdateManifestClient(string userAgent) { + this.Client = new FluentClient() + .SetUserAgent(userAgent); + this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter()); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() { + this.Client.Dispose(); + } + + /// + public async Task GetModData(string id) { + UpdateManifestModel? manifest; + try { + manifest = await this.Client.GetAsync(id).As(); + } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); + } + if (manifest is null) { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"Error parsing manifest at {id}"); + } + + return new UpdateManifestModPage(id, manifest); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs new file mode 100644 index 000000000..117ae15c6 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -0,0 +1,27 @@ +// Copyright 2022 Jamie Taylor +using System; +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Metadata about a mod download in an update manifest file. + internal class UpdateManifestModDownload : GenericModDownload { + /// The subkey for this mod download + private readonly string subkey; + /// Construct an instance. + /// The subkey for this download. + /// The mod name for this download. + /// The download's version. + /// The download's URL. + public UpdateManifestModDownload(string subkey, string name, string? version, string? url) : base(name, null, version, url) { + this.subkey = subkey; + } + + /// + /// Returns iff the given subkey is the same as the subkey for this download. + /// + /// The subkey to match + /// if is the same as the subkey for this download, otherwise. + public override bool MatchesSubkey(string subkey) { + return this.subkey == subkey; + } + } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs new file mode 100644 index 000000000..4ec9c03dc --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs @@ -0,0 +1,26 @@ +// Copyright 2022 Jamie Taylor +using System; +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Data model for a mod in an update manifest. + internal class UpdateManifestModModel { + /// The mod's name. + public string Name { get; } + + /// The mod's URL. + public string? Url { get; } + + /// The versions for this mod. + public UpdateManifestVersionModel[] Versions { get; } + + /// Construct an instance. + /// The mod's name. + /// The mod's URL. + /// The versions for this mod. + public UpdateManifestModModel(string name, string? url, UpdateManifestVersionModel[] versions) { + this.Name = name; + this.Url = url; + this.Versions = versions; + } + } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs new file mode 100644 index 000000000..109175b55 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -0,0 +1,58 @@ +// Copyright 2022 Jamie Taylor +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Metadata about an update manifest "page". + internal class UpdateManifestModPage : GenericModPage { + /// The update manifest model. + private UpdateManifestModel manifest; + + /// Constuct an instance. + /// The "id" (i.e., URL) of this update manifest. + /// The manifest object model. + public UpdateManifestModPage(string id, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, id) { + this.IsSubkeyStrict = true; + this.manifest = manifest; + this.SetInfo(name: id, url: id, version: null, downloads: TranslateDownloads(manifest).ToArray()); + } + + /// Return the mod name for the given subkey, if it exists in this update manifest. + /// The subkey. + /// The mod name for the given subkey, or if this manifest does not contain the given subkey. + public override string? GetName(string? subkey) { + if (subkey is null) + return null; + this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); + return modModel?.Name; + } + + /// Return the mod URL for the given subkey, if it exists in this update manifest. + /// The subkey. + /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. + public override string? GetUrl(string? subkey) { + if (subkey is null) + return null; + this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); + return modModel?.Url; + } + + + /********* + ** Private methods + *********/ + /// Translate the downloads from the manifest's object model into objects. + /// The manifest object model. + /// An for each in the manifest. + private static IEnumerable TranslateDownloads(UpdateManifestModel manifest) { + foreach (var entry in manifest.Subkeys) { + foreach (var version in entry.Value.Versions) { + yield return new UpdateManifestModDownload(entry.Key, entry.Value.Name, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); + } + } + } + + } +} \ No newline at end of file diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs new file mode 100644 index 000000000..03f89726e --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs @@ -0,0 +1,23 @@ +// Copyright 2022 Jamie Taylor +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Data model for an update manifest. + internal class UpdateManifestModel { + /// The manifest format version. + public string ManifestVersion { get; } + + /// The subkeys in this update manifest. + public IDictionary Subkeys { get; } + + /// Construct an instance. + /// The manifest format version. + /// The subkeys in this update manifest. + public UpdateManifestModel(string manifestVersion, IDictionary subkeys) { + this.ManifestVersion = manifestVersion; + this.Subkeys = subkeys; + } + } +} + diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs new file mode 100644 index 000000000..55b6db617 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs @@ -0,0 +1,26 @@ +// Copyright 2022 Jamie Taylor +using System; +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { + /// Data model for a Version in an update manifest. + internal class UpdateManifestVersionModel { + /// The semantic version string. + public string Version { get; } + + /// The URL for this version's download page (if any). + public string? DownloadPageUrl { get; } + + /// The URL for this version's direct file download (if any). + public string? DownloadFileUrl { get; } + + /// Construct an instance. + /// The semantic version string. + /// This version's download page URL (if any). + /// This version's direct file download URL (if any). + public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) { + this.Version = version; + this.DownloadPageUrl = downloadPageUrl; + this.DownloadFileUrl = downloadFileUrl; + } + } +} + diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index 13739f8f0..b0e9a664b 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -15,9 +15,12 @@ internal interface IModDownload /// The download's file version. string? Version { get; } - /// Return true iff the subkey matches this download + /// This download's URL (if it has a URL that is different from the containing mod page's URL). + string? Url { get; } + + /// Return iff the subkey matches this download /// the subkey - /// true if matches this download, otherwise false + /// if matches this download, otherwise bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 4d0a8d615..3fc8bb811 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,10 +39,27 @@ internal interface IModPage [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } + /// + /// Does this page use strict subkey matching. Pages that use string subkey matching do not fall back + /// to searching for versions without a subkey if there are no versions found when given a subkey. + /// Additionally, the leading @ is stripped from the subkey value before searching for matches. + /// + bool IsSubkeyStrict { get; } /********* ** Methods *********/ + + /// Get the mod name associated with the given subkey, if any. + /// The subkey. + /// The mod name associated with the given subkey (if any) + string? GetName(string? subkey); + + /// Get the URL for the mod associated with the given subkey, if any. + /// The subkey. + /// The URL for the mod associated with the given subkey (if any) + string? GetUrl(string? subkey); + /// Set the fetched mod info. /// The mod name. /// The mod's semantic version number. diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index e70b60bfe..174155890 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -27,6 +27,11 @@ internal class ModInfoModel /// The error message indicating why the mod is invalid (if applicable). public string? Error { get; private set; } + /// The URL associated with the mod's latest version (if distinct from the mod page's URL). + public string? MainVersionUrl { get; private set; } + + /// The URL associated with the mod's (if distinct from the mod page's URL). + public string? PreviewVersionUrl { get; private set; } /********* ** Public methods @@ -65,11 +70,15 @@ public ModInfoModel SetBasicInfo(string name, string url) /// Set the mod version info. /// The semantic version for the mod's latest release. /// The semantic version for the mod's latest preview release, if available and different from . + /// The URL associated with , if different from the mod page's URL. + /// The URL associated with , if different from the mod page's URL. [MemberNotNull(nameof(ModInfoModel.Version))] - public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null) + public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null, string? mainVersionUrl = null, string? previewVersionUrl = null) { this.Version = version; this.PreviewVersion = previewVersion; + this.MainVersionUrl = mainVersionUrl; + this.PreviewVersionUrl = previewVersionUrl; return this; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 70070d631..5581d7991 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -66,23 +66,25 @@ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNon { // get base model ModInfoModel model = new(); - if (page.IsValid) - model.SetBasicInfo(page.Name, page.Url); - else - { + if (!page.IsValid) { model.SetError(page.Status, page.Error); return model; } + if (page.IsSubkeyStrict && subkey is not null) { + if (subkey.Length > 0 && subkey[0] == '@') { + subkey = subkey.Substring(1); + } + } + // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion); - if (!hasVersions && subkey != null) - hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion); + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl); if (!hasVersions) - return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions."); + return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); + model.SetBasicInfo(page.GetName(subkey) ?? page.Name, page.GetUrl(subkey) ?? page.Url); // return info - return model.SetVersions(mainVersion!, previewVersion); + return model.SetVersions(mainVersion!, previewVersion, mainVersionUrl, previewVersionUrl); } /// Get a semantic local version for update checks. @@ -113,10 +115,12 @@ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNon /// The changes to apply to remote versions for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview) + private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainVersionUrl, out string? previewVersionUrl) { main = null; preview = null; + mainVersionUrl = null; + previewVersionUrl = null; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() @@ -132,7 +136,7 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt // get mod version ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); if (modVersion != null) - yield return (download: null, version: ParseAndMapVersion(mod.Version)); + yield return (download: null, version: modVersion); // get file versions foreach (IModDownload download in mod.Downloads) @@ -148,10 +152,12 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; + mainVersionUrl = null; + previewVersionUrl = null; // get latest main + preview version foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) @@ -159,28 +165,40 @@ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out I if (entry.version is null || filter?.Invoke(entry) == false) continue; - if (entry.version.IsPrerelease()) - previewVersion ??= entry.version; - else - mainVersion ??= entry.version; - - if (mainVersion != null) + if (entry.version.IsPrerelease()) { + if (previewVersion is null) { + previewVersion = entry.version; + previewVersionUrl = entry.download?.Url; + } + } else { + mainVersion = entry.version; + mainVersionUrl = entry.download?.Url; break; // any others will be older since entries are sorted by version + } } // normalize values if (previewVersion is not null) { - mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version - if (!previewVersion.IsNewerThan(mainVersion)) + if (mainVersion is null) { + // if every version is prerelease, latest one is the main version + mainVersion = previewVersion; + mainVersionUrl = previewVersionUrl; + } + if (!previewVersion.IsNewerThan(mainVersion)) { previewVersion = null; + previewVersionUrl = null; + } } } - if (subkey is not null) - TryGetVersions(out main, out preview, entry => entry.download?.MatchesSubkey(subkey) == true); + if (subkey is not null) { + TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl, entry => entry.download?.MatchesSubkey(subkey) == true); + if (mod?.IsSubkeyStrict == true) + return main != null; + } if (main is null) - TryGetVersions(out main, out preview); + TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl); return main != null; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index d26cb6f81..cfec1d08d 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,6 +12,7 @@ + @@ -48,4 +49,7 @@ PreserveNewest + + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 54c259799..a068a9984 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -20,6 +20,7 @@ using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest; using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RedirectRules; @@ -149,6 +150,8 @@ public void ConfigureServices(IServiceCollection services) baseUrl: api.PastebinBaseUrl, userAgent: userAgent )); + + services.AddSingleton(new UpdateManifestClient(userAgent: userAgent)); } // init helpers From 0ef55c4b1e2aea60bc7281c31b9ca0dac70f272a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dola=C5=9B?= Date: Mon, 9 Jan 2023 11:39:38 +0100 Subject: [PATCH 03/84] Mod-group specific config.json overriding --- src/SMAPI/Constants.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index c5058e4b0..3ff3159b7 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -142,6 +142,9 @@ public static string ExecutionPath /// The file path for the overrides file for , which is applied over it. internal static string ApiUserConfigPath => Path.Combine(Constants.InternalFilesPath, "config.user.json"); + /// The mod-group-specific file path for the overrides file for , which is applied over it. + internal static string ApiModGroupConfigPath => Path.Combine(ModsPath, "config.json"); + /// The file path for the SMAPI metadata file. internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json"); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c977ad65d..fea0f7d09 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -197,6 +197,8 @@ public SCore(string modsPath, bool writeToConsole, bool? developerMode) this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)) ?? throw new InvalidOperationException("The 'smapi-internal/config.json' file is missing or invalid. You can reinstall SMAPI to fix this."); if (File.Exists(Constants.ApiUserConfigPath)) JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); + if (File.Exists(Constants.ApiModGroupConfigPath)) + JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiModGroupConfigPath), this.Settings); if (developerMode.HasValue) this.Settings.OverrideDeveloperMode(developerMode.Value); From 55fd4839da43e7ca205eaa85480786e3dfe8af6f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: [PATCH 04/84] minor formatting, copyediting, and error-handling --- .../Framework/UpdateData/ModSiteKey.cs | 2 +- .../Framework/UpdateData/UpdateKey.cs | 47 +++++------ .../Controllers/ModsApiController.cs | 6 +- .../Framework/Clients/GenericModDownload.cs | 27 +++---- .../Framework/Clients/GenericModPage.cs | 19 ++--- .../UpdateManifest/IUpdateManifestClient.cs | 3 +- .../TextAsJsonMediaTypeFormatter.cs | 18 +++-- .../UpdateManifest/UpdateManifestClient.cs | 37 +++++---- .../UpdateManifestModDownload.cs | 40 ++++++---- .../UpdateManifest/UpdateManifestModModel.cs | 32 +++++--- .../UpdateManifest/UpdateManifestModPage.cs | 80 +++++++++++-------- .../UpdateManifest/UpdateManifestModel.cs | 30 ++++--- .../UpdateManifestVersionModel.cs | 28 ++++--- src/SMAPI.Web/Framework/IModDownload.cs | 13 +-- src/SMAPI.Web/Framework/IModPage.cs | 18 ++--- src/SMAPI.Web/Framework/ModInfoModel.cs | 37 +++++---- src/SMAPI.Web/Framework/ModSiteManager.cs | 72 +++++++++++------ 17 files changed, 293 insertions(+), 216 deletions(-) diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs index d1dd90497..195b03671 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -21,7 +21,7 @@ public enum ModSiteKey /// The Nexus Mods mod repository. Nexus, - /// An arbitrary URL for an update manifest file. + /// An arbitrary URL to a JSON file containing update data. UpdateManifest } } diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 7c2cc73cc..3e8064fdf 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -54,29 +54,6 @@ public UpdateKey(string? rawText, ModSiteKey site, string? id, string? subkey) public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } - /// - /// Split a string into two at a delimiter. If the delimiter does not appear in the string then the second - /// value of the returned tuple is null. Both returned strings are trimmed of whitespace. - /// - /// The string to split. - /// The character on which to split. - /// - /// If true then the second string returned will include the delimiter character - /// (provided that the string is not null) - /// - /// - /// A pair containing the string consisting of all characters in before the first - /// occurrence of , and a string consisting of all characters in - /// after the first occurrence of or null if the delimiter does not - /// exist in s. Both strings are trimmed of whitespace. - /// - private static (string, string?) Bifurcate(string s, char delimiter, bool keepDelimiter = false) { - int pos = s.IndexOf(delimiter); - if (pos < 0) - return (s.Trim(), null); - return (s.Substring(0, pos).Trim(), s.Substring(pos + (keepDelimiter ? 0 : 1)).Trim()); - } - /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string? raw) @@ -84,14 +61,14 @@ public static UpdateKey Parse(string? raw) if (raw is null) return new UpdateKey(raw, ModSiteKey.Unknown, null, null); // extract site + ID - (string rawSite, string? id) = Bifurcate(raw, ':'); - if (string.IsNullOrWhiteSpace(id)) + (string rawSite, string? id) = UpdateKey.SplitTwoParts(raw, ':'); + if (string.IsNullOrEmpty(id)) id = null; // extract subkey string? subkey = null; if (id != null) - (id, subkey) = Bifurcate(id, '@', true); + (id, subkey) = UpdateKey.SplitTwoParts(id, '@', true); // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) @@ -160,5 +137,23 @@ public static string GetString(ModSiteKey site, string? id, string? subkey = nul { return $"{site}:{id}{subkey}".Trim(); } + + + /********* + ** Private methods + *********/ + /// Split a string into two parts at a delimiter and trim whitespace. + /// The string to split. + /// The character on which to split. + /// Whether to include the delimiter in the second string. + /// Returns a tuple containing the two strings, with the second value null if the delimiter wasn't found. + private static (string, string?) SplitTwoParts(string str, char delimiter, bool keepDelimiter = false) + { + int splitIndex = str.IndexOf(delimiter); + + return splitIndex >= 0 + ? (str.Substring(0, splitIndex).Trim(), str.Substring(splitIndex + (keepDelimiter ? 0 : 1)).Trim()) + : (str.Trim(), null); + } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1c34f2afc..5fc4987d3 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -64,7 +64,7 @@ internal class ModsApiController : Controller /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - /// The UpdateManifest client. + /// The API client for arbitrary update manifest URLs. public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); @@ -163,9 +163,9 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod // if there's only a prerelease version (e.g. from GitHub), don't override the main version ISemanticVersion? curMain = data.Version; - string? curMainUrl = data.MainVersionUrl; ISemanticVersion? curPreview = data.PreviewVersion; - string? curPreviewUrl = data.PreviewVersionUrl; + string? curMainUrl = data.MainModPageUrl; + string? curPreviewUrl = data.PreviewModPageUrl; if (curPreview == null && curMain?.IsPrerelease() == true) { curPreview = curMain; diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 5cc03aba9..6c9c08efb 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -17,10 +17,9 @@ internal class GenericModDownload : IModDownload /// The download's file version. public string? Version { get; } - /// - /// The URL for this download, if it has one distinct from the mod page's URL. - /// - public string? Url { get; } + /// The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from. + public string? ModPageUrl { get; } + /********* ** Public methods @@ -29,23 +28,21 @@ internal class GenericModDownload : IModDownload /// The download's display name. /// The download's description. /// The download's file version. - /// The download's URL (if different from the mod page's URL). - public GenericModDownload(string name, string? description, string? version, string? url = null) + /// The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from. + public GenericModDownload(string name, string? description, string? version, string? modPageUrl = null) { this.Name = name; this.Description = description; this.Version = version; - this.Url = url; + this.ModPageUrl = modPageUrl; } - /// - /// Return if the subkey matches this download. A subkey matches if it appears as - /// a substring in the name or description. - /// - /// the subkey - /// if matches this download, otherwise - public virtual bool MatchesSubkey(string subkey) { - return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true + /// Get whether the subkey matches this download. + /// The update subkey to check. + public virtual bool MatchesSubkey(string subkey) + { + return + this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 4c66e1a0c..e939f1d85 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,9 +40,10 @@ internal class GenericModPage : IModPage [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; - /// Whether to use strict subkey matching or not. + /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. public bool IsSubkeyStrict { get; set; } = false; + /********* ** Public methods *********/ @@ -82,17 +83,17 @@ public IModPage SetError(RemoteModStatus status, string error) return this; } - /// Returns the mod page name. - /// ignored - /// The mod page name. - public virtual string? GetName(string? subkey) { + /// Get the mod name for an update subkey, if different from the mod page name. + /// The update subkey. + public virtual string? GetName(string? subkey) + { return this.Name; } - /// Returns the mod page URL. - /// ignored - /// The mod page URL. - public virtual string? GetUrl(string? subkey) { + /// Get the mod page URL for an update subkey, if different from the mod page it was fetched from. + /// The update subkey. + public virtual string? GetUrl(string? subkey) + { return this.Url; } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs index 36d018c76..dd9f5811d 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -3,7 +3,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// An HTTP client for fetching an update manifest from an arbitrary URL. + /// An API client for fetching update metadata from an arbitrary JSON URL. internal interface IUpdateManifestClient : IModSiteClient, IDisposable { } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs index 48e3c2948..02722cb1b 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs @@ -1,14 +1,18 @@ -// Copyright 2022 Jamie Taylor +// Copyright 2022 Jamie Taylor using System.Net.Http.Formatting; using System.Net.Http.Headers; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// - /// A that can parse from content of type text/plain. - /// - internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// A that can parse from content of type text/plain. + internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter + { + /********* + ** Public methods + *********/ /// Construct a new - public TextAsJsonMediaTypeFormatter() { + public TextAsJsonMediaTypeFormatter() + { this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index d7cf49455..88a5c2f67 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,19 +1,21 @@ // Copyright 2022 Jamie Taylor -using System; +using System.Net; +using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using System.Threading.Tasks; -using System.Net; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// An HTTP client for fetching an update manifest from an arbitrary URL. - internal class UpdateManifestClient : IUpdateManifestClient { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// An API client for fetching update metadata from an arbitrary JSON URL. + internal class UpdateManifestClient : IUpdateManifestClient + { /********* ** Fields *********/ /// The underlying HTTP client. private readonly IClient Client; + /********* ** Accessors *********/ @@ -26,30 +28,35 @@ internal class UpdateManifestClient : IUpdateManifestClient { *********/ /// Construct an instance. /// The user agent for the API client. - public UpdateManifestClient(string userAgent) { + public UpdateManifestClient(string userAgent) + { this.Client = new FluentClient() .SetUserAgent(userAgent); this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter()); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() { + public void Dispose() + { this.Client.Dispose(); } /// - public async Task GetModData(string id) { + public async Task GetModData(string id) + { UpdateManifestModel? manifest; - try { + try + { manifest = await this.Client.GetAsync(id).As(); - } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } - if (manifest is null) { - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"Error parsing manifest at {id}"); + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } - return new UpdateManifestModPage(id, manifest); + return manifest is not null + ? new UpdateManifestModPage(id, manifest) + : new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"The update manifest at {id} has an invalid format"); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 117ae15c6..0a6d47366 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -1,27 +1,35 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ /// Metadata about a mod download in an update manifest file. - internal class UpdateManifestModDownload : GenericModDownload { - /// The subkey for this mod download - private readonly string subkey; + internal class UpdateManifestModDownload : GenericModDownload + { + /********* + ** Fields + *********/ + /// The update subkey for this mod download. + private readonly string Subkey; + + + /********* + ** Public methods + *********/ /// Construct an instance. - /// The subkey for this download. + /// The field name for this mod download in the manifest. /// The mod name for this download. /// The download's version. /// The download's URL. - public UpdateManifestModDownload(string subkey, string name, string? version, string? url) : base(name, null, version, url) { - this.subkey = subkey; + public UpdateManifestModDownload(string fieldName, string name, string? version, string? url) + : base(name, null, version, url) + { + this.Subkey = fieldName; } - /// - /// Returns iff the given subkey is the same as the subkey for this download. - /// - /// The subkey to match - /// if is the same as the subkey for this download, otherwise. - public override bool MatchesSubkey(string subkey) { - return this.subkey == subkey; + /// Get whether the subkey matches this download. + /// The update subkey to check. + public override bool MatchesSubkey(string subkey) + { + return subkey == this.Subkey; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs index 4ec9c03dc..926423217 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs @@ -1,26 +1,34 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// Data model for a mod in an update manifest. - internal class UpdateManifestModModel { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// The data model for a mod in an update manifest file. + internal class UpdateManifestModModel + { + /********* + ** Accessors + *********/ /// The mod's name. - public string Name { get; } + public string? Name { get; } - /// The mod's URL. + /// The mod page URL from which to download updates. public string? Url { get; } - /// The versions for this mod. - public UpdateManifestVersionModel[] Versions { get; } + /// The available versions for this mod. + public UpdateManifestVersionModel[]? Versions { get; } + + /********* + ** Public methods + *********/ /// Construct an instance. /// The mod's name. - /// The mod's URL. - /// The versions for this mod. - public UpdateManifestModModel(string name, string? url, UpdateManifestVersionModel[] versions) { + /// The mod page URL from which to download updates. + /// The available versions for this mod. + public UpdateManifestModModel(string? name, string? url, UpdateManifestVersionModel[]? versions) + { this.Name = name; this.Url = url; this.Versions = versions; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index 109175b55..bbc7b5da6 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -1,58 +1,74 @@ -// Copyright 2022 Jamie Taylor -using System; +// Copyright 2022 Jamie Taylor using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ /// Metadata about an update manifest "page". - internal class UpdateManifestModPage : GenericModPage { - /// The update manifest model. - private UpdateManifestModel manifest; - - /// Constuct an instance. - /// The "id" (i.e., URL) of this update manifest. - /// The manifest object model. - public UpdateManifestModPage(string id, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, id) { + internal class UpdateManifestModPage : GenericModPage + { + /********* + ** Fields + *********/ + /// The mods from the update manifest. + private readonly IDictionary Mods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The URL of the update manifest file. + /// The parsed update manifest. + public UpdateManifestModPage(string url, UpdateManifestModel manifest) + : base(ModSiteKey.UpdateManifest, url) + { this.IsSubkeyStrict = true; - this.manifest = manifest; - this.SetInfo(name: id, url: id, version: null, downloads: TranslateDownloads(manifest).ToArray()); + this.Mods = manifest.Mods ?? new Dictionary(); + this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray()); } /// Return the mod name for the given subkey, if it exists in this update manifest. /// The subkey. /// The mod name for the given subkey, or if this manifest does not contain the given subkey. - public override string? GetName(string? subkey) { - if (subkey is null) - return null; - this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); - return modModel?.Name; + public override string? GetName(string? subkey) + { + return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + ? modModel.Name + : null; } /// Return the mod URL for the given subkey, if it exists in this update manifest. /// The subkey. /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. - public override string? GetUrl(string? subkey) { - if (subkey is null) - return null; - this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); - return modModel?.Url; + public override string? GetUrl(string? subkey) + { + return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + ? modModel.Url + : null; } /********* ** Private methods *********/ - /// Translate the downloads from the manifest's object model into objects. - /// The manifest object model. - /// An for each in the manifest. - private static IEnumerable TranslateDownloads(UpdateManifestModel manifest) { - foreach (var entry in manifest.Subkeys) { - foreach (var version in entry.Value.Versions) { - yield return new UpdateManifestModDownload(entry.Key, entry.Value.Name, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); - } + /// Convert the raw download info from an update manifest to . + /// The mods from the update manifest. + private IEnumerable ParseDownloads(IDictionary? mods) + { + if (mods is null) + yield break; + + foreach ((string modKey, UpdateManifestModModel mod) in mods) + { + if (mod.Versions is null) + continue; + + foreach (UpdateManifestVersionModel version in mod.Versions) + yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); } } } -} \ No newline at end of file +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs index 03f89726e..ad6180226 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs @@ -1,23 +1,31 @@ // Copyright 2022 Jamie Taylor -using System; using System.Collections.Generic; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// Data model for an update manifest. - internal class UpdateManifestModel { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// The data model for an update manifest file. + internal class UpdateManifestModel + { + /********* + ** Accessors + *********/ /// The manifest format version. - public string ManifestVersion { get; } + public string? ManifestVersion { get; } - /// The subkeys in this update manifest. - public IDictionary Subkeys { get; } + /// The mod info in this update manifest. + public IDictionary? Mods { get; } + + /********* + ** Public methods + *********/ /// Construct an instance. /// The manifest format version. - /// The subkeys in this update manifest. - public UpdateManifestModel(string manifestVersion, IDictionary subkeys) { + /// The mod info in this update manifest. + public UpdateManifestModel(string manifestVersion, IDictionary mods) + { this.ManifestVersion = manifestVersion; - this.Subkeys = subkeys; + this.Mods = mods; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs index 55b6db617..90e054d81 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs @@ -1,10 +1,14 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ /// Data model for a Version in an update manifest. - internal class UpdateManifestVersionModel { - /// The semantic version string. - public string Version { get; } + internal class UpdateManifestVersionModel + { + /********* + ** Accessors + *********/ + /// The mod's semantic version. + public string? Version { get; } /// The URL for this version's download page (if any). public string? DownloadPageUrl { get; } @@ -12,15 +16,19 @@ internal class UpdateManifestVersionModel { /// The URL for this version's direct file download (if any). public string? DownloadFileUrl { get; } + + /********* + ** Public methods + *********/ /// Construct an instance. - /// The semantic version string. - /// This version's download page URL (if any). - /// This version's direct file download URL (if any). - public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) { + /// The mod's semantic version. + /// This version's download page URL, if any. + /// This version's direct file download URL, if any. + public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) + { this.Version = version; this.DownloadPageUrl = downloadPageUrl; this.DownloadFileUrl = downloadFileUrl; } } } - diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index b0e9a664b..8cb829893 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -15,12 +15,15 @@ internal interface IModDownload /// The download's file version. string? Version { get; } - /// This download's URL (if it has a URL that is different from the containing mod page's URL). - string? Url { get; } + /// The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from. + string? ModPageUrl { get; } - /// Return iff the subkey matches this download - /// the subkey - /// if matches this download, otherwise + + /********* + ** Methods + *********/ + /// Get whether the subkey matches this download. + /// The update subkey to check. bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 3fc8bb811..ef1513eb1 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,25 +39,19 @@ internal interface IModPage [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// - /// Does this page use strict subkey matching. Pages that use string subkey matching do not fall back - /// to searching for versions without a subkey if there are no versions found when given a subkey. - /// Additionally, the leading @ is stripped from the subkey value before searching for matches. - /// + /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. Additionally, the leading @ is stripped from the subkey value before searching for matches. bool IsSubkeyStrict { get; } + /********* ** Methods *********/ - - /// Get the mod name associated with the given subkey, if any. - /// The subkey. - /// The mod name associated with the given subkey (if any) + /// Get the mod name for an update subkey, if different from the mod page name. + /// The update subkey. string? GetName(string? subkey); - /// Get the URL for the mod associated with the given subkey, if any. - /// The subkey. - /// The URL for the mod associated with the given subkey (if any) + /// Get the mod page URL for an update subkey, if different from the mod page it was fetched from. + /// The update subkey. string? GetUrl(string? subkey); /// Set the fetched mod info. diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index 174155890..502c08270 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -27,11 +27,12 @@ internal class ModInfoModel /// The error message indicating why the mod is invalid (if applicable). public string? Error { get; private set; } - /// The URL associated with the mod's latest version (if distinct from the mod page's URL). - public string? MainVersionUrl { get; private set; } + /// The mod page URL from which can be downloaded, if different from the . + public string? MainModPageUrl { get; private set; } + + /// The mod page URL from which can be downloaded, if different from the . + public string? PreviewModPageUrl { get; private set; } - /// The URL associated with the mod's (if distinct from the mod page's URL). - public string? PreviewVersionUrl { get; private set; } /********* ** Public methods @@ -51,7 +52,8 @@ public ModInfoModel(string name, string url, ISemanticVersion? version, ISemanti { this .SetBasicInfo(name, url) - .SetVersions(version!, previewVersion) + .SetMainVersion(version!) + .SetPreviewVersion(previewVersion) .SetError(status, error!); } @@ -67,18 +69,25 @@ public ModInfoModel SetBasicInfo(string name, string url) return this; } - /// Set the mod version info. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The URL associated with , if different from the mod page's URL. - /// The URL associated with , if different from the mod page's URL. + /// Set the mod's main version info. + /// The semantic version for the mod's latest stable release. + /// The mod page URL from which can be downloaded, if different from the . [MemberNotNull(nameof(ModInfoModel.Version))] - public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null, string? mainVersionUrl = null, string? previewVersionUrl = null) + public ModInfoModel SetMainVersion(ISemanticVersion version, string? modPageUrl = null) { this.Version = version; - this.PreviewVersion = previewVersion; - this.MainVersionUrl = mainVersionUrl; - this.PreviewVersionUrl = previewVersionUrl; + this.MainModPageUrl = modPageUrl; + + return this; + } + + /// Set the mod's preview version info. + /// The semantic version for the mod's latest preview release. + /// The mod page URL from which can be downloaded, if different from the . + public ModInfoModel SetPreviewVersion(ISemanticVersion? version, string? modPageUrl = null) + { + this.PreviewVersion = version; + this.PreviewModPageUrl = modPageUrl; return this; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 5581d7991..350159a38 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -66,25 +66,34 @@ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNon { // get base model ModInfoModel model = new(); - if (!page.IsValid) { + if (!page.IsValid) + { model.SetError(page.Status, page.Error); return model; } - if (page.IsSubkeyStrict && subkey is not null) { - if (subkey.Length > 0 && subkey[0] == '@') { + // trim subkey in strict mode + if (page.IsSubkeyStrict && subkey is not null) + { + if (subkey.StartsWith('@')) subkey = subkey.Substring(1); - } } // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl); + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); - model.SetBasicInfo(page.GetName(subkey) ?? page.Name, page.GetUrl(subkey) ?? page.Url); + // apply mod page info + model.SetBasicInfo( + name: page.GetName(subkey) ?? page.Name, + url: page.GetUrl(subkey) ?? page.Url + ); + // return info - return model.SetVersions(mainVersion!, previewVersion, mainVersionUrl, previewVersionUrl); + return model + .SetMainVersion(mainVersion!, mainModPageUrl) + .SetPreviewVersion(previewVersion, previewModPageUrl); } /// Get a semantic local version for update checks. @@ -115,12 +124,14 @@ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNon /// The changes to apply to remote versions for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainVersionUrl, out string? previewVersionUrl) + /// The mod page URL from which can be downloaded, if different from the 's URL. + /// The mod page URL from which can be downloaded, if different from the 's URL. + private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainModPageUrl, out string? previewModPageUrl) { main = null; preview = null; - mainVersionUrl = null; - previewVersionUrl = null; + mainModPageUrl = null; + previewModPageUrl = null; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() @@ -152,12 +163,12 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainUrl, out string? previewUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; - mainVersionUrl = null; - previewVersionUrl = null; + mainUrl = null; + previewUrl = null; // get latest main + preview version foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) @@ -165,14 +176,18 @@ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out I if (entry.version is null || filter?.Invoke(entry) == false) continue; - if (entry.version.IsPrerelease()) { - if (previewVersion is null) { + if (entry.version.IsPrerelease()) + { + if (previewVersion is null) + { previewVersion = entry.version; - previewVersionUrl = entry.download?.Url; + previewUrl = entry.download?.ModPageUrl; } - } else { + } + else + { mainVersion = entry.version; - mainVersionUrl = entry.download?.Url; + mainUrl = entry.download?.ModPageUrl; break; // any others will be older since entries are sorted by version } } @@ -180,26 +195,31 @@ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out I // normalize values if (previewVersion is not null) { - if (mainVersion is null) { + if (mainVersion is null) + { // if every version is prerelease, latest one is the main version mainVersion = previewVersion; - mainVersionUrl = previewVersionUrl; + mainUrl = previewUrl; } - if (!previewVersion.IsNewerThan(mainVersion)) { + if (!previewVersion.IsNewerThan(mainVersion)) + { previewVersion = null; - previewVersionUrl = null; + previewUrl = null; } } } - if (subkey is not null) { - TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl, entry => entry.download?.MatchesSubkey(subkey) == true); + // get versions for subkey + if (subkey is not null) + { + TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); if (mod?.IsSubkeyStrict == true) return main != null; } - if (main is null) - TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl); + // fallback to non-subkey versions + if (main is null) + TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl); return main != null; } From 25c2081d43bd4026552cda687fb56216dd3a9f8e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: [PATCH 05/84] encapsulate update manifest implementation details when possible --- .../Controllers/ModsApiController.cs | 2 +- .../UpdateManifestModDownload.cs | 2 +- .../UpdateManifest/UpdateManifestModPage.cs | 4 +- src/SMAPI.Web/Framework/IModPage.cs | 2 +- src/SMAPI.Web/Framework/ModSiteManager.cs | 63 ++++++++++--------- src/SMAPI.Web/SMAPI.Web.csproj | 4 -- 6 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 5fc4987d3..2003e25f5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -301,7 +301,7 @@ private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, b } // get version info - return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions); + return this.ModSites.GetPageVersions(page, updateKey, allowNonStandardVersions, mapRemoteVersions); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 0a6d47366..0128fa17b 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -22,7 +22,7 @@ internal class UpdateManifestModDownload : GenericModDownload public UpdateManifestModDownload(string fieldName, string name, string? version, string? url) : base(name, null, version, url) { - this.Subkey = fieldName; + this.Subkey = '@' + fieldName; } /// Get whether the subkey matches this download. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index bbc7b5da6..befad268b 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -34,7 +34,7 @@ public UpdateManifestModPage(string url, UpdateManifestModel manifest) /// The mod name for the given subkey, or if this manifest does not contain the given subkey. public override string? GetName(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) ? modModel.Name : null; } @@ -44,7 +44,7 @@ public UpdateManifestModPage(string url, UpdateManifestModel manifest) /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. public override string? GetUrl(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) ? modModel.Url : null; } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index ef1513eb1..84af9516d 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,7 +39,7 @@ internal interface IModPage [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. Additionally, the leading @ is stripped from the subkey value before searching for matches. + /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. bool IsSubkeyStrict { get; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 350159a38..f1a088d53 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -59,11 +59,13 @@ public async Task GetModPageAsync(UpdateKey updateKey) /// Parse version info for the given mod page info. /// The mod page info. - /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) + /// The update key to match in available files. /// The changes to apply to remote versions for update checks. /// Whether to allow non-standard versions. - public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) + public ModInfoModel GetPageVersions(IModPage page, UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) { + bool isManifest = updateKey.Site == ModSiteKey.UpdateManifest; + // get base model ModInfoModel model = new(); if (!page.IsValid) @@ -71,23 +73,23 @@ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNon model.SetError(page.Status, page.Error); return model; } - - // trim subkey in strict mode - if (page.IsSubkeyStrict && subkey is not null) - { - if (subkey.StartsWith('@')) - subkey = subkey.Substring(1); - } + else if (!isManifest) // if this is a manifest, the 'mod page' is the JSON file + model.SetBasicInfo(page.Name, page.Url); // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); + bool hasVersions = this.TryGetLatestVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) - return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); + { + string displayId = isManifest + ? page.Id + updateKey.Subkey + : page.Id; + return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{displayId}' has no valid versions."); + } // apply mod page info model.SetBasicInfo( - name: page.GetName(subkey) ?? page.Name, - url: page.GetUrl(subkey) ?? page.Url + name: page.GetName(updateKey.Subkey) ?? page.Name, + url: page.GetUrl(updateKey.Subkey) ?? page.Url ); // return info @@ -132,30 +134,29 @@ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonSt preview = null; mainModPageUrl = null; previewModPageUrl = null; + if (mod is null) + return false; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() { - if (mod != null) + ISemanticVersion? ParseAndMapVersion(string? raw) { - ISemanticVersion? ParseAndMapVersion(string? raw) - { - raw = this.NormalizeVersion(raw); - return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions); - } + raw = this.NormalizeVersion(raw); + return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions); + } - // get mod version - ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); - if (modVersion != null) - yield return (download: null, version: modVersion); + // get mod version + ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version); + if (modVersion != null) + yield return (download: null, version: modVersion); - // get file versions - foreach (IModDownload download in mod.Downloads) - { - ISemanticVersion? cur = ParseAndMapVersion(download.Version); - if (cur != null) - yield return (download, cur); - } + // get file versions + foreach (IModDownload download in mod.Downloads) + { + ISemanticVersion? cur = ParseAndMapVersion(download.Version); + if (cur != null) + yield return (download, cur); } } var versions = GetAllVersions() @@ -213,7 +214,7 @@ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out I if (subkey is not null) { TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); - if (mod?.IsSubkeyStrict == true) + if (mod.IsSubkeyStrict) return main != null; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index cfec1d08d..d26cb6f81 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,7 +12,6 @@ - @@ -49,7 +48,4 @@ PreserveNewest - - - From e5576d9c925210c83ba9f123c2ced86377ece560 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: [PATCH 06/84] require subkey for update manifest checks --- .../Framework/Clients/GenericModPage.cs | 4 +-- .../UpdateManifest/UpdateManifestModPage.cs | 2 +- src/SMAPI.Web/Framework/IModPage.cs | 4 +-- src/SMAPI.Web/Framework/ModSiteManager.cs | 29 ++++++++----------- src/SMAPI.Web/Framework/RemoteModStatus.cs | 3 ++ 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index e939f1d85..63ca5a95d 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,8 +40,8 @@ internal class GenericModPage : IModPage [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; - /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. - public bool IsSubkeyStrict { get; set; } = false; + /// Whether this mod page requires update subkeys and does not allow matching downloads without them. + public bool RequireSubkey { get; set; } = false; /********* diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index befad268b..7537eb24b 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -24,7 +24,7 @@ internal class UpdateManifestModPage : GenericModPage public UpdateManifestModPage(string url, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, url) { - this.IsSubkeyStrict = true; + this.RequireSubkey = true; this.Mods = manifest.Mods ?? new Dictionary(); this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray()); } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 84af9516d..85be41e2b 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,8 +39,8 @@ internal interface IModPage [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. - bool IsSubkeyStrict { get; } + /// Whether this mod page requires update subkeys and does not allow matching downloads without them. + bool RequireSubkey { get; } /********* diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index f1a088d53..4bb72f78e 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -64,27 +64,26 @@ public async Task GetModPageAsync(UpdateKey updateKey) /// Whether to allow non-standard versions. public ModInfoModel GetPageVersions(IModPage page, UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) { - bool isManifest = updateKey.Site == ModSiteKey.UpdateManifest; + // get ID to show in errors + string displayId = page.RequireSubkey + ? page.Id + updateKey.Subkey + : page.Id; - // get base model + // validate ModInfoModel model = new(); if (!page.IsValid) - { - model.SetError(page.Status, page.Error); - return model; - } - else if (!isManifest) // if this is a manifest, the 'mod page' is the JSON file + return model.SetError(page.Status, page.Error); + if (page.RequireSubkey && updateKey.Subkey is null) + return model.SetError(RemoteModStatus.RequiredSubkeyMissing, $"The {page.Site} mod with ID '{displayId}' requires an update subkey indicating which mod to fetch."); + + // add basic info (unless it's a manifest, in which case the 'mod page' is the JSON file) + if (updateKey.Site != ModSiteKey.UpdateManifest) model.SetBasicInfo(page.Name, page.Url); // fetch versions bool hasVersions = this.TryGetLatestVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) - { - string displayId = isManifest - ? page.Id + updateKey.Subkey - : page.Id; return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{displayId}' has no valid versions."); - } // apply mod page info model.SetBasicInfo( @@ -212,14 +211,10 @@ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out I // get versions for subkey if (subkey is not null) - { TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); - if (mod.IsSubkeyStrict) - return main != null; - } // fallback to non-subkey versions - if (main is null) + if (main is null && !mod.RequireSubkey) TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl); return main != null; } diff --git a/src/SMAPI.Web/Framework/RemoteModStatus.cs b/src/SMAPI.Web/Framework/RemoteModStatus.cs index 139ecfd39..235bcec4e 100644 --- a/src/SMAPI.Web/Framework/RemoteModStatus.cs +++ b/src/SMAPI.Web/Framework/RemoteModStatus.cs @@ -12,6 +12,9 @@ internal enum RemoteModStatus /// The mod does not exist. DoesNotExist, + /// The mod page exists, but it requires a subkey and none was provided. + RequiredSubkeyMissing, + /// The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred). TemporaryError } From 5c22406c13ef4933a5e17e9036d9fd3ca9b9a9a7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:22 -0500 Subject: [PATCH 07/84] adjust JSON formatter instead of adding a new one --- .../TextAsJsonMediaTypeFormatter.cs | 19 ------------------- .../UpdateManifest/UpdateManifestClient.cs | 4 +++- 2 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs deleted file mode 100644 index 02722cb1b..000000000 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2022 Jamie Taylor -using System.Net.Http.Formatting; -using System.Net.Http.Headers; - -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest -{ - /// A that can parse from content of type text/plain. - internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter - { - /********* - ** Public methods - *********/ - /// Construct a new - public TextAsJsonMediaTypeFormatter() - { - this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); - } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 88a5c2f67..0199027f1 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,5 +1,6 @@ // Copyright 2022 Jamie Taylor using System.Net; +using System.Net.Http.Headers; using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -32,7 +33,8 @@ public UpdateManifestClient(string userAgent) { this.Client = new FluentClient() .SetUserAgent(userAgent); - this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter()); + + this.Client.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. From d76964c5f381a5b8faba123646d851e7ae2c06f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: [PATCH 08/84] group response models --- .../{ => ResponseModels}/UpdateManifestModModel.cs | 2 +- .../UpdateManifest/{ => ResponseModels}/UpdateManifestModel.cs | 2 +- .../{ => ResponseModels}/UpdateManifestVersionModel.cs | 2 +- .../Framework/Clients/UpdateManifest/UpdateManifestClient.cs | 1 + .../Framework/Clients/UpdateManifest/UpdateManifestModPage.cs | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) rename src/SMAPI.Web/Framework/Clients/UpdateManifest/{ => ResponseModels}/UpdateManifestModModel.cs (98%) rename src/SMAPI.Web/Framework/Clients/UpdateManifest/{ => ResponseModels}/UpdateManifestModel.cs (98%) rename src/SMAPI.Web/Framework/Clients/UpdateManifest/{ => ResponseModels}/UpdateManifestVersionModel.cs (98%) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs similarity index 98% rename from src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs rename to src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs index 926423217..ee1fbeb67 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -1,5 +1,5 @@ // Copyright 2022 Jamie Taylor -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// The data model for a mod in an update manifest file. internal class UpdateManifestModModel diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs similarity index 98% rename from src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs rename to src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index ad6180226..e213fbab4 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -1,7 +1,7 @@ // Copyright 2022 Jamie Taylor using System.Collections.Generic; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// The data model for an update manifest file. internal class UpdateManifestModel diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs similarity index 98% rename from src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs rename to src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs index 90e054d81..1e84501f4 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -1,5 +1,5 @@ // Copyright 2022 Jamie Taylor -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// Data model for a Version in an update manifest. internal class UpdateManifestVersionModel diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 0199027f1..0d19900ea 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index 7537eb24b..251ed9ec8 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { From 610e2722c6687591faacb942d6f752c5f3c620d7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: [PATCH 09/84] rename & validate format version --- .../ResponseModels/UpdateManifestModel.cs | 10 ++++----- .../UpdateManifest/UpdateManifestClient.cs | 21 ++++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index e213fbab4..ff3dccbc3 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -9,8 +9,8 @@ internal class UpdateManifestModel /********* ** Accessors *********/ - /// The manifest format version. - public string? ManifestVersion { get; } + /// The manifest format version. This is equivalent to the SMAPI version, and is used to parse older manifests correctly if later versions of SMAPI change the expected format. + public string Format { get; } /// The mod info in this update manifest. public IDictionary? Mods { get; } @@ -20,11 +20,11 @@ internal class UpdateManifestModel ** Public methods *********/ /// Construct an instance. - /// The manifest format version. + /// The manifest format version. /// The mod info in this update manifest. - public UpdateManifestModel(string manifestVersion, IDictionary mods) + public UpdateManifestModel(string format, IDictionary mods) { - this.ManifestVersion = manifestVersion; + this.Format = format; this.Mods = mods; } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 0d19900ea..cd102ec5b 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,8 +1,12 @@ // Copyright 2022 Jamie Taylor + +using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; using System.Threading.Tasks; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels; @@ -45,21 +49,32 @@ public void Dispose() } /// + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method which ensures the annotations are correct.")] public async Task GetModData(string id) { + // get raw update manifest UpdateManifestModel? manifest; try { manifest = await this.Client.GetAsync(id).As(); + if (manifest is null) + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is empty"); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } + catch (Exception ex) + { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has an invalid format: {ex.Message}"); + } + + // validate + if (!SemanticVersion.TryParse(manifest.Format, out _)) + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has invalid format version '{manifest.Format}'"); - return manifest is not null - ? new UpdateManifestModPage(id, manifest) - : new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"The update manifest at {id} has an invalid format"); + // build model + return new UpdateManifestModPage(id, manifest); } } } From 3eb98b565f48c26384f0e83e4012fc9b40f1d819 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: [PATCH 10/84] simplify & validate manifest mod page URLs This avoids an issue where users are told to download it from the JSON manifest URL. --- .../ResponseModels/UpdateManifestModModel.cs | 14 ++++++++------ .../ResponseModels/UpdateManifestModel.cs | 6 +++--- .../ResponseModels/UpdateManifestVersionModel.cs | 15 +++++---------- .../UpdateManifest/UpdateManifestClient.cs | 11 +++++++++++ .../UpdateManifest/UpdateManifestModPage.cs | 15 ++++++--------- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs index ee1fbeb67..418fb26b4 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -1,4 +1,6 @@ // Copyright 2022 Jamie Taylor +using System; + namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// The data model for a mod in an update manifest file. @@ -11,10 +13,10 @@ internal class UpdateManifestModModel public string? Name { get; } /// The mod page URL from which to download updates. - public string? Url { get; } + public string? ModPageUrl { get; } /// The available versions for this mod. - public UpdateManifestVersionModel[]? Versions { get; } + public UpdateManifestVersionModel[] Versions { get; } /********* @@ -22,13 +24,13 @@ internal class UpdateManifestModModel *********/ /// Construct an instance. /// The mod's name. - /// The mod page URL from which to download updates. + /// The mod page URL from which to download updates. /// The available versions for this mod. - public UpdateManifestModModel(string? name, string? url, UpdateManifestVersionModel[]? versions) + public UpdateManifestModModel(string? name, string? modPageUrl, UpdateManifestVersionModel[]? versions) { this.Name = name; - this.Url = url; - this.Versions = versions; + this.ModPageUrl = modPageUrl; + this.Versions = versions ?? Array.Empty(); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index ff3dccbc3..3b930ff34 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -13,7 +13,7 @@ internal class UpdateManifestModel public string Format { get; } /// The mod info in this update manifest. - public IDictionary? Mods { get; } + public IDictionary Mods { get; } /********* @@ -22,10 +22,10 @@ internal class UpdateManifestModel /// Construct an instance. /// The manifest format version. /// The mod info in this update manifest. - public UpdateManifestModel(string format, IDictionary mods) + public UpdateManifestModel(string format, IDictionary? mods) { this.Format = format; - this.Mods = mods; + this.Mods = mods ?? new Dictionary(); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs index 1e84501f4..7cfb0cfcc 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -10,11 +10,8 @@ internal class UpdateManifestVersionModel /// The mod's semantic version. public string? Version { get; } - /// The URL for this version's download page (if any). - public string? DownloadPageUrl { get; } - - /// The URL for this version's direct file download (if any). - public string? DownloadFileUrl { get; } + /// The mod page URL from which to download updates, if different from . + public string? ModPageUrl { get; } /********* @@ -22,13 +19,11 @@ internal class UpdateManifestVersionModel *********/ /// Construct an instance. /// The mod's semantic version. - /// This version's download page URL, if any. - /// This version's direct file download URL, if any. - public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) + /// The mod page URL from which to download updates, if different from . + public UpdateManifestVersionModel(string version, string? modPageUrl) { this.Version = version; - this.DownloadPageUrl = downloadPageUrl; - this.DownloadFileUrl = downloadFileUrl; + this.ModPageUrl = modPageUrl; } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index cd102ec5b..9a2887c28 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -72,6 +72,17 @@ public void Dispose() // validate if (!SemanticVersion.TryParse(manifest.Format, out _)) return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has invalid format version '{manifest.Format}'"); + foreach ((string modKey, UpdateManifestModModel mod) in manifest.Mods) + { + if (string.IsNullOrWhiteSpace(mod.ModPageUrl)) + { + foreach (UpdateManifestVersionModel download in mod.Versions) + { + if (string.IsNullOrWhiteSpace(download.ModPageUrl)) + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is invalid (all mod downloads must have a mod page URL)"); + } + } + } // build model return new UpdateManifestModPage(id, manifest); diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index 251ed9ec8..f4ad05005 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -26,7 +26,7 @@ public UpdateManifestModPage(string url, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, url) { this.RequireSubkey = true; - this.Mods = manifest.Mods ?? new Dictionary(); + this.Mods = manifest.Mods; this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray()); } @@ -35,8 +35,8 @@ public UpdateManifestModPage(string url, UpdateManifestModel manifest) /// The mod name for the given subkey, or if this manifest does not contain the given subkey. public override string? GetName(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) - ? modModel.Name + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod) + ? mod.Name : null; } @@ -45,8 +45,8 @@ public UpdateManifestModPage(string url, UpdateManifestModel manifest) /// The mod URL for the given subkey, or if this manifest does not contain the given subkey. public override string? GetUrl(string? subkey) { - return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? modModel) - ? modModel.Url + return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod) + ? mod.ModPageUrl : null; } @@ -63,11 +63,8 @@ private IEnumerable ParseDownloads(IDictionary Date: Sun, 29 Jan 2023 16:37:23 -0500 Subject: [PATCH 11/84] don't allow update manifests before SMAPI 4.0.0 until the feature is released --- src/SMAPI.Web/Controllers/ModsApiController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 2003e25f5..f687c7dd2 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -147,7 +147,12 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod foreach (UpdateKey updateKey in updateKeys) { // validate update key - if (!updateKey.LooksValid) + if ( + !updateKey.LooksValid +#if SMAPI_DEPRECATED + || (updateKey.Site == ModSiteKey.UpdateManifest && apiVersion?.IsNewerThan("4.0.0-alpha") != true) // 4.0-alpha feature, don't make available to released mods in case it changes before release +#endif + ) { errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'."); continue; From b5f46000f4583dbb15a97981376a3695be2f74f3 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sun, 29 Jan 2023 18:14:24 -0500 Subject: [PATCH 12/84] Remove copyright line from individual files --- .../Framework/Clients/UpdateManifest/IUpdateManifestClient.cs | 1 - .../UpdateManifest/ResponseModels/UpdateManifestModModel.cs | 1 - .../Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs | 1 - .../UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs | 1 - .../Framework/Clients/UpdateManifest/UpdateManifestClient.cs | 1 - .../Clients/UpdateManifest/UpdateManifestModDownload.cs | 1 - .../Framework/Clients/UpdateManifest/UpdateManifestModPage.cs | 1 - 7 files changed, 7 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs index dd9f5811d..bf1edd3f5 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs index 418fb26b4..ead5c2299 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs index 3b930ff34..5ccd31b0d 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System.Collections.Generic; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs index 7cfb0cfcc..6678f5eba 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels { /// Data model for a Version in an update manifest. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 9a2887c28..6040c5b95 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System; using System.Diagnostics.CodeAnalysis; diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 0128fa17b..f8cb760a4 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { /// Metadata about a mod download in an update manifest file. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index f4ad05005..df7527136 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -1,4 +1,3 @@ -// Copyright 2022 Jamie Taylor using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; From a4b193b920879b7db255f387af8e3c9b4fa2b46a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Jan 2023 19:06:10 -0500 Subject: [PATCH 13/84] add stricter validation for update manifests --- .../UpdateManifest/UpdateManifestClient.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 6040c5b95..1614beab0 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -57,7 +57,7 @@ public void Dispose() { manifest = await this.Client.GetAsync(id).As(); if (manifest is null) - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is empty"); + return this.GetFormatError(id, "manifest can't be empty"); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { @@ -65,26 +65,43 @@ public void Dispose() } catch (Exception ex) { - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has an invalid format: {ex.Message}"); + return this.GetFormatError(id, ex.Message); } // validate if (!SemanticVersion.TryParse(manifest.Format, out _)) - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} has invalid format version '{manifest.Format}'"); - foreach ((string modKey, UpdateManifestModModel mod) in manifest.Mods) + return this.GetFormatError(id, $"invalid format version '{manifest.Format}'"); + foreach (UpdateManifestModModel mod in manifest.Mods.Values) { + if (mod is null) + return this.GetFormatError(id, "a mod record can't be null"); if (string.IsNullOrWhiteSpace(mod.ModPageUrl)) + return this.GetFormatError(id, $"all mods must have a {nameof(mod.ModPageUrl)} value"); + foreach (UpdateManifestVersionModel? version in mod.Versions) { - foreach (UpdateManifestVersionModel download in mod.Versions) - { - if (string.IsNullOrWhiteSpace(download.ModPageUrl)) - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.InvalidData, $"The update manifest at {id} is invalid (all mod downloads must have a mod page URL)"); - } + if (version is null) + return this.GetFormatError(id, "a version record can't be null"); + if (string.IsNullOrWhiteSpace(version.Version)) + return this.GetFormatError(id, $"all version records must have a {nameof(version.Version)} field"); + if (!SemanticVersion.TryParse(version.Version, out _)) + return this.GetFormatError(id, $"invalid mod version '{version.Version}'"); } } // build model return new UpdateManifestModPage(id, manifest); } + + + /********* + ** Private methods + *********/ + /// Get a mod page instance with an error indicating the update manifest is invalid. + /// The full URL to the update manifest. + /// A human-readable reason phrase indicating why it's invalid. + private IModPage GetFormatError(string url, string reason) + { + return new GenericModPage(this.SiteKey, url).SetError(RemoteModStatus.InvalidData, $"The update manifest at {url} is invalid ({reason})"); + } } } From 8a52f4c3def67b9ca38b5dde59ccaeba45522f96 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Mar 2023 13:28:53 -0400 Subject: [PATCH 14/84] update release notes & tweak recent changes --- docs/release-notes.md | 10 ++++++++++ .../Clients/UpdateManifest/UpdateManifestClient.cs | 1 - src/SMAPI/Constants.cs | 6 +++--- src/SMAPI/SMAPI.config.json | 12 ++++++------ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3cc2239db..788296c3f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,16 @@ _If needed, you can update to SMAPI 3.16.0 first and then install the latest version._ --> +## Upcoming release +* For players: + * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). + +* For mod authors: + * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!). + +* For the web UI: + * Fixed uploaded log/JSON file expiry alway shown as renewed. + ## 3.18.6 Released 05 October 2023 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index 1614beab0..270728977 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,4 +1,3 @@ - using System; using System.Diagnostics.CodeAnalysis; using System.Net; diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 91e1f223f..360e91bbf 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -139,11 +139,11 @@ public static string ExecutionPath /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); - /// The file path for the overrides file for , which is applied over it. + /// The file path for the per-user override file, which is applied over it. internal static string ApiUserConfigPath => Path.Combine(Constants.InternalFilesPath, "config.user.json"); - /// The mod-group-specific file path for the overrides file for , which is applied over it. - internal static string ApiModGroupConfigPath => Path.Combine(ModsPath, "config.json"); + /// The file path for the per-mods-folder override file, which is applied over it. + internal static string ApiModGroupConfigPath => Path.Combine(ModsPath, "SMAPI-config.json"); /// The file path for the SMAPI metadata file. internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json"); diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 0d00db4d7..c2cffbde6 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -2,14 +2,14 @@ -This file contains advanced configuration for SMAPI. You generally shouldn't change this file. -The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to log custom changes. +This file has advanced configuration for SMAPI. +Don't edit this file directly! It will be reset each time you update or reinstall SMAPI. -This file is overwritten each time you update or reinstall SMAPI. To avoid losing custom settings, -create a 'config.user.json' file in the same folder with *only* the settings you want to change. -That file won't be overwritten, and any settings in it will override the default options. Don't -copy all the settings, or you may cause bugs due to overridden changes in future SMAPI versions. +Instead create a `smapi-internal/config.user.json` or `Mods/SMAPI-config.json` file with *only* the +settings you want to change. That file won't be overwritten, and any settings in it will override +the default options. Don't copy all the settings, or you may cause bugs due to overridden changes +in future SMAPI versions. From affc86fdd74ea3af3fd175f95fcc60037a2237ed Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 Nov 2022 20:05:04 -0500 Subject: [PATCH 15/84] update game version --- docs/release-notes.md | 4 ++++ src/SMAPI/Constants.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 788296c3f..e992461e5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,10 @@ _If needed, you can update to SMAPI 3.16.0 first and then install the latest version._ --> +## Upcoming release for Stardew Valley 1.6 +* For players: + * Updated for Stardew Valley 1.6. + ## Upcoming release * For players: * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 360e91bbf..0d743d7c0 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -68,10 +68,10 @@ public static class Constants public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.6.0"); /// The maximum supported version of Stardew Valley, if any. - public static ISemanticVersion? MaximumGameVersion { get; } = new GameVersion("1.5.6"); + public static ISemanticVersion? MaximumGameVersion { get; } = null; /// The target game platform. public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform; From fedcb6bd8af0b6d8313ea1a8d7fe3239887affc4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 13 Nov 2022 22:13:57 -0500 Subject: [PATCH 16/84] update to .NET 6 --- build/unix/prepare-install-package.sh | 2 +- build/windows/prepare-install-package.ps1 | 2 +- docs/release-notes.md | 3 +++ src/SMAPI.Installer/SMAPI.Installer.csproj | 2 +- src/SMAPI.Installer/assets/runtimeconfig.json | 4 ++-- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- .../SMAPI.Mods.ConsoleCommands.csproj | 2 +- src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj | 2 +- src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj | 2 +- .../SMAPI.Tests.ModApiConsumer.csproj | 2 +- .../SMAPI.Tests.ModApiProvider.csproj | 2 +- src/SMAPI.Tests/SMAPI.Tests.csproj | 2 +- src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs | 2 +- .../SMAPI.Toolkit.CoreInterfaces.csproj | 2 +- .../Framework/Clients/Wiki/ChangeDescriptor.cs | 2 +- src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 2 +- src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs | 2 +- src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs | 2 +- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- src/SMAPI.Toolkit/SemanticVersion.cs | 6 +++--- src/SMAPI.Toolkit/Serialization/JsonHelper.cs | 2 +- src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 2 +- .../Serialization/Models/ManifestContentPackFor.cs | 2 +- .../Serialization/Models/ManifestDependency.cs | 2 +- src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 6 +++--- src/SMAPI/Framework/SCore.cs | 3 +-- src/SMAPI/SMAPI.csproj | 2 +- src/SMAPI/SemanticVersion.cs | 2 +- 28 files changed, 35 insertions(+), 33 deletions(-) diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 3e33c2770..033994bcb 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -69,7 +69,7 @@ for folder in ${folders[@]}; do for modName in ${bundleModNames[@]}; do echo "Compiling $modName for $folder..." echo "-------------------------------------------------" - dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" + dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained false echo "" echo "" done diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 434d24663..ee27537ac 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -85,7 +85,7 @@ foreach ($folder in $folders) { foreach ($modName in $bundleModNames) { echo "Compiling $modName for $folder..." echo "-------------------------------------------------" - dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" + dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained false echo "" echo "" } diff --git a/docs/release-notes.md b/docs/release-notes.md index e992461e5..921a37966 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,9 @@ * For players: * Updated for Stardew Valley 1.6. +* For mod authors: + * Updated to .NET 6. + ## Upcoming release * For players: * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). diff --git a/src/SMAPI.Installer/SMAPI.Installer.csproj b/src/SMAPI.Installer/SMAPI.Installer.csproj index 928e5c187..4d4d0214c 100644 --- a/src/SMAPI.Installer/SMAPI.Installer.csproj +++ b/src/SMAPI.Installer/SMAPI.Installer.csproj @@ -2,7 +2,7 @@ StardewModdingAPI.Installer The SMAPI installer for players. - net5.0 + net6.0 Exe false diff --git a/src/SMAPI.Installer/assets/runtimeconfig.json b/src/SMAPI.Installer/assets/runtimeconfig.json index bd6a5240f..8741544fe 100644 --- a/src/SMAPI.Installer/assets/runtimeconfig.json +++ b/src/SMAPI.Installer/assets/runtimeconfig.json @@ -1,10 +1,10 @@ { "runtimeOptions": { - "tfm": "net5.0", + "tfm": "net6.0", "includedFrameworks": [ { "name": "Microsoft.NETCore.App", - "version": "5.0.0", + "version": "6.0.0", "rollForward": "latestMinor" } ], diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index 86d35e1c0..c63935c25 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 latest diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index e3db8b477..26574eeb7 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -2,7 +2,7 @@ ConsoleCommands StardewModdingAPI.Mods.ConsoleCommands - net5.0 + net6.0 false diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj index 53c37e97d..77f912153 100644 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj @@ -2,7 +2,7 @@ ErrorHandler StardewModdingAPI.Mods.ErrorHandler - net5.0 + net6.0 false diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj index a8b0dfdbc..03130ff72 100644 --- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj @@ -2,7 +2,7 @@ SaveBackup StardewModdingAPI.Mods.SaveBackup - net5.0 + net6.0 false diff --git a/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj b/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj index 7fef4ebdd..b4b1a14ec 100644 --- a/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj +++ b/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 diff --git a/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj b/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj index 7fef4ebdd..b4b1a14ec 100644 --- a/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj +++ b/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index ea024f0d8..6c4e930b0 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index dc226b7c4..555806c6b 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -29,7 +29,7 @@ public interface ISemanticVersion : IComparable, IEquatableWhether this is a prerelease version. -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [MemberNotNullWhen(true, nameof(ISemanticVersion.PrereleaseTag))] #endif bool IsPrerelease(); diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj index 4c92b4dbe..327f8ed96 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj +++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj @@ -2,7 +2,7 @@ StardewModdingAPI Provides toolkit interfaces which are available to SMAPI mods. - net5.0; netstandard2.0 + net6.0; netstandard2.0 true diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs index a2497dea7..f4f62b4c1 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs @@ -48,7 +48,7 @@ public ChangeDescriptor(ISet add, ISet remove, IReadOnlyDictiona /// Apply the change descriptors to a comma-delimited field. /// The raw field text. /// Returns the modified field. -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [return: NotNullIfNotNull("rawField")] #endif public string? ApplyToCopy(string? rawField) diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index fc50125fd..586f4b671 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -51,7 +51,7 @@ public class WikiModEntry public WikiCompatibilityInfo? BetaCompatibility { get; } /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [MemberNotNullWhen(true, nameof(WikiModEntry.BetaCompatibility))] #endif public bool HasBetaInfo => this.BetaCompatibility != null; diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs index 939be7715..fd17820b0 100644 --- a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -106,7 +106,7 @@ private static bool TryParseLiteral(char[] raw, ref int index, char ch) /// The index of the next character to read. /// The parsed tag. private static bool TryParseTag(char[] raw, ref int index, -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [NotNullWhen(true)] #endif out string? tag diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 3e8064fdf..6cf1c6d09 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -22,7 +22,7 @@ public class UpdateKey : IEquatable public string? Subkey { get; } /// Whether the update key seems to be valid. -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [MemberNotNullWhen(true, nameof(UpdateKey.ID))] #endif public bool LooksValid { get; } diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index a915957e3..7fc1f30bc 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -2,7 +2,7 @@ StardewModdingAPI.Toolkit A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods. - net5.0; netstandard2.0 + net6.0; netstandard2.0 true diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 3713758f8..19861cca0 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -119,7 +119,7 @@ public bool Equals(ISemanticVersion? other) } /// -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [MemberNotNullWhen(true, nameof(SemanticVersion.PrereleaseTag))] #endif public bool IsPrerelease() @@ -202,7 +202,7 @@ public bool IsNonStandard() /// The parsed representation. /// Returns whether parsing the version succeeded. public static bool TryParse(string? version, -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [NotNullWhen(true)] #endif out ISemanticVersion? parsed @@ -217,7 +217,7 @@ out ISemanticVersion? parsed /// The parsed representation. /// Returns whether parsing the version succeeded. public static bool TryParse(string? version, bool allowNonStandard, -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [NotNullWhen(true)] #endif out ISemanticVersion? parsed diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs index a5d7e2e89..208cd6567 100644 --- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -44,7 +44,7 @@ public static JsonSerializerSettings CreateDefaultSettings() /// The given is empty or invalid. /// The file contains invalid JSON. public bool ReadJsonFileIfExists(string fullPath, -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [NotNullWhen(true)] #endif out TModel? result diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 8a449f0a7..fe514f554 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -117,7 +117,7 @@ internal void OverrideUpdateKeys(params string[] updateKeys) /// Normalize a manifest field to strip newlines, trim whitespace, and optionally strip square brackets. /// The input to strip. /// Whether to replace square brackets with round ones. This is used in the mod name to avoid breaking the log format. -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [return: NotNullIfNotNull("input")] #endif private string? NormalizeField(string? input, bool replaceSquareBrackets = false) diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs index f7dc8aa80..fe425d3c5 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs @@ -33,7 +33,7 @@ public ManifestContentPackFor(string uniqueId, ISemanticVersion? minimumVersion) *********/ /// Normalize whitespace in a raw string. /// The input to strip. -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [return: NotNullIfNotNull("input")] #endif private string? NormalizeWhitespace(string? input) diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs index fa254ea7c..9d108a789 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs @@ -54,7 +54,7 @@ public ManifestDependency(string uniqueID, ISemanticVersion? minimumVersion, boo *********/ /// Normalize whitespace in a raw string. /// The input to strip. -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [return: NotNullIfNotNull("input")] #endif private string? NormalizeWhitespace(string? input) diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 136279f2d..f397ffcd5 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -50,7 +50,7 @@ public static string[] GetSegments(string? path, int? limit = null) /// Normalize an asset name to match how MonoGame's content APIs would normalize and cache it. /// The asset name to normalize. [Pure] -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [return: NotNullIfNotNull("assetName")] #endif public static string? NormalizeAssetName(string? assetName) @@ -66,7 +66,7 @@ public static string[] GetSegments(string? path, int? limit = null) /// The file path to normalize. /// This should only be used for file paths. For asset names, use instead. [Pure] -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [return: NotNullIfNotNull("path")] #endif public static string? NormalizePath(string? path) @@ -105,7 +105,7 @@ public static string[] GetSegments(string? path, int? limit = null) [Pure] public static string GetRelativePath(string sourceDir, string targetPath) { -#if NET5_0 +#if NET6_0_OR_GREATER return Path.GetRelativePath(sourceDir, targetPath); #else // NOTE: diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 0207e832a..68e963937 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Reflection; -using System.Runtime.ExceptionServices; using System.Security; using System.Text; using System.Threading; @@ -233,7 +232,7 @@ public SCore(string modsPath, bool writeToConsole, bool? developerMode) } /// Launch SMAPI. - [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions + [SecurityCritical] public void RunInteractively() { // initialize SMAPI diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 2235623dd..f86509be8 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -3,7 +3,7 @@ StardewModdingAPI StardewModdingAPI The modding API for Stardew Valley. - net5.0 + net6.0 x64 Exe true diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 81e526e72..ad44d83c4 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -85,7 +85,7 @@ internal SemanticVersion(ISemanticVersion version) } /// -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER [MemberNotNullWhen(true, nameof(SemanticVersion.PrereleaseTag))] #endif public bool IsPrerelease() From 77ea201e85aba8f634d8033488182188d3808dc8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 12 Nov 2022 20:04:12 -0500 Subject: [PATCH 17/84] remove deprecated APIs --- build/common.targets | 4 - docs/release-notes.md | 10 +- docs/technical/smapi.md | 1 - src/SMAPI.Installer/InteractiveInstaller.cs | 127 +-------- src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 8 - .../ModPatches/PyTkPatcher.cs | 81 ------ .../Framework/ModData/ModWarning.cs | 27 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- src/SMAPI/Constants.cs | 22 -- src/SMAPI/Framework/Content/AssetInfo.cs | 49 ---- .../Content/AssetInterceptorChange.cs | 106 -------- src/SMAPI/Framework/ContentCoordinator.cs | 229 +--------------- .../ContentManagers/GameContentManager.cs | 38 +-- .../ContentManagers/ModContentManager.cs | 43 +-- src/SMAPI/Framework/ContentPack.cs | 17 -- src/SMAPI/Framework/Logging/LogManager.cs | 10 +- .../Framework/ModHelpers/CommandHelper.cs | 19 -- .../Framework/ModHelpers/ContentHelper.cs | 253 ------------------ src/SMAPI/Framework/ModHelpers/ModHelper.cs | 53 +--- .../Framework/ModLoading/AssemblyLoader.cs | 42 --- .../Finders/LegacyAssemblyFinder.cs | 51 ---- .../ModLoading/InstructionHandleResult.cs | 13 +- src/SMAPI/Framework/SCore.cs | 153 +---------- src/SMAPI/GameFramework.cs | 10 - src/SMAPI/IAssetEditor.cs | 23 -- src/SMAPI/IAssetInfo.cs | 25 -- src/SMAPI/IAssetLoader.cs | 23 -- src/SMAPI/ICommandHelper.cs | 9 - src/SMAPI/IContentHelper.cs | 84 ------ src/SMAPI/IContentPack.cs | 22 -- src/SMAPI/IModHelper.cs | 9 - src/SMAPI/Metadata/InstructionMetadata.cs | 5 - src/SMAPI/Utilities/PerScreen.cs | 21 +- 33 files changed, 25 insertions(+), 1564 deletions(-) delete mode 100644 src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs delete mode 100644 src/SMAPI/Framework/Content/AssetInterceptorChange.cs delete mode 100644 src/SMAPI/Framework/ModHelpers/ContentHelper.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs delete mode 100644 src/SMAPI/IAssetEditor.cs delete mode 100644 src/SMAPI/IAssetLoader.cs delete mode 100644 src/SMAPI/IContentHelper.cs diff --git a/build/common.targets b/build/common.targets index aec6e8791..cba06cbd2 100644 --- a/build/common.targets +++ b/build/common.targets @@ -11,7 +11,6 @@ repo. It imports the other MSBuild files as needed. SMAPI latest $(AssemblySearchPaths);{GAC} - $(DefineConstants);SMAPI_DEPRECATED true @@ -34,13 +33,10 @@ repo. It imports the other MSBuild files as needed. warning | builds | summary | rationale ┄┄┄┄┄┄┄ | ┄┄┄┄┄┄┄┄┄┄ | ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ | ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ CS0436 | all | local type conflicts with imported type | SMAPI needs to use certain low-level code during very early compatibility checks, before it's safe to load any other DLLs. - CS0612 | deprecated | member is obsolete | internal references to deprecated code when deprecated code is enabled. - CS0618 | deprecated | member is obsolete (with message) | internal references to deprecated code when deprecated code is enabled. CA1416 | all | platform code available on all platforms | Compiler doesn't recognize the #if constants used by SMAPI. CS0809 | all | obsolete overload for non-obsolete member | This is deliberate to signal to mods that certain APIs are only implemented for the game and shouldn't be called by mods. NU1701 | all | NuGet package targets older .NET version | All such packages are carefully tested to make sure they do work. --> - $(NoWarn);CS0612;CS0618 $(NoWarn);CS0436;CA1416;CS0809;NU1701 diff --git a/docs/release-notes.md b/docs/release-notes.md index 921a37966..1b9bb3d77 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,18 +1,16 @@ ← [README](README.md) # Release notes - - ## Upcoming release for Stardew Valley 1.6 * For players: * Updated for Stardew Valley 1.6. + * Improved performance. + * Removed support for seamlessly updating from SMAPI 2.11.3 and earlier (released in 2019). + _If needed, you can update to SMAPI 3.18.0 first and then install the latest version._ * For mod authors: * Updated to .NET 6. + * Removed all deprecated APIs. ## Upcoming release * For players: diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index d15911438..cdcb4b1f4 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -64,7 +64,6 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled for Windows; if not set, the code assumes Linux/macOS. Set automatically in `common.targets`. -`SMAPI_DEPRECATED` | Whether to include deprecated code in the build. ## Compile from source code ### Main project diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 22f88f2f6..eaca97643 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -41,7 +41,7 @@ private IEnumerable GetUninstallPaths(DirectoryInfo installDir, Director { string GetInstallPath(string path) => Path.Combine(installDir.FullName, path); - // current files + // installed files yield return GetInstallPath("StardewModdingAPI"); // Linux/macOS only yield return GetInstallPath("StardewModdingAPI.deps.json"); yield return GetInstallPath("StardewModdingAPI.dll"); @@ -54,38 +54,8 @@ private IEnumerable GetUninstallPaths(DirectoryInfo installDir, Director yield return GetInstallPath("smapi-internal"); yield return GetInstallPath("steam_appid.txt"); -#if SMAPI_DEPRECATED - // obsolete - yield return GetInstallPath("libgdiplus.dylib"); // before 3.13 (macOS only) - yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 - yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands) - yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 - yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3-2.5.5 - yield return GetInstallPath("0Harmony.dll"); // moved in 2.8 - yield return GetInstallPath("0Harmony.pdb"); // moved in 2.8 - yield return GetInstallPath("Mono.Cecil.dll"); // moved in 2.8 - yield return GetInstallPath("Newtonsoft.Json.dll"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.config.json"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.crash.marker"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.metadata.json"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.update.marker"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8 - yield return GetInstallPath("StardewModdingAPI-x64.exe"); // before 3.13 - - if (modsDir.Exists) - { - foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) - yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7 - } -#endif - - yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files + // old log files + yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); } /// Handles writing text to the console. @@ -516,11 +486,6 @@ public void Run(string[] args) .Replace(@"""UseScheme"": ""AutoDetect""", $@"""UseScheme"": ""{scheme}"""); File.WriteAllText(paths.ApiConfigPath, text); } - -#if SMAPI_DEPRECATED - // remove obsolete appdata mods - this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir, allowUserInput); -#endif } } Console.WriteLine(); @@ -848,92 +813,6 @@ private IEnumerable DetectGameFolders(ModToolkit toolkit, Install } } -#if SMAPI_DEPRECATED - /// Interactively move mods out of the app data directory. - /// The directory which should contain all mods. - /// The installer directory containing packaged mods. - /// Whether the installer can ask for user input from the terminal. - private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir, bool allowUserInput) - { - // get packaged mods to delete - string[] packagedModNames = packagedModsDir.GetDirectories().Select(p => p.Name).ToArray(); - - // get path - string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); - DirectoryInfo modDir = new(Path.Combine(appDataPath, "Mods")); - - // check if migration needed - if (!modDir.Exists) - return; - this.PrintDebug($"Found an obsolete mod path: {modDir.FullName}"); - this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported)."); - - // move mods if no conflicts (else warn) - foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopy)) - { - // get type - bool isDir = entry is DirectoryInfo; - if (!isDir && entry is not FileInfo) - continue; // should never happen - - // delete packaged mods (newer version bundled into SMAPI) - if (isDir && packagedModNames.Contains(entry.Name, StringComparer.OrdinalIgnoreCase)) - { - this.PrintDebug($" Deleting {entry.Name} because it's bundled into SMAPI..."); - this.InteractivelyDelete(entry.FullName, allowUserInput); - continue; - } - - // check paths - string newPath = Path.Combine(properModsDir.FullName, entry.Name); - if (isDir ? Directory.Exists(newPath) : File.Exists(newPath)) - { - this.PrintWarning($" Can't move {entry.Name} because it already exists in your game's mod directory."); - continue; - } - - // move into mods - this.PrintDebug($" Moving {entry.Name} into the game's mod directory..."); - this.Move(entry, newPath); - } - - // delete if empty - if (modDir.EnumerateFileSystemInfos().Any()) - this.PrintWarning(" You have files in this folder which couldn't be moved automatically. These will be ignored by SMAPI."); - else - { - this.PrintDebug(" Deleted empty directory."); - modDir.Delete(recursive: true); - } - } - - /// Move a filesystem entry to a new parent directory. - /// The filesystem entry to move. - /// The destination path. - /// We can't use or , because those don't work across partitions. - private void Move(FileSystemInfo entry, string newPath) - { - // file - if (entry is FileInfo file) - { - file.CopyTo(newPath); - file.Delete(); - } - - // directory - else - { - Directory.CreateDirectory(newPath); - - DirectoryInfo directory = (DirectoryInfo)entry; - foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopy)) - this.Move(child, Path.Combine(newPath, child.Name)); - - directory.Delete(recursive: true); - } - } -#endif - /// Get whether a file or folder should be copied from the installer files. /// The file or folder info. private bool ShouldCopy(FileSystemInfo entry) diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index 25056b5e4..e1c338656 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -2,9 +2,6 @@ using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Internal.Patching; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Mods.ErrorHandler.ModPatches; -#endif using StardewModdingAPI.Mods.ErrorHandler.Patches; using StardewValley; @@ -42,11 +39,6 @@ public override void Entry(IModHelper helper) new SaveGamePatcher(this.Monitor, this.OnSaveContentRemoved), new SpriteBatchPatcher(), new UtilityPatcher() - -#if SMAPI_DEPRECATED - // mod patches - , new PyTkPatcher(helper.ModRegistry) -#endif ); // hook events diff --git a/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs b/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs deleted file mode 100644 index f084902a5..000000000 --- a/src/SMAPI.Mods.ErrorHandler/ModPatches/PyTkPatcher.cs +++ /dev/null @@ -1,81 +0,0 @@ -#if SMAPI_DEPRECATED -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using HarmonyLib; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Patching; - -// -// This is part of a three-part fix for PyTK 1.23.* and earlier. When removing this, search -// 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher. -// - -namespace StardewModdingAPI.Mods.ErrorHandler.ModPatches -{ - /// Harmony patches for the PyTK mod for compatibility with newer SMAPI versions. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "'Platonymous' is part of the mod ID.")] - internal class PyTkPatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// The PyTK mod metadata, if it's installed. - private static IModMetadata? PyTk; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod registry from which to read PyTK metadata. - public PyTkPatcher(IModRegistry modRegistry) - { - IModMetadata? pyTk = (IModMetadata?)modRegistry.Get(@"Platonymous.Toolkit"); - if (pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.24.0")) - PyTkPatcher.PyTk = pyTk; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - try - { - // get mod info - IModMetadata? pyTk = PyTkPatcher.PyTk; - if (pyTk is null) - return; - - // get patch method - const string patchMethodName = "PatchImage"; - MethodInfo? patch = AccessTools.Method(pyTk.Mod!.GetType(), patchMethodName); - if (patch is null) - { - monitor.Log("Failed applying compatibility patch for PyTK. Its image scaling feature may not work correctly.", LogLevel.Warn); - monitor.Log($"Couldn't find patch method '{pyTk.Mod.GetType().FullName}.{patchMethodName}'."); - return; - } - - // apply patch - harmony = new($"{harmony.Id}.compatibility-patches.PyTK"); - harmony.Patch( - original: AccessTools.Method(typeof(AssetDataForImage), nameof(AssetDataForImage.PatchImage), new[] { typeof(Texture2D), typeof(Rectangle), typeof(Rectangle), typeof(PatchMode) }), - prefix: new HarmonyMethod(patch) - ); - } - catch (Exception ex) - { - monitor.Log("Failed applying compatibility patch for PyTK. Its image scaling feature may not work correctly.", LogLevel.Warn); - monitor.Log(ex.GetLogSummary()); - } - } - } -} -#endif diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs index 4c76f4171..b3082fa2c 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs @@ -18,36 +18,19 @@ public enum ModWarning /// The mod patches the game in a way that may impact stability. PatchesGame = 4, -#if SMAPI_DEPRECATED - /// The mod uses the dynamic keyword which won't work on Linux/macOS. - [Obsolete("This value is no longer used by SMAPI and will be removed in the upcoming SMAPI 4.0.0.")] - UsesDynamic = 8, -#endif - /// The mod references specialized 'unvalidated update tick' events which may impact stability. - UsesUnvalidatedUpdateTick = 16, + UsesUnvalidatedUpdateTick = 8, /// The mod has no update keys set. - NoUpdateKeys = 32, + NoUpdateKeys = 16, /// Uses .NET APIs for reading and writing to the console. - AccessesConsole = 64, + AccessesConsole = 32, /// Uses .NET APIs for filesystem access. - AccessesFilesystem = 128, + AccessesFilesystem = 64, /// Uses .NET APIs for shell or process access. - AccessesShell = 256, - -#if SMAPI_DEPRECATED - /// References the legacy System.Configuration.ConfigurationManager assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. - DetectedLegacyConfigurationDll = 512, - - /// References the legacy System.Runtime.Caching assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. - DetectedLegacyCachingDll = 1024, - - /// References the legacy System.Security.Permissions assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. - DetectedLegacyPermissionsDll = 2048 -#endif + AccessesShell = 128 } } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index b3ea7db21..ae3c0505d 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -18,7 +18,7 @@ LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler"); bool missingErrorHandler = errorHandler is null && log?.OperatingSystem?.Contains("Android Unix", StringComparison.OrdinalIgnoreCase) != true; 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.24.0") is true); + bool isPyTkCompatibilityMode = log?.ApiVersionParsed?.IsBetween("3.15.0", "3.19.0") is true && log.Mods.Any(p => p.IsCodeMod && p.Name == "PyTK" && p.GetParsedVersion()?.IsOlderThan("1.24.0") is true); // get filters IDictionary defaultFilters = Enum diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 0d743d7c0..ee33045b4 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -6,9 +6,6 @@ using Mono.Cecil; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Framework.Deprecations; -#endif using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Utilities; @@ -79,25 +76,6 @@ public static class Constants /// The game framework running the game. public static GameFramework GameFramework { get; } = EarlyConstants.GameFramework; -#if SMAPI_DEPRECATED - /// The path to the game folder. - [Obsolete($"Use {nameof(Constants)}.{nameof(GamePath)} instead. This property will be removed in SMAPI 4.0.0.")] - public static string ExecutionPath - { - get - { - SCore.DeprecationManager.Warn( - source: null, - nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval - ); - - return Constants.GamePath; - } - } -#endif - /// The path to the game folder. public static string GamePath { get; } = EarlyConstants.GamePath; diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index 52ef02e64..30dc325a5 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Framework.Deprecations; -#endif namespace StardewModdingAPI.Framework.Content { @@ -34,30 +31,6 @@ internal class AssetInfo : IAssetInfo /// public Type DataType { get; } -#if SMAPI_DEPRECATED - /// - [Obsolete($"Use {nameof(AssetInfo.Name)} or {nameof(AssetInfo.NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")] - public string AssetName - { - get - { - SCore.DeprecationManager.Warn( - source: null, - nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval, - unlessStackIncludes: new[] - { - $"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}", - $"{typeof(ContentCoordinator).FullName}.{nameof(ContentCoordinator.GetAssetOperations)}" - } - ); - - return this.NameWithoutLocale.Name; - } - } -#endif - /********* ** Public methods @@ -75,28 +48,6 @@ public AssetInfo(string? locale, IAssetName assetName, Type type, Func - [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(AssetInfo.NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")] - public bool AssetNameEquals(string path) - { - SCore.DeprecationManager.Warn( - source: null, - nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval, - unlessStackIncludes: new[] - { - $"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}", - $"{typeof(ContentCoordinator).FullName}.{nameof(ContentCoordinator.GetAssetOperations)}" - } - ); - - - return this.NameWithoutLocale.IsEquivalentTo(path); - } -#endif - /********* ** Protected methods diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs deleted file mode 100644 index 3b5068dca..000000000 --- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs +++ /dev/null @@ -1,106 +0,0 @@ -#if SMAPI_DEPRECATED -using System; -using System.Reflection; -using StardewModdingAPI.Internal; - -namespace StardewModdingAPI.Framework.Content -{ - /// A wrapper for and for internal cache invalidation. - internal class AssetInterceptorChange - { - /********* - ** Accessors - *********/ - /// The mod which registered the interceptor. - public IModMetadata Mod { get; } - - /// The interceptor instance. - public object Instance { get; } - - /// Whether the asset interceptor was added since the last tick. Mutually exclusive with . - public bool WasAdded { get; } - - /// Whether the asset interceptor was removed since the last tick. Mutually exclusive with . - public bool WasRemoved => this.WasAdded; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod registering the interceptor. - /// The interceptor. This must be an or instance. - /// Whether the asset interceptor was added since the last tick; else removed. - public AssetInterceptorChange(IModMetadata mod, object instance, bool wasAdded) - { - this.Mod = mod ?? throw new ArgumentNullException(nameof(mod)); - this.Instance = instance ?? throw new ArgumentNullException(nameof(instance)); - this.WasAdded = wasAdded; - - if (instance is not (IAssetEditor or IAssetLoader)) - throw new InvalidCastException($"The provided {nameof(instance)} value must be an {nameof(IAssetEditor)} or {nameof(IAssetLoader)} instance."); - } - - /// Get whether this instance can intercept the given asset. - /// Basic metadata about the asset being loaded. - public bool CanIntercept(IAssetInfo asset) - { - MethodInfo? canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic); - if (canIntercept == null) - throw new InvalidOperationException($"SMAPI couldn't access the {nameof(AssetInterceptorChange)}.{nameof(this.CanInterceptImpl)} implementation."); - - return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset })!; - } - - - /********* - ** Private methods - *********/ - /// Get whether this instance can intercept the given asset. - /// The asset type. - /// Basic metadata about the asset being loaded. - private bool CanInterceptImpl(IAssetInfo asset) - { - // check edit - if (this.Instance is IAssetEditor editor) - { - Context.HeuristicModsRunningCode.Push(this.Mod); - try - { - if (editor.CanEdit(asset)) - return true; - } - catch (Exception ex) - { - this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - finally - { - Context.HeuristicModsRunningCode.TryPop(out _); - } - } - - // check load - if (this.Instance is IAssetLoader loader) - { - Context.HeuristicModsRunningCode.Push(this.Mod); - try - { - if (loader.CanLoad(asset)) - return true; - } - catch (Exception ex) - { - this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - finally - { - Context.HeuristicModsRunningCode.TryPop(out _); - } - } - - return false; - } - } -} -#endif diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 86415a5f0..71495d7e2 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -12,9 +12,6 @@ using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Internal; -#endif using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities.PathLookups; @@ -83,16 +80,6 @@ internal class ContentCoordinator : IDisposable /// The cached asset load/edit operations to apply, indexed by asset name. private readonly TickCacheDictionary AssetOperationsByKey = new(); -#if SMAPI_DEPRECATED - /// A cache of asset operation groups created for legacy implementations. - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - private readonly Dictionary> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance); - - /// A cache of asset operation groups created for legacy implementations. - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - private readonly Dictionary> LegacyEditorCache = new(ReferenceEqualityComparer.Instance); -#endif - /********* ** Accessors @@ -103,16 +90,6 @@ internal class ContentCoordinator : IDisposable /// The current language as a constant. public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; -#if SMAPI_DEPRECATED - /// Interceptors which provide the initial versions of matching assets. - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - public IList> Loaders { get; } = new List>(); - - /// Interceptors which edit matching assets after they're loaded. - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - public IList> Editors { get; } = new List>(); -#endif - /// The absolute path to the . public string FullRootDirectory { get; } @@ -267,9 +244,9 @@ public void OnReturningToTitleScreen() { // The game clears LocalizedContentManager.localizedAssetNames after returning to the title screen. That // causes an inconsistency in the SMAPI asset cache, which leads to an edge case where assets already - // provided by mods via IAssetLoader when playing in non-English are ignored. + // provided by mods via a load operation when playing in non-English are ignored. // - // For example, let's say a mod provides the 'Data\mail' asset through IAssetLoader when playing in + // For example, let's say a mod provides the 'Data\mail' asset via a load operation when playing in // Portuguese. Here's the normal load process after it's loaded: // 1. The game requests Data\mail. // 2. SMAPI sees that it's already cached, and calls LoadRaw to bypass asset interception. @@ -499,25 +476,13 @@ out bool updatedWarpRoutes return invalidatedAssets.Keys; } -#if SMAPI_DEPRECATED - /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now. - /// The asset type. - /// The asset info to load or edit. - public AssetOperationGroup? GetAssetOperations(IAssetInfo info) - where T : notnull -#else /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now. /// The asset info to load or edit. public AssetOperationGroup? GetAssetOperations(IAssetInfo info) -#endif { return this.AssetOperationsByKey.GetOrSet( info.Name, -#if SMAPI_DEPRECATED - () => this.GetAssetOperationsWithoutCache(info) -#else () => this.RequestAssetOperations(info) -#endif ); } @@ -639,195 +604,5 @@ private bool TryLoadVanillaAsset(string assetName, [NotNullWhen(true)] out T? return map; } - -#if SMAPI_DEPRECATED - /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now, ignoring the cache. - /// The asset type. - /// The asset info to load or edit. - private AssetOperationGroup? GetAssetOperationsWithoutCache(IAssetInfo info) - where T : notnull - { - // new content API - AssetOperationGroup? group = this.RequestAssetOperations(info); - - // legacy load operations - if (this.Editors.Count > 0 || this.Loaders.Count > 0) - { - IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info); - - foreach (ModLinked loader in this.Loaders) - { - // check if loader applies - Context.HeuristicModsRunningCode.Push(loader.Mod); - try - { - if (!loader.Data.CanLoad(legacyInfo)) - continue; - } - catch (Exception ex) - { - loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - finally - { - Context.HeuristicModsRunningCode.TryPop(out _); - } - - // add operation - group ??= new AssetOperationGroup(new List(), new List()); - group.LoadOperations.Add( - this.GetOrCreateLegacyOperation( - cache: this.LegacyLoaderCache, - editor: loader.Data, - dataType: info.DataType, - create: () => new AssetLoadOperation( - Mod: loader.Mod, - OnBehalfOf: null, - Priority: AssetLoadPriority.Exclusive, - GetData: assetInfo => loader.Data.Load(this.GetLegacyAssetInfo(assetInfo)) - ) - ) - ); - } - - // legacy edit operations - foreach (var editor in this.Editors) - { - // check if editor applies - Context.HeuristicModsRunningCode.Push(editor.Mod); - try - { - if (!editor.Data.CanEdit(legacyInfo)) - continue; - } - catch (Exception ex) - { - editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - finally - { - Context.HeuristicModsRunningCode.TryPop(out _); - } - - // HACK - // - // If two editors have the same priority, they're applied in registration order (so - // whichever was registered first is applied first). Mods often depend on this - // behavior, like Json Assets registering its interceptors before Content Patcher. - // - // Unfortunately the old & new content APIs have separate lists, so new-API - // interceptors always ran before old-API interceptors with the same priority, - // regardless of the registration order *between* APIs. Since the new API works in - // a fundamentally different way (i.e. loads/edits are defined on asset request - // instead of by registering a global 'editor' or 'loader' class), there's no way - // to track registration order between them. - // - // Until we drop the old content API in SMAPI 4.0.0, this sets the priority for - // specific legacy editors to maintain compatibility. - AssetEditPriority priority = editor.Data.GetType().FullName switch - { - "JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher - _ => AssetEditPriority.Default - }; - - // add operation - group ??= new AssetOperationGroup(new List(), new List()); - group.EditOperations.Add( - this.GetOrCreateLegacyOperation( - cache: this.LegacyEditorCache, - editor: editor.Data, - dataType: info.DataType, - create: () => new AssetEditOperation( - Mod: editor.Mod, - OnBehalfOf: null, - Priority: priority, - ApplyEdit: assetData => editor.Data.Edit(this.GetLegacyAssetData(assetData)) - ) - ) - ); - } - } - - return group; - } - - /// Get a cached asset operation group for a legacy or instance, creating it if needed. - /// The editor type (one of or ). - /// The operation model type. - /// The cached operation groups for the interceptor type. - /// The legacy asset interceptor. - /// The asset data type. - /// Create the asset operation group if it's not cached yet. - private TOperation GetOrCreateLegacyOperation(Dictionary> cache, TInterceptor editor, Type dataType, Func create) - where TInterceptor : class - { - if (!cache.TryGetValue(editor, out Dictionary? cacheByType)) - cache[editor] = cacheByType = new Dictionary(); - - if (!cacheByType.TryGetValue(dataType, out TOperation? operation)) - cacheByType[dataType] = operation = create(); - - return operation; - } - - /// Get an asset info compatible with legacy and instances, which always expect the base name. - /// The asset info. - private IAssetInfo GetLegacyAssetInfo(IAssetInfo asset) - { - return new AssetInfo( - locale: this.GetLegacyLocale(asset), - assetName: this.GetLegacyAssetName(asset.Name), - type: asset.DataType, - getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName - ); - } - - /// Get an asset data compatible with legacy and instances, which always expect the base name. - /// The asset data. - private IAssetData GetLegacyAssetData(IAssetData asset) - { - return new AssetDataForObject( - locale: this.GetLegacyLocale(asset), - assetName: this.GetLegacyAssetName(asset.Name), - data: asset.Data, - getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName, - reflection: this.Reflection, - onDataReplaced: asset.ReplaceWith - ); - } - - /// Get the value compatible with legacy and instances, which expect the locale to default to the current game locale or an empty string. - /// The non-legacy asset info to map. - private string GetLegacyLocale(IAssetInfo asset) - { - return asset.Locale ?? this.GetLocale(); - } - - /// Get an asset name compatible with legacy and instances, which always expect the base name. - /// The asset name to map. - /// Returns the legacy asset name if needed, or the if no change is needed. - private IAssetName GetLegacyAssetName(IAssetName asset) - { - // strip _international suffix - const string internationalSuffix = "_international"; - if (asset.Name.EndsWith(internationalSuffix)) - { - return new AssetName( - baseName: asset.Name[..^internationalSuffix.Length], - localeCode: null, - languageCode: null - ); - } - - // else strip locale - if (asset.LocaleCode != null) - return new AssetName(asset.BaseName, null, null); - - // else no change needed - return asset; - } -#endif } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index df7bdc59e..4d92db0f6 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -8,7 +8,6 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.Deprecations; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; @@ -25,7 +24,7 @@ internal class GameContentManager : BaseContentManager /********* ** Fields *********/ - /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. private readonly ContextHash AssetsBeingLoaded = new(); /// Whether the next load is the first for any game content manager. @@ -76,11 +75,7 @@ public override bool DoesAssetExist(IAssetName assetName) // custom asset from a loader string locale = this.GetLocale(); IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName); - AssetOperationGroup? operations = this.Coordinator.GetAssetOperations -#if SMAPI_DEPRECATED - -#endif - (info); + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(info); if (operations?.LoadOperations.Count > 0) { if (!this.AssertMaxOneRequiredLoader(info, operations.LoadOperations, out string? error)) @@ -133,11 +128,7 @@ public override T LoadExact(IAssetName assetName, bool useCache) data = this.AssetsBeingLoaded.Track(assetName.Name, () => { IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName); - AssetOperationGroup? operations = this.Coordinator.GetAssetOperations -#if SMAPI_DEPRECATED - -#endif - (info); + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(info); IAssetData asset = this.ApplyLoader(info, operations?.LoadOperations) ?? new AssetDataForObject(info, this.RawLoad(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection); @@ -302,11 +293,7 @@ private bool AssertMaxOneRequiredLoader(IAssetInfo info, List(IAssetInfo info, [NotNullWhen(true) // handle mismatch if (loadedMap.TileSheets.Count <= vanillaSheet.Index || loadedMap.TileSheets[vanillaSheet.Index].Id != vanillaSheet.Id) { -#if SMAPI_DEPRECATED - // only show warning if not farm map - // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. - bool isFarmMap = info.Name.IsEquivalentTo("Maps/Farm") || info.Name.IsEquivalentTo("Maps/Farm_Combat") || info.Name.IsEquivalentTo("Maps/Farm_Fishing") || info.Name.IsEquivalentTo("Maps/Farm_Foraging") || info.Name.IsEquivalentTo("Maps/Farm_FourCorners") || info.Name.IsEquivalentTo("Maps/Farm_Island") || info.Name.IsEquivalentTo("Maps/Farm_Mining"); - - string reason = $"{this.GetOnBehalfOfLabel(loader.OnBehalfOf, parenthetical: false) ?? "mod"} reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."; - - SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); - if (isFarmMap) - { - mod.LogAsMod($"SMAPI blocked a '{info.Name}' map load: {reason}", LogLevel.Error); - return false; - } - - mod.LogAsMod($"SMAPI found an issue with a '{info.Name}' map load: {reason}", LogLevel.Warn); -#else mod.LogAsMod($"SMAPI found an issue with a '{info.Name}' map load: {this.GetOnBehalfOfLabel(loader.OnBehalfOf, parenthetical: false) ?? "mod"} reordered the original tilesheets, which often causes crashes.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help.", LogLevel.Error); return false; -#endif } } } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 8cdb65eb9..dd4061acf 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -46,15 +46,6 @@ internal sealed class ModContentManager : BaseContentManager private static readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" }; - /********* - ** Accessors - *********/ -#if SMAPI_DEPRECATED - /// Whether to enable legacy compatibility mode for PyTK scale-up textures. - internal static bool EnablePyTkLegacyMode; -#endif - - /********* ** Public methods *********/ @@ -201,16 +192,6 @@ private T LoadImageFile(IAssetName assetName, FileInfo file) this.AssertValidType(assetName, file, typeof(Texture2D), typeof(IRawTextureData)); bool returnRawData = typeof(T).IsAssignableTo(typeof(IRawTextureData)); -#if SMAPI_DEPRECATED - if (!returnRawData && this.ShouldDisableIntermediateRawDataLoad(assetName, file)) - { - using FileStream stream = File.OpenRead(file.FullName); - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream).SetName(assetName); - this.PremultiplyTransparency(texture); - return (T)(object)texture; - } -#endif - IRawTextureData raw = this.LoadRawImageData(file, returnRawData); if (returnRawData) @@ -223,28 +204,6 @@ private T LoadImageFile(IAssetName assetName, FileInfo file) } } -#if SMAPI_DEPRECATED - /// Get whether to disable loading an image as before building a instance. This isn't called if the mod requested directly. - /// The type of asset being loaded. - /// The asset name relative to the loader root directory. - /// The file being loaded. - private bool ShouldDisableIntermediateRawDataLoad(IAssetName assetName, FileInfo file) - { - // disable raw data if PyTK will rescale the image (until it supports raw data) - if (ModContentManager.EnablePyTkLegacyMode) - { - // PyTK intercepts Texture2D file loads to rescale them (e.g. for HD portraits), - // but doesn't support IRawTextureData loads yet. We can't just check if the - // current file has a '.pytk.json' rescale file though, since PyTK may still - // rescale it if the original asset or another edit gets rescaled. - this.Monitor.LogOnce("Enabled compatibility mode for PyTK 1.23.* or earlier. This won't cause any issues, but may impact performance. This will no longer be supported in the upcoming SMAPI 4.0.0.", LogLevel.Warn); - return true; - } - - return false; - } -#endif - /// Load the raw image data from a file on disk. /// The file whose data to load. /// Whether the data is being loaded for an (true) or (false) instance. @@ -523,7 +482,7 @@ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relati // ignore file-not-found errors // TODO: while it's useful to suppress an asset-not-found error here to avoid // confusion, this is a pretty naive approach. Even if the file doesn't exist, - // the file may have been loaded through an IAssetLoader which failed. So even + // the file may have been loaded through a load operation which failed. So even // if the content file doesn't exist, that doesn't mean the error here is a // content-not-found error. Unfortunately XNA doesn't provide a good way to // detect the error type. diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index a1d977e4a..b6d0c07eb 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -96,23 +96,6 @@ public void WriteJsonFile(string path, TModel data) where TModel : class } } -#if SMAPI_DEPRECATED - /// - [Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.Load)} instead. This method will be removed in SMAPI 4.0.0.")] - public T LoadAsset(string key) - where T : notnull - { - return this.ModContent.Load(key); - } - - /// - [Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.GetInternalAssetName)} instead. This method will be removed in SMAPI 4.0.0.")] - public string GetActualAssetKey(string key) - { - return this.ModContent.GetInternalAssetName(key).Name; - } -#endif - /********* ** Private methods diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index ffffc9c71..d5b332899 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -269,11 +269,7 @@ public void LogFatalLaunchError(Exception exception) public void LogIntro(string modsPath, IDictionary customSettings) { // log platform - this.Monitor.Log($"SMAPI {Constants.ApiVersion} " -#if !SMAPI_DEPRECATED - + "(strict mode) " -#endif - + $"with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); // log basic info this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); @@ -284,10 +280,6 @@ public void LogIntro(string modsPath, IDictionary customSetting // log custom settings if (customSettings.Any()) this.Monitor.Log($"Loaded with custom settings: {string.Join(", ", customSettings.OrderBy(p => p.Key).Select(p => $"{p.Key}: {p.Value}"))}"); - -#if !SMAPI_DEPRECATED - this.Monitor.Log("SMAPI is running in 'strict mode', which removes all deprecated APIs. This can significantly improve performance, but some mods may not work. You can reinstall SMAPI to disable it if you run into problems.", LogLevel.Info); -#endif } /// Log details for settings that don't match the default. diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs index 90edc137c..d3c5a1f93 100644 --- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs @@ -1,7 +1,4 @@ using System; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Framework.Deprecations; -#endif namespace StardewModdingAPI.Framework.ModHelpers { @@ -33,21 +30,5 @@ public ICommandHelper Add(string name, string documentation, Action - [Obsolete("Use mod-provided APIs to integrate with mods instead. This method will be removed in SMAPI 4.0.0.")] - public bool Trigger(string name, string[] arguments) - { - SCore.DeprecationManager.Warn( - source: this.Mod, - nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}", - version: "3.8.1", - severity: DeprecationLevel.PendingRemoval - ); - - return this.CommandManager.Trigger(name, arguments); - } -#endif } } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs deleted file mode 100644 index 152b264c4..000000000 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ /dev/null @@ -1,253 +0,0 @@ -#if SMAPI_DEPRECATED -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.IO; -using System.Linq; -using StardewModdingAPI.Framework.Content; -using StardewModdingAPI.Framework.ContentManagers; -using StardewModdingAPI.Framework.Deprecations; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Reflection; -using StardewValley; - -namespace StardewModdingAPI.Framework.ModHelpers -{ - /// Provides an API for loading content assets. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.GameContent)} or {nameof(IMod.Helper)}.{nameof(IModHelper.ModContent)} instead. This interface will be removed in SMAPI 4.0.0.")] - internal class ContentHelper : BaseHelper, IContentHelper - { - /********* - ** Fields - *********/ - /// SMAPI's core content logic. - private readonly ContentCoordinator ContentCore; - - /// A content manager for this mod which manages files from the game's Content folder. - private readonly IContentManager GameContentManager; - - /// A content manager for this mod which manages files from the mod's folder. - private readonly ModContentManager ModContentManager; - - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; - - /// Simplifies access to private code. - private readonly Reflector Reflection; - - - /********* - ** Accessors - *********/ - /// - public string CurrentLocale => this.GameContentManager.GetLocale(); - - /// - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; - - /// The observable implementation of . - internal ObservableCollection ObservableAssetEditors { get; } = new(); - - /// The observable implementation of . - internal ObservableCollection ObservableAssetLoaders { get; } = new(); - - /// - public IList AssetLoaders - { - get - { - SCore.DeprecationManager.Warn( - source: this.Mod, - nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval - ); - - return this.ObservableAssetLoaders; - } - } - - /// - public IList AssetEditors - { - get - { - SCore.DeprecationManager.Warn( - source: this.Mod, - nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval - ); - - return this.ObservableAssetEditors; - } - } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's core content logic. - /// The absolute path to the mod folder. - /// The mod using this instance. - /// Encapsulates monitoring and logging. - /// Simplifies access to private code. - public ContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, IMonitor monitor, Reflector reflection) - : base(mod) - { - string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID); - - this.ContentCore = contentCore; - this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content"); - this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, this.Mod.DisplayName, modFolderPath, this.GameContentManager); - this.Monitor = monitor; - this.Reflection = reflection; - } - - /// - public T Load(string key, ContentSource source = ContentSource.ModFolder) - where T : notnull - { - IAssetName assetName = this.ContentCore.ParseAssetName(key, allowLocales: source == ContentSource.GameContent); - - try - { - this.AssertAndNormalizeAssetName(key); - switch (source) - { - case ContentSource.GameContent: - if (assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)) - { - assetName = this.ContentCore.ParseAssetName(assetName.Name[..^4], allowLocales: true); - SCore.DeprecationManager.Warn( - this.Mod, - "loading assets from the Content folder with a .xnb file extension", - "3.14.0", - DeprecationLevel.Info - ); - } - - return this.GameContentManager.LoadLocalized(assetName, this.CurrentLocaleConstant, useCache: false); - - case ContentSource.ModFolder: - try - { - return this.ModContentManager.LoadExact(assetName, useCache: false); - } - catch (SContentLoadException ex) when (ex.ErrorType == ContentLoadErrorType.AssetDoesNotExist) - { - // legacy behavior: you can load a .xnb file without the file extension - try - { - IAssetName newName = this.ContentCore.ParseAssetName(assetName.Name + ".xnb", allowLocales: false); - if (this.ModContentManager.DoesAssetExist(newName)) - { - T data = this.ModContentManager.LoadExact(newName, useCache: false); - SCore.DeprecationManager.Warn( - this.Mod, - "loading XNB files from the mod folder without the .xnb file extension", - "3.14.0", - DeprecationLevel.Info - ); - return data; - } - } - catch { /* legacy behavior failed, rethrow original error */ } - - throw; - } - - default: - throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); - } - } - catch (Exception ex) when (ex is not SContentLoadException) - { - throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex); - } - } - - /// - [Pure] - public string NormalizeAssetName(string? assetName) - { - return this.ModContentManager.AssertAndNormalizeAssetName(assetName); - } - - /// - public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) - { - switch (source) - { - case ContentSource.GameContent: - return this.GameContentManager.AssertAndNormalizeAssetName(key); - - case ContentSource.ModFolder: - return this.ModContentManager.GetInternalAssetKey(key).Name; - - default: - throw new NotSupportedException($"Unknown content source '{source}'."); - } - } - - /// - public bool InvalidateCache(string key) - { - string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); - this.Monitor.Log($"Requested cache invalidation for '{actualKey}'."); - return this.ContentCore.InvalidateCache(asset => asset.Name.IsEquivalentTo(actualKey)).Any(); - } - - /// - public bool InvalidateCache() - where T : notnull - { - this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible."); - return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any(); - } - - /// - public bool InvalidateCache(Func predicate) - { - this.Monitor.Log("Requested cache invalidation for all assets matching a predicate."); - return this.ContentCore.InvalidateCache(predicate).Any(); - } - - /// - public IAssetData GetPatchHelper(T data, string? assetName = null) - where T : notnull - { - if (data == null) - throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); - - assetName ??= $"temp/{Guid.NewGuid():N}"; - - return new AssetDataForObject( - locale: this.CurrentLocale, - assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/), - data: data, - getNormalizedPath: this.NormalizeAssetName, - reflection: this.Reflection - ); - } - - - /********* - ** Private methods - *********/ - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertAndNormalizeAssetName(string key) - { - this.ModContentManager.AssertAndNormalizeAssetName(key); - if (Path.IsPathRooted(key)) - throw new ArgumentException("The asset key must not be an absolute path."); - } - } -} -#endif diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 531289d00..d1cf357e9 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,9 +1,6 @@ using System; using System.IO; using StardewModdingAPI.Events; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Framework.Deprecations; -#endif using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Framework.ModHelpers @@ -11,16 +8,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Provides simplified APIs for writing mods. internal class ModHelper : BaseHelper, IModHelper, IDisposable { -#if SMAPI_DEPRECATED - /********* - ** Fields - *********/ - /// The backing field for . - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - private readonly ContentHelper ContentImpl; -#endif - - /********* ** Accessors *********/ @@ -30,25 +17,6 @@ internal class ModHelper : BaseHelper, IModHelper, IDisposable /// public IModEvents Events { get; } -#if SMAPI_DEPRECATED - /// - [Obsolete($"Use {nameof(IGameContentHelper)} or {nameof(IModContentHelper)} instead.")] - public IContentHelper Content - { - get - { - SCore.DeprecationManager.Warn( - source: this.Mod, - nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval - ); - - return this.ContentImpl; - } - } -#endif - /// public IGameContentHelper GameContent { get; } @@ -88,7 +56,6 @@ public IContentHelper Content /// The full path to the mod's folder. /// Manages the game's input state for the current player instance. That may not be the main player in split-screen mode. /// Manages access to events raised by SMAPI. - /// An API for loading content assets. /// An API for loading content assets from the game's Content folder or via . /// An API for loading content assets from your mod's files. /// An API for managing content packs. @@ -100,13 +67,7 @@ public IContentHelper Content /// An API for reading translations stored in the mod's i18n folder. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper( - IModMetadata mod, string modDirectory, Func currentInputState, IModEvents events, -#if SMAPI_DEPRECATED - ContentHelper contentHelper, -#endif - IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper - ) + public ModHelper(IModMetadata mod, string modDirectory, Func currentInputState, IModEvents events, IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) : base(mod) { // validate directory @@ -117,9 +78,6 @@ public ModHelper( // initialize this.DirectoryPath = modDirectory; -#if SMAPI_DEPRECATED - this.ContentImpl = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); -#endif this.GameContent = gameContentHelper ?? throw new ArgumentNullException(nameof(gameContentHelper)); this.ModContent = modContentHelper ?? throw new ArgumentNullException(nameof(modContentHelper)); this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper)); @@ -133,15 +91,6 @@ public ModHelper( this.Events = events; } -#if SMAPI_DEPRECATED - /// Get the underlying instance for . - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - public ContentHelper GetLegacyContentHelper() - { - return this.ContentImpl; - } -#endif - /**** ** Mod config file ****/ diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index a4354e57d..17432085d 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -170,31 +170,6 @@ select name this.AssemblyDefinitionResolver.Add(assembly.Definition); } -#if SMAPI_DEPRECATED - // special case: clear legacy-DLL warnings if the mod bundles a copy - if (mod.Warnings.HasFlag(ModWarning.DetectedLegacyCachingDll)) - { - if (File.Exists(Path.Combine(mod.DirectoryPath, "System.Runtime.Caching.dll"))) - mod.RemoveWarning(ModWarning.DetectedLegacyCachingDll); - else - { - // remove duplicate warnings (System.Runtime.Caching.dll references these) - mod.RemoveWarning(ModWarning.DetectedLegacyConfigurationDll); - mod.RemoveWarning(ModWarning.DetectedLegacyPermissionsDll); - } - } - if (mod.Warnings.HasFlag(ModWarning.DetectedLegacyConfigurationDll)) - { - if (File.Exists(Path.Combine(mod.DirectoryPath, "System.Configuration.ConfigurationManager.dll"))) - mod.RemoveWarning(ModWarning.DetectedLegacyConfigurationDll); - } - if (mod.Warnings.HasFlag(ModWarning.DetectedLegacyPermissionsDll)) - { - if (File.Exists(Path.Combine(mod.DirectoryPath, "System.Security.Permissions.dll"))) - mod.RemoveWarning(ModWarning.DetectedLegacyPermissionsDll); - } -#endif - // throw if incompatibilities detected if (!assumeCompatible && mod.Warnings.HasFlag(ModWarning.BrokenCodeLoaded)) throw new IncompatibleInstructionException(); @@ -478,23 +453,6 @@ private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandle mod.SetWarning(ModWarning.AccessesShell); break; -#if SMAPI_DEPRECATED - case InstructionHandleResult.DetectedLegacyCachingDll: - template = $"{logPrefix}Detected reference to System.Runtime.Caching.dll, which will be removed in SMAPI 4.0.0."; - mod.SetWarning(ModWarning.DetectedLegacyCachingDll); - break; - - case InstructionHandleResult.DetectedLegacyConfigurationDll: - template = $"{logPrefix}Detected reference to System.Configuration.ConfigurationManager.dll, which will be removed in SMAPI 4.0.0."; - mod.SetWarning(ModWarning.DetectedLegacyConfigurationDll); - break; - - case InstructionHandleResult.DetectedLegacyPermissionsDll: - template = $"{logPrefix}Detected reference to System.Security.Permissions.dll, which will be removed in SMAPI 4.0.0."; - mod.SetWarning(ModWarning.DetectedLegacyPermissionsDll); - break; -#endif - case InstructionHandleResult.None: break; diff --git a/src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs deleted file mode 100644 index 773809075..000000000 --- a/src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs +++ /dev/null @@ -1,51 +0,0 @@ -#if SMAPI_DEPRECATED -using Mono.Cecil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Detects assembly references which will break in SMAPI 4.0.0. - internal class LegacyAssemblyFinder : BaseInstructionHandler - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public LegacyAssemblyFinder() - : base(defaultPhrase: "legacy assembly references") { } - - - /// - public override bool Handle(ModuleDefinition module) - { - foreach (AssemblyNameReference assembly in module.AssemblyReferences) - { - InstructionHandleResult flag = this.GetFlag(assembly); - if (flag is InstructionHandleResult.None) - continue; - - this.MarkFlag(flag); - } - - return false; - } - - - /********* - ** Private methods - *********/ - /// Get the instruction handle flag for the given assembly reference, if any. - /// The assembly reference. - private InstructionHandleResult GetFlag(AssemblyNameReference assemblyRef) - { - return assemblyRef.Name switch - { - "System.Configuration.ConfigurationManager" => InstructionHandleResult.DetectedLegacyConfigurationDll, - "System.Runtime.Caching" => InstructionHandleResult.DetectedLegacyCachingDll, - "System.Security.Permission" => InstructionHandleResult.DetectedLegacyPermissionsDll, - _ => InstructionHandleResult.None - }; - } - } -} -#endif diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index 189ca64ef..e3f108cb8 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -30,17 +30,6 @@ internal enum InstructionHandleResult DetectedFilesystemAccess, /// The instruction accesses the OS shell or processes directly. - DetectedShellAccess, - -#if SMAPI_DEPRECATED - /// The module references the legacy System.Configuration.ConfigurationManager assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. - DetectedLegacyConfigurationDll, - - /// The module references the legacy System.Runtime.Caching assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. - DetectedLegacyCachingDll, - - /// The module references the legacy System.Security.Permissions assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. - DetectedLegacyPermissionsDll -#endif + DetectedShellAccess } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 68e963937..166d007d1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -30,9 +30,6 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Rendering; using StardewModdingAPI.Framework.Serialization; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Framework.StateTracking.Comparers; -#endif using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; @@ -143,11 +140,6 @@ internal class SCore : IDisposable /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. private readonly Countdown UpdateCrashTimer = new(60); // 60 ticks = roughly one second -#if SMAPI_DEPRECATED - /// Asset interceptors added or removed since the last tick. - private readonly List ReloadAssetInterceptorsQueue = new(); -#endif - /// A list of queued commands to parse and execute. private readonly CommandQueue RawCommandQueue = new(); @@ -544,41 +536,6 @@ private void OnGameUpdating(GameTime gameTime, Action runGameUpdate) this.Monitor.LogOnce("A mod enabled Harmony debug mode, which impacts performance and creates a file on your desktop. SMAPI will try to keep it disabled. (You can allow debug mode by editing the smapi-internal/config.json file.)", LogLevel.Warn); } -#if SMAPI_DEPRECATED - /********* - ** Reload assets when interceptors are added/removed - *********/ - if (this.ReloadAssetInterceptorsQueue.Any()) - { - // get unique interceptors - AssetInterceptorChange[] interceptors = this.ReloadAssetInterceptorsQueue - .GroupBy(p => p.Instance, new ObjectReferenceComparer()) - .Select(p => p.First()) - .ToArray(); - this.ReloadAssetInterceptorsQueue.Clear(); - - // log summary - this.Monitor.Log("Invalidating cached assets for new editors & loaders..."); - this.Monitor.Log( - " changed: " - + string.Join(", ", - interceptors - .GroupBy(p => p.Mod) - .OrderBy(p => p.Key.DisplayName) - .Select(modGroup => - $"{modGroup.Key.DisplayName} (" - + string.Join(", ", modGroup.GroupBy(p => p.WasAdded).ToDictionary(p => p.Key, p => p.Count()).Select(p => $"{(p.Key ? "added" : "removed")} {p.Value}")) - + ")" - ) - ) - + "." - ); - - // reload affected assets - this.ContentCore.InvalidateCache(asset => interceptors.Any(p => p.CanIntercept(asset))); - } -#endif - /********* ** Parse commands *********/ @@ -1718,19 +1675,6 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin // initialize translations this.ReloadTranslations(loaded); - // set temporary PyTK compatibility mode - // This is part of a three-part fix for PyTK 1.23.* and earlier. When removing this, - // search 'Platonymous.Toolkit' to find the other part in SMAPI and Content Patcher. - { - IModInfo? pyTk = this.ModRegistry.Get("Platonymous.Toolkit"); - if (pyTk is not null && pyTk.Manifest.Version.IsOlderThan("1.24.0")) -#if SMAPI_DEPRECATED - ModContentManager.EnablePyTkLegacyMode = true; -#else - this.Monitor.Log("PyTK's image scaling is not compatible with SMAPI strict mode.", LogLevel.Warn); -#endif - } - // initialize loaded non-content-pack mods this.Monitor.Log("Launching mods...", LogLevel.Debug); foreach (IModMetadata metadata in loadedMods) @@ -1739,69 +1683,6 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin metadata.Mod ?? throw new InvalidOperationException($"The '{metadata.DisplayName}' mod is not initialized correctly."); // should never happen, but avoids nullability warnings -#if SMAPI_DEPRECATED - // add interceptors - if (mod.Helper is ModHelper helper) - { - // ReSharper disable SuspiciousTypeConversion.Global - if (mod is IAssetEditor editor) - { - SCore.DeprecationManager.Warn( - source: metadata, - nounPhrase: $"{nameof(IAssetEditor)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval, - logStackTrace: false - ); - - this.ContentCore.Editors.Add(new ModLinked(metadata, editor)); - } - - if (mod is IAssetLoader loader) - { - SCore.DeprecationManager.Warn( - source: metadata, - nounPhrase: $"{nameof(IAssetLoader)}", - version: "3.14.0", - severity: DeprecationLevel.PendingRemoval, - logStackTrace: false - ); - - this.ContentCore.Loaders.Add(new ModLinked(metadata, loader)); - } - // ReSharper restore SuspiciousTypeConversion.Global - - ContentHelper content = helper.GetLegacyContentHelper(); - content.ObservableAssetEditors.CollectionChanged += (_, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); - content.ObservableAssetLoaders.CollectionChanged += (_, e) => this.OnAssetInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); - } - - // log deprecation warnings - if (metadata.HasWarnings(ModWarning.DetectedLegacyCachingDll, ModWarning.DetectedLegacyConfigurationDll, ModWarning.DetectedLegacyPermissionsDll)) - { - string?[] referenced = - new[] - { - metadata.Warnings.HasFlag(ModWarning.DetectedLegacyConfigurationDll) ? "System.Configuration.ConfigurationManager" : null, - metadata.Warnings.HasFlag(ModWarning.DetectedLegacyCachingDll) ? "System.Runtime.Caching" : null, - metadata.Warnings.HasFlag(ModWarning.DetectedLegacyPermissionsDll) ? "System.Security.Permissions" : null - } - .Where(p => p is not null) - .ToArray(); - - foreach (string? name in referenced) - { - DeprecationManager.Warn( - metadata, - $"using {name} without bundling it", - "3.14.7", - DeprecationLevel.PendingRemoval, - logStackTrace: false - ); - } - } -#endif - // initialize mod Context.HeuristicModsRunningCode.Push(metadata); { @@ -1847,31 +1728,6 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin this.Monitor.Log("Mods loaded and ready!", LogLevel.Debug); } -#if SMAPI_DEPRECATED - /// Raised after a mod adds or removes asset interceptors. - /// The asset interceptor type (one of or ). - /// The mod metadata. - /// The interceptors that were added. - /// The interceptors that were removed. - /// A list of interceptors to update for the change. - private void OnAssetInterceptorsChanged(IModMetadata mod, IEnumerable? added, IEnumerable? removed, IList> list) - where T : notnull - { - foreach (T interceptor in added ?? Array.Empty()) - { - this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, interceptor, wasAdded: true)); - list.Add(new ModLinked(mod, interceptor)); - } - - foreach (T interceptor in removed ?? Array.Empty()) - { - this.ReloadAssetInterceptorsQueue.Add(new AssetInterceptorChange(mod, interceptor, wasAdded: false)); - foreach (ModLinked entry in list.Where(p => p.Mod == mod && object.ReferenceEquals(p.Data, interceptor)).ToArray()) - list.Remove(entry); - } - } -#endif - /// Load a given mod. /// The mod to load. /// The mods being loaded. @@ -2014,9 +1870,6 @@ IContentPack[] GetContentPacks() { IModEvents events = new ModEvents(mod, this.EventManager); ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager); -#if SMAPI_DEPRECATED - ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, mod, monitor, this.Reflection); -#endif GameContentHelper gameContentHelper = new(contentCore, mod, mod.DisplayName, monitor, this.Reflection); IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), this.Reflection); IContentPackHelper contentPackHelper = new ContentPackHelper( @@ -2029,11 +1882,7 @@ IContentPack[] GetContentPacks() IModRegistry modRegistryHelper = new ModRegistryHelper(mod, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(mod, this.Multiplayer); - modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, -#if SMAPI_DEPRECATED - contentHelper, -#endif - gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); + modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); } // init mod diff --git a/src/SMAPI/GameFramework.cs b/src/SMAPI/GameFramework.cs index 009865fe1..98662f780 100644 --- a/src/SMAPI/GameFramework.cs +++ b/src/SMAPI/GameFramework.cs @@ -1,18 +1,8 @@ -#if SMAPI_DEPRECATED -using System; -#endif - namespace StardewModdingAPI { /// The game framework running the game. public enum GameFramework { -#if SMAPI_DEPRECATED - /// The XNA Framework, previously used on Windows. - [Obsolete("Stardew Valley no longer uses XNA Framework on any supported platform. This value will be removed in SMAPI 4.0.0.")] - Xna, -#endif - /// The MonoGame framework. MonoGame } diff --git a/src/SMAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs deleted file mode 100644 index f3238ba95..000000000 --- a/src/SMAPI/IAssetEditor.cs +++ /dev/null @@ -1,23 +0,0 @@ -#if SMAPI_DEPRECATED -using System; -using StardewModdingAPI.Events; - -namespace StardewModdingAPI -{ - /// Edits matching content assets. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This interface will be removed in SMAPI 4.0.0.")] - public interface IAssetEditor - { - /********* - ** Public methods - *********/ - /// Get whether this instance can edit the given asset. - /// Basic metadata about the asset being loaded. - bool CanEdit(IAssetInfo asset); - - /// Edit a matched asset. - /// A helper which encapsulates metadata about an asset and enables changes to it. - void Edit(IAssetData asset); - } -} -#endif diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index 20064946f..f0cadffc8 100644 --- a/src/SMAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs @@ -8,20 +8,10 @@ public interface IAssetInfo /********* ** Accessors *********/ -#if SMAPI_DEPRECATED /// The content's locale code, if the content is localized. - /// LEGACY NOTE: when reading this field from an or implementation, for non-localized assets it will return the current game locale (or an empty string for English) instead of null. -#else - /// The content's locale code, if the content is localized. -#endif string? Locale { get; } -#if SMAPI_DEPRECATED - /// The asset name being read. - /// LEGACY NOTE: when reading this field from an or implementation, it's always equivalent to for backwards compatibility. -#else /// The asset name being read. -#endif public IAssetName Name { get; } /// The with any locale codes stripped. @@ -30,20 +20,5 @@ public interface IAssetInfo /// The content data type. Type DataType { get; } - -#if SMAPI_DEPRECATED - /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. - [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")] - string AssetName { get; } - - - /********* - ** Public methods - *********/ - /// Get whether the asset name being loaded matches a given name after normalization. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")] - bool AssetNameEquals(string path); -#endif } } diff --git a/src/SMAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs deleted file mode 100644 index 205980a74..000000000 --- a/src/SMAPI/IAssetLoader.cs +++ /dev/null @@ -1,23 +0,0 @@ -#if SMAPI_DEPRECATED -using System; -using StardewModdingAPI.Events; - -namespace StardewModdingAPI -{ - /// Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use instead. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This interface will be removed in SMAPI 4.0.0.")] - public interface IAssetLoader - { - /********* - ** Public methods - *********/ - /// Get whether this instance can load the initial version of the given asset. - /// Basic metadata about the asset being loaded. - bool CanLoad(IAssetInfo asset); - - /// Load a matched asset. - /// Basic metadata about the asset being loaded. - T Load(IAssetInfo asset); - } -} -#endif diff --git a/src/SMAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs index c92a09c2b..afbcf2b02 100644 --- a/src/SMAPI/ICommandHelper.cs +++ b/src/SMAPI/ICommandHelper.cs @@ -16,14 +16,5 @@ public interface ICommandHelper : IModLinked /// The is not a valid format. /// There's already a command with that name. ICommandHelper Add(string name, string documentation, Action callback); - -#if SMAPI_DEPRECATED - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. - [Obsolete("Use mod-provided APIs to integrate with mods instead. This method will be removed in SMAPI 4.0.0.")] - bool Trigger(string name, string[] arguments); -#endif } } diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs deleted file mode 100644 index b0e30a829..000000000 --- a/src/SMAPI/IContentHelper.cs +++ /dev/null @@ -1,84 +0,0 @@ -#if SMAPI_DEPRECATED -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Events; -using StardewValley; -using xTile; - -namespace StardewModdingAPI -{ - /// Provides an API for loading content assets. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.GameContent)} or {nameof(IMod.Helper)}.{nameof(IModHelper.ModContent)} instead. This interface will be removed in SMAPI 4.0.0.")] - public interface IContentHelper : IModLinked - { - /********* - ** Accessors - *********/ - /// Interceptors which provide the initial versions of matching content assets. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This property will be removed in SMAPI 4.0.0.")] - IList AssetLoaders { get; } - - /// Interceptors which edit matching content assets after they're loaded. - [Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.Events)}.{nameof(IModEvents.Content)} instead. This property will be removed in SMAPI 4.0.0.")] - IList AssetEditors { get; } - - /// The game's current locale code (like pt-BR). - string CurrentLocale { get; } - - /// The game's current locale as an enum value. - LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } - - - /********* - ** Public methods - *********/ - /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , (for mod content only), and data structures; other types may be supported by the game's content pipeline. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - T Load(string key, ContentSource source = ContentSource.ModFolder) - where T : notnull; - - /// Normalize an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. - /// The asset key. - /// The asset key is empty or contains invalid characters. - [Pure] - string NormalizeAssetName(string? assetName); - - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); - - /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to invalidate in the content folder. - /// The is empty or contains invalid characters. - /// Returns whether the given asset key was cached. - bool InvalidateCache(string key); - - /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. - /// The asset type to remove from the cache. - /// Returns whether any assets were invalidated. - bool InvalidateCache() - where T : notnull; - - /// Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// A predicate matching the assets to invalidate. - /// Returns whether any cache entries were invalidated. - bool InvalidateCache(Func predicate); - - /// Get a patch helper for arbitrary data. - /// The data type. - /// The asset data. - /// The asset name. This is only used for tracking purposes and has no effect on the patch helper. - IAssetData GetPatchHelper(T data, string? assetName = null) - where T : notnull; - } -} -#endif diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 5047b172b..5dac451df 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -1,9 +1,4 @@ using System; -#if SMAPI_DEPRECATED -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using xTile; -#endif namespace StardewModdingAPI { @@ -48,22 +43,5 @@ public interface IContentPack /// The is not relative or contains directory climbing (../). void WriteJsonFile(string path, TModel data) where TModel : class; - -#if SMAPI_DEPRECATED - /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are , , , and data structures; other types may be supported by the game's content pipeline. - /// The relative file path within the content pack (case-insensitive). - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - [Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.Load)} instead. This method will be removed in SMAPI 4.0.0.")] - T LoadAsset(string key) - where T : notnull; - - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The relative file path within the content pack (case-insensitive). - /// The is empty or contains invalid characters. - [Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.GetInternalAssetName)} instead. This method will be removed in SMAPI 4.0.0.")] - string GetActualAssetKey(string key); -#endif } } diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index a44d92c18..5e56bf823 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,6 +1,3 @@ -#if SMAPI_DEPRECATED -using System; -#endif using StardewModdingAPI.Events; namespace StardewModdingAPI @@ -27,12 +24,6 @@ public interface IModHelper /// This API is intended for reading content assets from the mod files (like game data, images, etc); see also which is intended for persisting internal mod data. IModContentHelper ModContent { get; } -#if SMAPI_DEPRECATED - /// An API for loading content assets. - [Obsolete($"Use {nameof(IGameContentHelper)} or {nameof(IModContentHelper)} instead.")] - IContentHelper Content { get; } -#endif - /// An API for managing content packs. IContentPackHelper ContentPacks { get; } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 1024b19c1..e5b99e0b0 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -53,11 +53,6 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr // detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update) yield return new HarmonyRewriter(); - -#if SMAPI_DEPRECATED - // detect issues for SMAPI 4.0.0 - yield return new LegacyAssemblyFinder(); -#endif } else yield return new HarmonyRewriter(shouldRewrite: false); diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 674ec760e..ba6434e4f 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -1,10 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -#if SMAPI_DEPRECATED -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Deprecations; -#endif namespace StardewModdingAPI.Utilities { @@ -51,22 +47,7 @@ public PerScreen() /// Create the initial state for a screen. public PerScreen(Func createNewState) { - if (createNewState is null) - { -#if SMAPI_DEPRECATED - createNewState = (() => default!); - SCore.DeprecationManager.Warn( - null, - $"calling the {nameof(PerScreen)} constructor with null", - "3.14.0", - DeprecationLevel.PendingRemoval - ); -#else - throw new ArgumentNullException(nameof(createNewState)); -#endif - } - - this.CreateNewState = createNewState; + this.CreateNewState = createNewState ?? throw new ArgumentNullException(nameof(createNewState)); } /// Get all active values by screen ID. This doesn't initialize the value for a screen ID if it's not created yet. From 26edc252c0630d67e9b1be52c76b0b4edbce7be8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Dec 2023 14:23:04 -0500 Subject: [PATCH 18/84] remove bundled ErrorHandler mod All of its error-handling has been integrated into Stardew Valley 1.6. --- build/deploy-local-smapi.targets | 2 +- build/unix/prepare-install-package.sh | 2 +- build/unix/set-smapi-version.sh | 2 +- build/windows/prepare-install-package.ps1 | 2 +- build/windows/set-smapi-version.ps1 | 2 +- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 3 +- src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 87 --------- .../Patches/DialoguePatcher.cs | 75 -------- .../Patches/EventPatcher.cs | 52 ------ .../Patches/GameLocationPatcher.cs | 78 -------- .../Patches/IClickableMenuPatcher.cs | 44 ----- .../Patches/NpcPatcher.cs | 85 --------- .../Patches/ObjectPatcher.cs | 71 -------- .../Patches/SaveGamePatcher.cs | 171 ------------------ .../Patches/SpriteBatchPatcher.cs | 39 ---- .../Patches/UtilityPatcher.cs | 43 ----- .../SMAPI.Mods.ErrorHandler.csproj | 27 --- src/SMAPI.Mods.ErrorHandler/i18n/de.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/default.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/es.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/fr.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/hu.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/it.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/ja.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/ko.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/pl.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/pt.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/ru.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/th.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/tr.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/uk.json | 4 - src/SMAPI.Mods.ErrorHandler/i18n/zh.json | 4 - src/SMAPI.Mods.ErrorHandler/manifest.json | 9 - src/SMAPI.Web/Views/LogParser/Index.cshtml | 8 +- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 14 +- src/SMAPI.sln | 8 - src/SMAPI/Framework/Models/SConfig.cs | 1 - src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Properties/AssemblyInfo.cs | 1 - src/SMAPI/SMAPI.config.json | 1 - 41 files changed, 19 insertions(+), 872 deletions(-) delete mode 100644 src/SMAPI.Mods.ErrorHandler/ModEntry.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs delete mode 100644 src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/de.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/default.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/es.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/fr.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/hu.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/it.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/ja.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/ko.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/pl.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/pt.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/ru.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/th.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/tr.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/uk.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/i18n/zh.json delete mode 100644 src/SMAPI.Mods.ErrorHandler/manifest.json diff --git a/build/deploy-local-smapi.targets b/build/deploy-local-smapi.targets index e6c018001..d30967fe5 100644 --- a/build/deploy-local-smapi.targets +++ b/build/deploy-local-smapi.targets @@ -55,7 +55,7 @@ This assumes `find-game-folder.targets` has already been imported and validated. - + diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 033994bcb..cfd24afd4 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -13,7 +13,7 @@ ########## # paths gamePath="/home/pathoschild/Stardew Valley" -bundleModNames=("ConsoleCommands" "ErrorHandler" "SaveBackup") +bundleModNames=("ConsoleCommands" "SaveBackup") # build configuration buildConfig="Release" diff --git a/build/unix/set-smapi-version.sh b/build/unix/set-smapi-version.sh index 02b5e615a..3ab48eb5c 100755 --- a/build/unix/set-smapi-version.sh +++ b/build/unix/set-smapi-version.sh @@ -21,6 +21,6 @@ cd "`dirname "$0"`/../.." # apply changes sed "s/.+<\/Version>/$version<\/Version>/" "build/common.targets" --in-place --regexp-extended sed "s/RawApiVersion = \".+?\";/RawApiVersion = \"$version\";/" "src/SMAPI/Constants.cs" --in-place --regexp-extended -for modName in "ConsoleCommands" "ErrorHandler" "SaveBackup"; do +for modName in "ConsoleCommands" "SaveBackup"; do sed "s/\"(Version|MinimumApiVersion)\": \".+?\"/\"\1\": \"$version\"/g" "src/SMAPI.Mods.$modName/manifest.json" --in-place --regexp-extended done diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index ee27537ac..2e6dc9912 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -17,7 +17,7 @@ ########## # paths $gamePath = "C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley" -$bundleModNames = "ConsoleCommands", "ErrorHandler", "SaveBackup" +$bundleModNames = "ConsoleCommands", "SaveBackup" # build configuration $buildConfig = "Release" diff --git a/build/windows/set-smapi-version.ps1 b/build/windows/set-smapi-version.ps1 index ff6b20964..1e1099757 100644 --- a/build/windows/set-smapi-version.ps1 +++ b/build/windows/set-smapi-version.ps1 @@ -20,6 +20,6 @@ cd "$PSScriptRoot/../.." # apply changes In-Place-Regex -Path "build/common.targets" -Search ".+" -Replace "$version" In-Place-Regex -Path "src/SMAPI/Constants.cs" -Search "RawApiVersion = `".+?`";" -Replace "RawApiVersion = `"$version`";" -ForEach ($modName in "ConsoleCommands","ErrorHandler","SaveBackup") { +ForEach ($modName in "ConsoleCommands","SaveBackup") { In-Place-Regex -Path "src/SMAPI.Mods.$modName/manifest.json" -Search "`"(Version|MinimumApiVersion)`": `".+?`"" -Replace "`"`$1`": `"$version`"" } diff --git a/docs/release-notes.md b/docs/release-notes.md index 1b9bb3d77..c9c0c20e2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * Updated for Stardew Valley 1.6. * Improved performance. + * Removed the bundled `ErrorHandler` mod (now integrated into Stardew Valley 1.6). * Removed support for seamlessly updating from SMAPI 2.11.3 and earlier (released in 2019). _If needed, you can update to SMAPI 3.18.0 first and then install the latest version._ diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index eaca97643..670b79d8c 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -29,8 +29,7 @@ internal class InteractiveInstaller /// The mod IDs which the installer should allow as bundled mods. private readonly string[] BundledModIDs = { "SMAPI.SaveBackup", - "SMAPI.ConsoleCommands", - "SMAPI.ErrorHandler" + "SMAPI.ConsoleCommands" }; /// Get the absolute file or folder paths to remove when uninstalling SMAPI. diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs deleted file mode 100644 index e1c338656..000000000 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Reflection; -using StardewModdingAPI.Events; -using StardewModdingAPI.Internal.Patching; -using StardewModdingAPI.Mods.ErrorHandler.Patches; -using StardewValley; - -namespace StardewModdingAPI.Mods.ErrorHandler -{ - /// The main entry point for the mod. - public class ModEntry : Mod - { - /********* - ** Private methods - *********/ - /// Whether custom content was removed from the save data to avoid a crash. - private bool IsSaveContentRemoved; - - - /********* - ** Public methods - *********/ - /// The mod entry point, called after the mod is first loaded. - /// Provides simplified APIs for writing mods. - public override void Entry(IModHelper helper) - { - // get SMAPI core types - IMonitor monitorForGame = this.GetMonitorForGame(); - - // apply patches - HarmonyPatcher.Apply(this.ModManifest.UniqueID, this.Monitor, - // game patches - new DialoguePatcher(monitorForGame, this.Helper.Reflection), - new EventPatcher(monitorForGame), - new GameLocationPatcher(monitorForGame), - new IClickableMenuPatcher(), - new NpcPatcher(monitorForGame), - new ObjectPatcher(), - new SaveGamePatcher(this.Monitor, this.OnSaveContentRemoved), - new SpriteBatchPatcher(), - new UtilityPatcher() - ); - - // hook events - this.Helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded; - } - - - /********* - ** Private methods - *********/ - /// Raised after custom content is removed from the save data to avoid a crash. - internal void OnSaveContentRemoved() - { - this.IsSaveContentRemoved = true; - } - - /// The method invoked when a save is loaded. - /// The event sender. - /// The event arguments. - private void OnSaveLoaded(object? sender, SaveLoadedEventArgs e) - { - // show in-game warning for removed save content - if (this.IsSaveContentRemoved) - { - this.IsSaveContentRemoved = false; - Game1.addHUDMessage(new HUDMessage(this.Helper.Translation.Get("warn.invalid-content-removed"), HUDMessage.error_type)); - } - } - - /// Get the monitor with which to log game errors. - private IMonitor GetMonitorForGame() - { - // get SMAPI core - Type coreType = Type.GetType("StardewModdingAPI.Framework.SCore, StardewModdingAPI", throwOnError: false) - ?? throw new InvalidOperationException("Can't access SMAPI's core type. This mod may not work correctly."); - object core = coreType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) - ?? throw new InvalidOperationException("Can't access SMAPI's core instance. This mod may not work correctly."); - - // get monitor - MethodInfo getMonitorForGame = coreType.GetMethod("GetMonitorForGame") - ?? throw new InvalidOperationException("Can't access the SMAPI's 'GetMonitorForGame' method. This mod may not work correctly."); - - return (IMonitor?)getMonitorForGame.Invoke(core, Array.Empty()) ?? this.Monitor; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs deleted file mode 100644 index e98eec3c9..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Patching; -using StardewValley; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which intercept invalid dialogue lines and logs an error instead of crashing. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class DialoguePatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// Writes messages to the console and log file on behalf of the game. - private static IMonitor MonitorForGame = null!; - - /// Simplifies access to private code. - private static IReflectionHelper Reflection = null!; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Writes messages to the console and log file on behalf of the game. - /// Simplifies access to private code. - public DialoguePatcher(IMonitor monitorForGame, IReflectionHelper reflector) - { - DialoguePatcher.MonitorForGame = monitorForGame; - DialoguePatcher.Reflection = reflector; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireConstructor(typeof(string), typeof(NPC)), - finalizer: this.GetHarmonyMethod(nameof(DialoguePatcher.Finalize_Constructor)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call when the Dialogue constructor throws an exception. - /// The instance being patched. - /// The dialogue being parsed. - /// The NPC for which the dialogue is being parsed. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC? speaker, Exception? __exception) - { - if (__exception != null) - { - // log message - string? name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; - DialoguePatcher.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error); - - // set default dialogue - IReflectedMethod parseDialogueString = DialoguePatcher.Reflection.GetMethod(__instance, "parseDialogueString"); - IReflectedMethod checkForSpecialDialogueAttributes = DialoguePatcher.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); - parseDialogueString.Invoke("..."); - checkForSpecialDialogueAttributes.Invoke(); - } - - return null; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs deleted file mode 100644 index 073c62cc0..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal.Patching; -using StardewValley; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which intercept errors to log more details. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class EventPatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// Writes messages to the console and log file on behalf of the game. - private static IMonitor MonitorForGame = null!; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Writes messages to the console and log file on behalf of the game. - public EventPatcher(IMonitor monitorForGame) - { - EventPatcher.MonitorForGame = monitorForGame; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod(nameof(Event.LogErrorAndHalt)), - postfix: this.GetHarmonyMethod(nameof(EventPatcher.After_LogErrorAndHalt)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call after . - /// The exception being logged. - private static void After_LogErrorAndHalt(Exception e) - { - EventPatcher.MonitorForGame.Log(e.ToString(), LogLevel.Error); - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs deleted file mode 100644 index 9247fa487..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal.Patching; -using StardewValley; -using xTile; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which intercept errors instead of crashing. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class GameLocationPatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// Writes messages to the console and log file on behalf of the game. - private static IMonitor MonitorForGame = null!; - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Writes messages to the console and log file on behalf of the game. - public GameLocationPatcher(IMonitor monitorForGame) - { - GameLocationPatcher.MonitorForGame = monitorForGame; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod(nameof(GameLocation.checkEventPrecondition)), - finalizer: this.GetHarmonyMethod(nameof(GameLocationPatcher.Finalize_CheckEventPrecondition)) - ); - harmony.Patch( - original: this.RequireMethod(nameof(GameLocation.updateSeasonalTileSheets)), - finalizer: this.GetHarmonyMethod(nameof(GameLocationPatcher.Finalize_UpdateSeasonalTileSheets)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call when throws an exception. - /// The return value of the original method. - /// The precondition to be parsed. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception? __exception) - { - if (__exception != null) - { - __result = -1; - GameLocationPatcher.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{__exception.InnerException}", LogLevel.Error); - } - - return null; - } - - /// The method to call when throws an exception. - /// The instance being patched. - /// The map whose tilesheets to update. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception? __exception) - { - if (__exception != null) - GameLocationPatcher.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}': \n{__exception}", LogLevel.Error); - - return null; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs deleted file mode 100644 index b65a695ac..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal.Patching; -using StardewValley; -using StardewValley.Menus; -using SObject = StardewValley.Object; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which intercept crashes due to invalid items. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class IClickableMenuPatcher : BasePatcher - { - /********* - ** Public methods - *********/ - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod(nameof(IClickableMenu.drawToolTip)), - prefix: this.GetHarmonyMethod(nameof(IClickableMenuPatcher.Before_DrawTooltip)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of . - /// The item for which to draw a tooltip. - /// Returns whether to execute the original method. - private static bool Before_DrawTooltip(Item hoveredItem) - { - // invalid edible item cause crash when drawing tooltips - if (hoveredItem is SObject obj && obj.Edibility != -300 && !Game1.objectInformation.ContainsKey(obj.ParentSheetIndex)) - return false; - - return true; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs deleted file mode 100644 index 11f7ec696..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Patching; -using StardewValley; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which intercept crashes due to invalid schedule data. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class NpcPatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// Writes messages to the console and log file on behalf of the game. - private static IMonitor MonitorForGame = null!; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Writes messages to the console and log file on behalf of the game. - public NpcPatcher(IMonitor monitorForGame) - { - NpcPatcher.MonitorForGame = monitorForGame; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod($"get_{nameof(NPC.CurrentDialogue)}"), - finalizer: this.GetHarmonyMethod(nameof(NpcPatcher.Finalize_CurrentDialogue)) - ); - - harmony.Patch( - original: this.RequireMethod(nameof(NPC.parseMasterSchedule)), - finalizer: this.GetHarmonyMethod(nameof(NpcPatcher.Finalize_ParseMasterSchedule)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call when throws an exception. - /// The instance being patched. - /// The return value of the original method. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_CurrentDialogue(NPC __instance, ref Stack __result, Exception? __exception) - { - if (__exception == null) - return null; - - NpcPatcher.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{__exception.GetLogSummary()}", LogLevel.Error); - __result = new Stack(); - - return null; - } - - /// The method to call instead of . - /// The raw schedule data to parse. - /// The instance being patched. - /// The patched method's return value. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, Exception? __exception) - { - if (__exception != null) - { - NpcPatcher.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{__exception.GetLogSummary()}", LogLevel.Error); - __result = new Dictionary(); - } - - return null; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs deleted file mode 100644 index 09a6fbbdd..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal.Patching; -using StardewValley; -using SObject = StardewValley.Object; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which intercept crashes due to invalid items. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class ObjectPatcher : BasePatcher - { - /********* - ** Public methods - *********/ - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - // object.getDescription - harmony.Patch( - original: this.RequireMethod(nameof(SObject.getDescription)), - prefix: this.GetHarmonyMethod(nameof(ObjectPatcher.Before_Object_GetDescription)) - ); - - // object.getDisplayName - harmony.Patch( - original: this.RequireMethod("loadDisplayName"), - finalizer: this.GetHarmonyMethod(nameof(ObjectPatcher.Finalize_Object_loadDisplayName)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of . - /// The instance being patched. - /// The patched method's return value. - /// Returns whether to execute the original method. - private static bool Before_Object_GetDescription(SObject __instance, ref string __result) - { - // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables - if (!__instance.IsRecipe && __instance.bigCraftable.Value && !Game1.bigCraftablesInformation.ContainsKey(__instance.ParentSheetIndex)) - { - __result = "???"; - return false; - } - - return true; - } - - /// The method to call after . - /// The patched method's return value. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_Object_loadDisplayName(ref string __result, Exception? __exception) - { - if (__exception is KeyNotFoundException) - { - __result = "???"; - return null; - } - - return __exception; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs deleted file mode 100644 index 490bbfb6d..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using HarmonyLib; -using Microsoft.Xna.Framework.Content; -using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Patching; -using StardewValley; -using StardewValley.Buildings; -using StardewValley.Locations; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which prevent some errors due to broken save data. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class SaveGamePatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// Writes messages to the console and log file. - private static IMonitor Monitor = null!; - - /// A callback invoked when custom content is removed from the save data to avoid a crash. - private static Action OnContentRemoved = null!; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Writes messages to the console and log file. - /// A callback invoked when custom content is removed from the save data to avoid a crash. - public SaveGamePatcher(IMonitor monitor, Action onContentRemoved) - { - SaveGamePatcher.Monitor = monitor; - SaveGamePatcher.OnContentRemoved = onContentRemoved; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod(nameof(SaveGame.loadDataToLocations)), - prefix: this.GetHarmonyMethod(nameof(SaveGamePatcher.Before_LoadDataToLocations)) - ); - - harmony.Patch( - original: this.RequireMethod(nameof(SaveGame.LoadFarmType)), - finalizer: this.GetHarmonyMethod(nameof(SaveGamePatcher.Finalize_LoadFarmType)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of . - /// The game locations being loaded. - /// Returns whether to execute the original method. - private static bool Before_LoadDataToLocations(List gamelocations) - { - // missing locations/NPCs - IDictionary npcs = Game1.content.Load>("Data\\NPCDispositions"); - if (SaveGamePatcher.RemoveBrokenContent(gamelocations, npcs)) - SaveGamePatcher.OnContentRemoved(); - - return true; - } - - /// The method to call after throws an exception. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_LoadFarmType(Exception? __exception) - { - // missing custom farm type - if (__exception?.Message.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _)) - { - SaveGamePatcher.Monitor.Log(__exception.GetLogSummary(), LogLevel.Error); - SaveGamePatcher.Monitor.Log($"Removed invalid custom farm type '{SaveGame.loaded.whichFarm}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom farm type mod?)", LogLevel.Warn); - - SaveGame.loaded.whichFarm = Farm.default_layout.ToString(); - SaveGame.LoadFarmType(); - SaveGamePatcher.OnContentRemoved(); - - __exception = null; - } - - return __exception; - } - - /// Remove content which no longer exists in the game data. - /// The current game locations. - /// The NPC data. - private static bool RemoveBrokenContent(IEnumerable locations, IDictionary npcs) - { - bool removedAny = false; - - foreach (GameLocation location in locations) - removedAny |= SaveGamePatcher.RemoveBrokenContent(location, npcs); - - return removedAny; - } - - /// Remove content which no longer exists in the game data. - /// The current game location. - /// The NPC data. - private static bool RemoveBrokenContent(GameLocation? location, IDictionary npcs) - { - bool removedAny = false; - if (location == null) - return false; - - // check buildings - if (location is BuildableGameLocation buildableLocation) - { - foreach (Building building in buildableLocation.buildings.ToArray()) - { - try - { - BluePrint _ = new(building.buildingType.Value); - } - catch (ContentLoadException) - { - SaveGamePatcher.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn); - buildableLocation.buildings.Remove(building); - removedAny = true; - continue; - } - - SaveGamePatcher.RemoveBrokenContent(building.indoors.Value, npcs); - } - } - - // check NPCs - foreach (NPC npc in location.characters.ToArray()) - { - if (npc.isVillager() && !npcs.ContainsKey(npc.Name)) - { - try - { - npc.reloadSprite(); // this won't crash for special villagers like Bouncer - } - catch - { - SaveGamePatcher.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); - location.characters.Remove(npc); - removedAny = true; - } - } - } - - // check objects - foreach (var pair in location.objects.Pairs.ToArray()) - { - // SpaceCore can leave null values when removing its custom content - if (pair.Value == null) - { - location.Objects.Remove(pair.Key); - SaveGamePatcher.Monitor.Log($"Removed invalid null object in {location.Name} ({pair.Key}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom item mod?)", LogLevel.Warn); - removedAny = true; - } - } - - return removedAny; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs deleted file mode 100644 index d369e0ef9..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Internal.Patching; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which validate textures earlier. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class SpriteBatchPatcher : BasePatcher - { - /********* - ** Public methods - *********/ - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod("CheckValid", new[] { typeof(Texture2D) }), - postfix: this.GetHarmonyMethod(nameof(SpriteBatchPatcher.After_CheckValid)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call after . - /// The texture to validate. - private static void After_CheckValid(Texture2D? texture) - { - if (texture?.IsDisposed == true) - throw new ObjectDisposedException("Cannot draw this texture because it's disposed."); - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs deleted file mode 100644 index 6d75a5817..000000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal.Patching; -using StardewValley; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// A Harmony patch for methods to log more detailed errors. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class UtilityPatcher : BasePatcher - { - /********* - ** Public methods - *********/ - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod(nameof(Utility.getItemFromStandardTextDescription)), - finalizer: this.GetHarmonyMethod(nameof(UtilityPatcher.Finalize_GetItemFromStandardTextDescription)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call when throws an exception. - /// The item text description to parse. - /// The delimiter by which to split the text description. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception? Finalize_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception? __exception) - { - return __exception != null - ? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception) - : null; - } - } -} diff --git a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj b/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj deleted file mode 100644 index 77f912153..000000000 --- a/src/SMAPI.Mods.ErrorHandler/SMAPI.Mods.ErrorHandler.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - ErrorHandler - StardewModdingAPI.Mods.ErrorHandler - net6.0 - false - - - - - - - - - - - - - - - - - - - - - diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/de.json b/src/SMAPI.Mods.ErrorHandler/i18n/de.json deleted file mode 100644 index 1de6301cf..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/de.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/default.json b/src/SMAPI.Mods.ErrorHandler/i18n/default.json deleted file mode 100644 index b74dcea00..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/default.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/es.json b/src/SMAPI.Mods.ErrorHandler/i18n/es.json deleted file mode 100644 index 8ba10b705..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/es.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/fr.json b/src/SMAPI.Mods.ErrorHandler/i18n/fr.json deleted file mode 100644 index 769785263..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/fr.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/hu.json b/src/SMAPI.Mods.ErrorHandler/i18n/hu.json deleted file mode 100644 index 92aca7d08..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/hu.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/it.json b/src/SMAPI.Mods.ErrorHandler/i18n/it.json deleted file mode 100644 index 5182972ee..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/it.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/ja.json b/src/SMAPI.Mods.ErrorHandler/i18n/ja.json deleted file mode 100644 index 559c7fbe8..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/ja.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)" -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/ko.json b/src/SMAPI.Mods.ErrorHandler/i18n/ko.json deleted file mode 100644 index 48f05c26c..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/ko.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/pl.json b/src/SMAPI.Mods.ErrorHandler/i18n/pl.json deleted file mode 100644 index f080bcd48..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/pl.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Nieprawidłowa zawartość została usunięta, aby zapobiec awarii (zobacz konsolę SMAPI po więcej informacji)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/pt.json b/src/SMAPI.Mods.ErrorHandler/i18n/pt.json deleted file mode 100644 index 8ea8cec94..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/pt.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/ru.json b/src/SMAPI.Mods.ErrorHandler/i18n/ru.json deleted file mode 100644 index e9c3b313a..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/ru.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)" -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/th.json b/src/SMAPI.Mods.ErrorHandler/i18n/th.json deleted file mode 100644 index e2a67dda2..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/th.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "ทำการลบเนื้อหาที่ไม่ถูกต้องออก เพื่อป้องกันไฟล์เกมเสียหาย (ดูรายละเอียดที่หน้าคอลโซลของ SMAPI)" -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/tr.json b/src/SMAPI.Mods.ErrorHandler/i18n/tr.json deleted file mode 100644 index a05ab1522..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/tr.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/uk.json b/src/SMAPI.Mods.ErrorHandler/i18n/uk.json deleted file mode 100644 index a58102abe..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/uk.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "Недійсний вміст видалено, щоб запобігти аварійному завершенню роботи (Додаткову інформацію див. на консолі SMAPI)." -} diff --git a/src/SMAPI.Mods.ErrorHandler/i18n/zh.json b/src/SMAPI.Mods.ErrorHandler/i18n/zh.json deleted file mode 100644 index e959aa40f..000000000 --- a/src/SMAPI.Mods.ErrorHandler/i18n/zh.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // warning messages - "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)" -} diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json deleted file mode 100644 index fb268fa03..000000000 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Name": "Error Handler", - "Author": "SMAPI", - "Version": "3.18.6", - "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", - "UniqueID": "SMAPI.ErrorHandler", - "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.18.6" -} diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index ae3c0505d..a05282860 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -15,11 +15,13 @@ // detect suggested fixes LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? Array.Empty(); - LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler"); - bool missingErrorHandler = errorHandler is null && log?.OperatingSystem?.Contains("Android Unix", StringComparison.OrdinalIgnoreCase) != true; - bool hasOlderErrorHandler = errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion()); bool isPyTkCompatibilityMode = log?.ApiVersionParsed?.IsBetween("3.15.0", "3.19.0") is true && log.Mods.Any(p => p.IsCodeMod && p.Name == "PyTK" && p.GetParsedVersion()?.IsOlderThan("1.24.0") is true); + LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler"); + bool errorHandlerNeeded = log?.ApiVersionParsed?.IsOlderThan("4.0.0-alpha") == true && log.OperatingSystem?.Contains("Android Unix", StringComparison.OrdinalIgnoreCase) != true; + bool missingErrorHandler = errorHandlerNeeded && errorHandler is null; + bool hasOlderErrorHandler = errorHandlerNeeded && errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion()); + // get filters IDictionary defaultFilters = Enum .GetValues() diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index d654b1819..6c8aa81e3 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -51,14 +51,6 @@ * was overridden. If not provided, it defaults to the StatusReasonPhrase or 'no reason given'. */ "ModData": { - /********* - ** Mods bundles with SMAPI - *********/ - "Error Handler": { - "ID": "SMAPI.ErrorHandler", - "SuppressWarnings": "PatchesGame" - }, - /********* ** Common dependencies for friendly errors *********/ @@ -143,6 +135,12 @@ "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." }, + "Error Handler": { + "ID": "SMAPI.ErrorHandler", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "its error handling was integrated into Stardew Valley 1.6." + }, + "Modder Serialization Utility": { "ID": "SerializerUtils-0-1", "~ | Status": "Obsolete", diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 99b9dc834..c7eac4c3e 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -62,7 +62,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Installer", "SMAPI.Installer\SMAPI.Installer.csproj", "{0A9BB24F-15FF-4C26-B1A2-81F7AE316518}" ProjectSection(ProjectDependencies) = postProject {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} - {491E775B-EAD0-44D4-B6CA-F1FC3E316D33} = {491E775B-EAD0-44D4-B6CA-F1FC3E316D33} {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {CD53AD6F-97F4-4872-A212-50C2A0FD3601} {E6DA2198-7686-4F1D-B312-4A4DC70884C0} = {E6DA2198-7686-4F1D-B312-4A4DC70884C0} EndProjectSection @@ -73,8 +72,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\SMAPI.Mods.ConsoleCommands.csproj", "{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ErrorHandler", "SMAPI.Mods.ErrorHandler\SMAPI.Mods.ErrorHandler.csproj", "{491E775B-EAD0-44D4-B6CA-F1FC3E316D33}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\SMAPI.Mods.SaveBackup.csproj", "{CD53AD6F-97F4-4872-A212-50C2A0FD3601}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit", "SMAPI.Toolkit\SMAPI.Toolkit.csproj", "{08184F74-60AD-4EEE-A78C-F4A35ADE6246}" @@ -149,10 +146,6 @@ Global {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.Build.0 = Debug|Any CPU {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.Build.0 = Release|Any CPU - {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {491E775B-EAD0-44D4-B6CA-F1FC3E316D33}.Release|Any CPU.Build.0 = Release|Any CPU {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -192,7 +185,6 @@ Global {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} - {491E775B-EAD0-44D4-B6CA-F1FC3E316D33} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} {4D661178-38FB-43E4-AA5F-9B0406919344} = {09CF91E5-5BAB-4650-A200-E5EA9A633046} {CAA1488E-842B-433D-994D-1D3D0B5DD125} = {09CF91E5-5BAB-4650-A200-E5EA9A633046} diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 87970e6c0..b347f7d71 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -31,7 +31,6 @@ internal class SConfig private static readonly HashSet DefaultSuppressUpdateChecks = new(StringComparer.OrdinalIgnoreCase) { "SMAPI.ConsoleCommands", - "SMAPI.ErrorHandler", "SMAPI.SaveBackup" }; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 166d007d1..cb8610dd8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -154,7 +154,7 @@ internal class SCore : IDisposable internal static DeprecationManager DeprecationManager { get; private set; } = null!; // initialized in constructor, which happens before other code can access it /// The singleton instance. - /// This is only intended for use by external code like the Error Handler mod. + /// This is only intended for use by external code. internal static SCore Instance { get; private set; } = null!; // initialized in constructor, which happens before other code can access it /// The number of game update ticks which have already executed. This is similar to , but incremented more consistently for every tick. @@ -313,7 +313,6 @@ public void RunInteractively() } /// Get the core logger and monitor on behalf of the game. - /// This method is called using reflection by the ErrorHandler mod to log game errors. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via reflection")] public IMonitor GetMonitorForGame() { diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs index afff16b8a..daa58943c 100644 --- a/src/SMAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -3,4 +3,3 @@ [assembly: InternalsVisibleTo("SMAPI.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing [assembly: InternalsVisibleTo("ContentPatcher")] -[assembly: InternalsVisibleTo("ErrorHandler")] diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index c2cffbde6..1bd467d97 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -145,7 +145,6 @@ in future SMAPI versions. */ "SuppressUpdateChecks": [ "SMAPI.ConsoleCommands", - "SMAPI.ErrorHandler", "SMAPI.SaveBackup" ], From c47b64cb734526f2be3f63433e18d3634bb584cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jan 2022 18:18:40 -0500 Subject: [PATCH 19/84] remove console commands which are obsolete in SDV 1.6 --- docs/release-notes.md | 1 + .../Commands/Player/ListItemTypesCommand.cs | 54 ------------------- .../Commands/Player/SetImmunityCommand.cs | 39 -------------- 3 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index c9c0c20e2..68df5af68 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * Updated for Stardew Valley 1.6. * Improved performance. * Removed the bundled `ErrorHandler` mod (now integrated into Stardew Valley 1.6). + * Removed obsolete console commands: `list_item_types` (no longer needed) and `player_setimmunity` (broke in 1.6 and rarely used). * Removed support for seamlessly updating from SMAPI 2.11.3 and earlier (released in 2019). _If needed, you can update to SMAPI 3.18.0 first and then install the latest version._ diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs deleted file mode 100644 index ef35ad195..000000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player -{ - /// A command which list item types. - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] - internal class ListItemTypesCommand : ConsoleCommand - { - /********* - ** Fields - *********/ - /// Provides methods for searching and constructing items. - private readonly ItemRepository Items = new(); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ListItemTypesCommand() - : base("list_item_types", "Lists item types you can filter in other commands.\n\nUsage: list_item_types") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!Context.IsWorldReady) - { - monitor.Log("You need to load a save to use this command.", LogLevel.Error); - return; - } - - // handle - ItemType[] matches = - ( - from item in this.Items.GetAll() - orderby item.Type.ToString() - select item.Type - ) - .Distinct() - .ToArray(); - string summary = "Searching...\n"; - if (matches.Any()) - monitor.Log(summary + this.GetTableString(matches, new[] { "type" }, val => new[] { val.ToString() }), LogLevel.Info); - else - monitor.Log(summary + "No item types found.", LogLevel.Info); - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs deleted file mode 100644 index 1065bd219..000000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player -{ - /// A command which edits the player's current immunity. - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] - internal class SetImmunityCommand : ConsoleCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SetImmunityCommand() - : base("player_setimmunity", "Sets the player's immunity.\n\nUsage: player_setimmunity [value]\n- value: an integer amount.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // validate - if (!args.Any()) - { - monitor.Log($"You currently have {Game1.player.immunity} immunity. Specify a value to change it.", LogLevel.Info); - return; - } - - // handle - if (args.TryGetInt(0, "amount", out int amount, min: 0)) - { - Game1.player.immunity = amount; - monitor.Log($"OK, you now have {Game1.player.immunity} immunity.", LogLevel.Info); - } - } - } -} From 2a8e496a9a6be1e60321ee7feff81a0fca89c261 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jan 2022 18:29:36 -0500 Subject: [PATCH 20/84] update for item overhaul in SDV 1.6 --- .../Framework/Commands/Player/AddCommand.cs | 118 +++-- .../Commands/Player/ListItemsCommand.cs | 5 +- .../Commands/Player/SetColorCommand.cs | 3 +- .../Commands/Player/SetMaxStaminaCommand.cs | 6 +- .../Commands/Player/SetStyleCommand.cs | 92 ++-- .../Framework/ItemData/ItemType.cs | 39 -- .../Framework/ItemRepository.cs | 405 +++++------------- .../{ItemData => }/SearchableItem.cs | 21 +- src/SMAPI/Framework/SCore.cs | 60 --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 18 +- 10 files changed, 275 insertions(+), 492 deletions(-) delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs rename src/SMAPI.Mods.ConsoleCommands/Framework/{ItemData => }/SearchableItem.cs (73%) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 74d3d9df1..1273bc705 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; using Object = StardewValley.Object; @@ -15,9 +14,6 @@ internal class AddCommand : ConsoleCommand /// Provides methods for searching and constructing items. private readonly ItemRepository Items = new(); - /// The type names recognized by this command. - private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray(); - /********* ** Public methods @@ -40,65 +36,111 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg } // read arguments - if (!args.TryGet(0, "item type", out string? type, oneOf: this.ValidTypes)) + if (!this.TryReadArguments(args, out string? id, out string? name, out int? count, out int? quality)) return; - if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) - count = 1; - if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) - quality = Object.lowQuality; // find matching item - SearchableItem? match = Enum.TryParse(type, true, out ItemType itemType) - ? this.FindItemByID(monitor, args, itemType) - : this.FindItemByName(monitor, args); + SearchableItem? match = id != null + ? this.FindItemByID(monitor, id) + : this.FindItemByName(monitor, name); if (match == null) return; // apply count - match.Item.Stack = count; + match.Item.Stack = count ?? 1; // apply quality - if (match.Item is Object obj) - obj.Quality = quality; - else if (match.Item is Tool tool) - tool.UpgradeLevel = quality; + if (quality != null) + { + if (match.Item is Object obj) + obj.Quality = quality.Value; + else if (match.Item is Tool tool && args.Count >= 3) + tool.UpgradeLevel = quality.Value; + } // add to inventory Game1.player.addItemByMenuIfNecessary(match.Item); - monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); + monitor.Log($"OK, added {match.Name} (ID: {match.QualifiedItemId}) to your inventory.", LogLevel.Info); } /********* ** Private methods *********/ + /// Parse the arguments from the user if they're valid. + /// The arguments to parse. + /// The ID of the item to add, or null if searching by . + /// The name of the item to add, or null if searching by . + /// The number of the item to add. + /// The item quality to set. + /// Returns whether the arguments are valid. + private bool TryReadArguments(ArgumentParser args, out string? id, out string? name, out int? count, out int? quality) + { + // get id or 'name' flag + if (!args.TryGet(0, "id or 'name'", out id, required: true)) + { + name = null; + count = null; + quality = null; + return false; + } + + // get name + int argOffset = 0; + if (string.Equals(id, "name", StringComparison.OrdinalIgnoreCase)) + { + id = null; + if (!args.TryGet(1, "item name", out name)) + { + count = null; + quality = null; + return false; + } + + argOffset = 1; + } + else + name = null; + + // get count + count = null; + if (args.TryGetInt(1 + argOffset, "count", out int rawCount, min: 1, required: false)) + count = rawCount; + + // get quality + quality = null; + if (args.TryGetInt(2 + argOffset, "quality", out int rawQuality, min: Object.lowQuality, max: Object.bestQuality, required: false)) + quality = rawQuality; + + return true; + } + + /// Get a matching item by its ID. /// Writes messages to the console and log file. - /// The command arguments. - /// The item type. - private SearchableItem? FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type) + /// The qualified item ID. + private SearchableItem? FindItemByID(IMonitor monitor, string id) { - // read arguments - if (!args.TryGetInt(1, "item ID", out int id, min: 0)) - return null; + SearchableItem? item = this.Items + .GetAll() + .Where(p => string.Equals(p.QualifiedItemId, id, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(p => p.QualifiedItemId == id) // prefer case-sensitive match + .FirstOrDefault(); - // find matching item - SearchableItem? item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); if (item == null) - monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); + monitor.Log($"There's no item with the qualified ID {id}.", LogLevel.Error); + return item; } /// Get a matching item by its name. /// Writes messages to the console and log file. - /// The command arguments. - private SearchableItem? FindItemByName(IMonitor monitor, ArgumentParser args) + /// The partial item name to match. + private SearchableItem? FindItemByName(IMonitor monitor, string? name) { - // read arguments - if (!args.TryGet(1, "item name", out string? name)) + if (string.IsNullOrWhiteSpace(name)) return null; - // find matching items SearchableItem[] matches = this.Items.GetAll().Where(p => p.NameContains(name)).ToArray(); if (!matches.Any()) { @@ -115,7 +157,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg string options = this.GetTableString( data: matches, header: new[] { "type", "name", "command" }, - getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" } + getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.QualifiedItemId}" } ); monitor.Log($"There's no item with name '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info); return null; @@ -124,17 +166,15 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg /// Get the command description. private static string GetDescription() { - string[] typeValues = Enum.GetNames(typeof(ItemType)); return "Gives the player an item.\n" + "\n" - + "Usage: player_add [count] [quality]\n" - + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" - + "- item: the item ID (use the 'list_items' command to see a list).\n" + + "Usage: player_add [count] [quality]\n" + + "- item id: the item ID (use the 'list_items' command to see a list).\n" + "- count (optional): how many of the item to give.\n" + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + "\n" - + "Usage: player_add name \"\" [count] [quality]\n" - + "- name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items.\n" + + "Usage: player_add name \"\" [count] [quality]\n" + + "- item name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items.\n" + "- count (optional): how many of the item to give.\n" + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + "\n" diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index 73d5b79dd..1f949f303 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { @@ -47,7 +46,7 @@ select item .ToArray(); string summary = "Searching...\n"; if (matches.Any()) - monitor.Log(summary + this.GetTableString(matches, new[] { "type", "name", "id" }, val => new[] { val.Type.ToString(), val.Name, val.ID.ToString() }), LogLevel.Info); + monitor.Log(summary + this.GetTableString(matches, new[] { "name", "id" }, val => new[] { val.Name, val.QualifiedItemId }), LogLevel.Info); else monitor.Log(summary + "No items found", LogLevel.Info); } @@ -67,7 +66,7 @@ private IEnumerable GetItems(string[] searchWords) // find matches return ( from item in this.Items.GetAll() - let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}" + let term = $"{item.QualifiedItemId}|{item.Type}|{item.Name}|{item.DisplayName}" where getAll || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1) select item ); diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index ea9f1d829..d267ae21b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -48,7 +48,8 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg break; case "pants": - Game1.player.pantsColor.Value = color; + Game1.player.changePantsColor(color); + Game1.player.UpdateClothing(); monitor.Log("OK, your pants color is updated.", LogLevel.Info); break; } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs index 8c794e757..a17d039fd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs @@ -24,15 +24,15 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // validate if (!args.Any()) { - monitor.Log($"You currently have {Game1.player.MaxStamina} max stamina. Specify a value to change it.", LogLevel.Info); + monitor.Log($"You currently have {Game1.player.maxStamina} base max stamina. Specify a value to change it.", LogLevel.Info); return; } // handle if (args.TryGetInt(0, "amount", out int amount, min: 1)) { - Game1.player.MaxStamina = amount; - monitor.Log($"OK, you now have {Game1.player.MaxStamina} max stamina.", LogLevel.Info); + Game1.player.maxStamina.Value = amount; + monitor.Log($"OK, you now have {Game1.player.maxStamina.Value} base max stamina.", LogLevel.Info); } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs index 8193ff27c..473faad86 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -12,7 +12,7 @@ internal class SetStyleCommand : ConsoleCommand *********/ /// Construct an instance. public SetStyleCommand() - : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changestyle .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.") { } + : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changestyle .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the style ID.") { } /// Handle the command. /// Writes messages to the console and log file. @@ -23,15 +23,27 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // parse arguments if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" })) return; - if (!args.TryGetInt(1, "style ID", out int styleID)) + if (!args.TryGet(1, "style ID", out string? styleID)) return; + bool AssertIntStyle(out int id) + { + if (int.TryParse(styleID, out id)) + return true; + + monitor.Log($"The style ID must be a numeric integer for the '{target}' target.", LogLevel.Error); + return false; + } + // handle switch (target) { case "hair": - Game1.player.changeHairStyle(styleID); - monitor.Log("OK, your hair style is updated.", LogLevel.Info); + if (AssertIntStyle(out int hairId)) + { + Game1.player.changeHairStyle(hairId); + monitor.Log("OK, your hair style is updated.", LogLevel.Info); + } break; case "shirt": @@ -40,13 +52,19 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg break; case "acc": - Game1.player.changeAccessory(styleID); - monitor.Log("OK, your accessory style is updated.", LogLevel.Info); + if (AssertIntStyle(out int accId)) + { + Game1.player.changeAccessory(accId); + monitor.Log("OK, your accessory style is updated.", LogLevel.Info); + } break; case "skin": - Game1.player.changeSkinColor(styleID); - monitor.Log("OK, your skin color is updated.", LogLevel.Info); + if (AssertIntStyle(out int skinId)) + { + Game1.player.changeSkinColor(skinId); + monitor.Log("OK, your skin color is updated.", LogLevel.Info); + } break; case "shoe": @@ -55,36 +73,46 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg break; case "swim": - switch (styleID) + if (AssertIntStyle(out int swimId)) { - case 0: - Game1.player.changeOutOfSwimSuit(); - monitor.Log("OK, you're no longer in your swimming suit.", LogLevel.Info); - break; - case 1: - Game1.player.changeIntoSwimsuit(); - monitor.Log("OK, you're now in your swimming suit.", LogLevel.Info); - break; - default: - this.LogUsageError(monitor, "The swim value should be 0 (no swimming suit) or 1 (swimming suit)."); - break; + switch (swimId) + { + case 0: + Game1.player.changeOutOfSwimSuit(); + monitor.Log("OK, you're no longer in your swimming suit.", LogLevel.Info); + break; + + case 1: + Game1.player.changeIntoSwimsuit(); + monitor.Log("OK, you're now in your swimming suit.", LogLevel.Info); + break; + + default: + this.LogUsageError(monitor, "The swim value should be 0 (no swimming suit) or 1 (swimming suit)."); + break; + } } break; case "gender": - switch (styleID) + if (AssertIntStyle(out int genderId)) { - case 0: - Game1.player.changeGender(true); - monitor.Log("OK, you're now male.", LogLevel.Info); - break; - case 1: - Game1.player.changeGender(false); - monitor.Log("OK, you're now female.", LogLevel.Info); - break; - default: - this.LogUsageError(monitor, "The gender value should be 0 (male) or 1 (female)."); - break; + switch (genderId) + { + case 0: + Game1.player.changeGender(true); + monitor.Log("OK, you're now male.", LogLevel.Info); + break; + + case 1: + Game1.player.changeGender(false); + monitor.Log("OK, you're now female.", LogLevel.Info); + break; + + default: + this.LogUsageError(monitor, "The gender value should be 0 (male) or 1 (female)."); + break; + } } break; } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs deleted file mode 100644 index 5d269c896..000000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData -{ - /// An item type that can be searched and added to the player through the console. - internal enum ItemType - { - /// A big craftable object in - BigCraftable, - - /// A item. - Boots, - - /// A item. - Clothing, - - /// A flooring item. - Flooring, - - /// A item. - Furniture, - - /// A item. - Hat, - - /// Any object in (except rings). - Object, - - /// A item. - Ring, - - /// A tool. - Tool, - - /// A wall item. - Wallpaper, - - /// A or item. - Weapon - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 52bf149ec..d8ebe4dac 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -2,14 +2,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; -using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; +using StardewValley.Buildings; using StardewValley.GameData.FishPond; -using StardewValley.Menus; +using StardewValley.ItemTypeDefinitions; using StardewValley.Objects; -using StardewValley.Tools; using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework @@ -17,21 +15,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework /// Provides methods for searching and constructing items. internal class ItemRepository { - /********* - ** Fields - *********/ - /// The custom ID offset for items don't have a unique ID in the game. - private readonly int CustomIDOffset = 1000; - - /********* ** Public methods *********/ /// Get all spawnable items. - /// The item types to fetch (or null for any type). + /// Only include items for the given . /// Whether to include flavored variants like "Sunflower Honey". [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = $"{nameof(ItemRepository.TryCreate)} invokes the lambda immediately.")] - public IEnumerable GetAll(ItemType[]? itemTypes = null, bool includeVariants = true) + public IEnumerable GetAll(string? onlyType = null, bool includeVariants = true) { // // @@ -45,161 +36,79 @@ public IEnumerable GetAll(ItemType[]? itemTypes = null, bool inc IEnumerable GetAllRaw() { - HashSet? types = itemTypes?.Any() == true ? new HashSet(itemTypes) : null; - bool ShouldGet(ItemType type) => types == null || types.Contains(type); - - // get tools - if (ShouldGet(ItemType.Tool)) + // get from item data definitions + foreach (IItemDataDefinition itemType in ItemRegistry.ItemTypes) { - for (int q = Tool.stone; q <= Tool.iridium; q++) + if (onlyType != null && itemType.Identifier != onlyType) + continue; + + switch (itemType.Identifier) { - int quality = q; - - yield return this.TryCreate(ItemType.Tool, ToolFactory.axe, _ => ToolFactory.getToolFromDescription(ToolFactory.axe, quality)); - yield return this.TryCreate(ItemType.Tool, ToolFactory.hoe, _ => ToolFactory.getToolFromDescription(ToolFactory.hoe, quality)); - yield return this.TryCreate(ItemType.Tool, ToolFactory.pickAxe, _ => ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality)); - yield return this.TryCreate(ItemType.Tool, ToolFactory.wateringCan, _ => ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality)); - if (quality != Tool.iridium) - yield return this.TryCreate(ItemType.Tool, ToolFactory.fishingRod, _ => ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality)); - } - yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset, _ => new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones - yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 1, _ => new Shears()); - yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 2, _ => new Pan()); - yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 3, _ => new Wand()); - } + // objects + case "(O)": + { + ObjectDataDefinition objectDataDefinition = (ObjectDataDefinition)ItemRegistry.GetTypeDefinition(ItemRegistry.type_object); - // clothing - if (ShouldGet(ItemType.Clothing)) - { - foreach (int id in this.GetShirtIds()) - yield return this.TryCreate(ItemType.Clothing, id, p => new Clothing(p.ID)); - } + foreach (string id in itemType.GetAllIds()) + { + // base item + SearchableItem? result = this.TryCreate(itemType.Identifier, id, p => ItemRegistry.Create(itemType.Identifier + p.Id)); - // wallpapers - if (ShouldGet(ItemType.Wallpaper)) - { - for (int id = 0; id < 112; id++) - yield return this.TryCreate(ItemType.Wallpaper, id, p => new Wallpaper(p.ID) { Category = SObject.furnitureCategory }); - } + // ring + if (result?.Item is Ring) + yield return result; - // flooring - if (ShouldGet(ItemType.Flooring)) - { - for (int id = 0; id < 56; id++) - yield return this.TryCreate(ItemType.Flooring, id, p => new Wallpaper(p.ID, isFloor: true) { Category = SObject.furnitureCategory }); - } + // journal scraps + else if (result?.QualifiedItemId == "(O)842") + { + foreach (SearchableItem? journalScrap in this.GetSecretNotes(itemType, isJournalScrap: true)) + yield return journalScrap; + } - // equipment - if (ShouldGet(ItemType.Boots)) - { - foreach (int id in this.TryLoad("Data\\Boots").Keys) - yield return this.TryCreate(ItemType.Boots, id, p => new Boots(p.ID)); - } - if (ShouldGet(ItemType.Hat)) - { - foreach (int id in this.TryLoad("Data\\hats").Keys) - yield return this.TryCreate(ItemType.Hat, id, p => new Hat(p.ID)); - } + // secret notes + else if (result?.QualifiedItemId == "(O)79") + { + foreach (SearchableItem? secretNote in this.GetSecretNotes(itemType, isJournalScrap: false)) + yield return secretNote; + } - // weapons - if (ShouldGet(ItemType.Weapon)) - { - Dictionary weaponsData = this.TryLoad("Data\\weapons"); - foreach (KeyValuePair pair in weaponsData) - { - string rawFields = pair.Value; - yield return this.TryCreate(ItemType.Weapon, pair.Key, p => - { - string[] fields = rawFields.Split('/'); - bool isSlingshot = fields.Length > 8 && fields[8] == "4"; - return isSlingshot - ? new Slingshot(p.ID) - : new MeleeWeapon(p.ID); - }); - } - } + // object + else + { + yield return result?.QualifiedItemId == "(O)340" + ? this.TryCreate(itemType.Identifier, result.Id, _ => objectDataDefinition.CreateFlavoredHoney(null)) // game creates "Wild Honey" when there's no ingredient, instead of the base Honey item + : result; + + if (includeVariants) + { + foreach (SearchableItem? variant in this.GetFlavoredObjectVariants(objectDataDefinition, result?.Item as SObject, itemType)) + yield return variant; + } + } + } + } + break; - // furniture - if (ShouldGet(ItemType.Furniture)) - { - foreach (int id in this.TryLoad("Data\\Furniture").Keys) - yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID)); + // no special handling needed + default: + foreach (string id in itemType.GetAllIds()) + yield return this.TryCreate(itemType.Identifier, id, p => ItemRegistry.Create(itemType.Identifier + p.Id)); + break; + } } - // craftables - if (ShouldGet(ItemType.BigCraftable)) + // wallpapers + if (onlyType is null or "(WP)") { - foreach (int id in Game1.bigCraftablesInformation.Keys) - yield return this.TryCreate(ItemType.BigCraftable, id, p => new SObject(Vector2.Zero, p.ID)); + for (int id = 0; id < 112; id++) + yield return this.TryCreate("(WP)", id.ToString(), p => new Wallpaper(int.Parse(p.Id)) { Category = SObject.furnitureCategory }); } - // objects - if (ShouldGet(ItemType.Object) || ShouldGet(ItemType.Ring)) + // flooring + if (onlyType is null or "(FL)") { - foreach (int id in Game1.objectInformation.Keys) - { - string[]? fields = Game1.objectInformation[id]?.Split('/'); - - // ring - if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring - { - if (ShouldGet(ItemType.Ring)) - yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID)); - } - - // journal scrap - else if (id == 842) - { - if (ShouldGet(ItemType.Object)) - { - foreach (SearchableItem? journalScrap in this.GetSecretNotes(isJournalScrap: true)) - yield return journalScrap; - } - } - - // secret notes - else if (id == 79) - { - if (ShouldGet(ItemType.Object)) - { - foreach (SearchableItem? secretNote in this.GetSecretNotes(isJournalScrap: false)) - yield return secretNote; - } - } - - // object - else if (ShouldGet(ItemType.Object)) - { - // spawn main item - SearchableItem? mainItem = this.TryCreate(ItemType.Object, id, p => - { - // roe - if (p.ID == 812) - return new ColoredObject(p.ID, 1, Color.White); - - // Wild Honey - if (p.ID == 340) - { - return new SObject(Vector2.Zero, 340, "Wild Honey", false, true, false, false) - { - Name = "Wild Honey", - preservedParentSheetIndex = { -1 } - }; - } - - // else plain item - return new SObject(p.ID, 1); - }); - yield return mainItem; - - // flavored items - if (includeVariants && mainItem?.Item != null) - { - foreach (SearchableItem? variant in this.GetFlavoredObjectVariants((SObject)mainItem.Item)) - yield return variant; - } - } - } + for (int id = 0; id < 56; id++) + yield return this.TryCreate("(FL)", id.ToString(), p => new Wallpaper(int.Parse(p.Id), isFloor: true) { Category = SObject.furnitureCategory }); } } @@ -215,12 +124,13 @@ select item ** Private methods *********/ /// Get the individual secret note or journal scrap items. + /// The object data definition. /// Whether to get journal scraps. /// Derived from . - private IEnumerable GetSecretNotes(bool isJournalScrap) + private IEnumerable GetSecretNotes(IItemDataDefinition itemType, bool isJournalScrap) { // get base item ID - int baseId = isJournalScrap ? 842 : 79; + string baseId = isJournalScrap ? "842" : "79"; // get secret note IDs var ids = this @@ -236,15 +146,13 @@ select item ); // build items - foreach (int id in ids) + foreach (int i in ids) { - int fakeId = this.CustomIDOffset * 8 + id; - if (isJournalScrap) - fakeId += GameLocation.JOURNAL_INDEX; + int id = i; // avoid closure capture - yield return this.TryCreate(ItemType.Object, fakeId, _ => + yield return this.TryCreate(itemType.Identifier, $"{baseId}/{id}", _ => { - SObject note = new(baseId, 1); + Item note = ItemRegistry.Create(itemType.Identifier + baseId); note.Name = $"{note.Name} #{id}"; return note; }); @@ -252,78 +160,45 @@ select item } /// Get flavored variants of a base item (like Blueberry Wine for Blueberry), if any. + /// The item data definition for object items. /// A sample of the base item. - private IEnumerable GetFlavoredObjectVariants(SObject item) + /// The object data definition. + private IEnumerable GetFlavoredObjectVariants(ObjectDataDefinition objectDataDefinition, SObject? item, IItemDataDefinition itemType) { - int id = item.ParentSheetIndex; + if (item is null) + yield break; + + string id = item.ItemId; + // by category switch (item.Category) { // fruit products case SObject.FruitsCategory: - // wine - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + id, _ => new SObject(348, 1) - { - Name = $"{item.Name} Wine", - Price = item.Price * 3, - preserve = { SObject.PreserveType.Wine }, - preservedParentSheetIndex = { id } - }); - - // jelly - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + id, _ => new SObject(344, 1) - { - Name = $"{item.Name} Jelly", - Price = 50 + item.Price * 2, - preserve = { SObject.PreserveType.Jelly }, - preservedParentSheetIndex = { id } - }); + yield return this.TryCreate(itemType.Identifier, $"348/{id}", _ => objectDataDefinition.CreateFlavoredWine(item)); + yield return this.TryCreate(itemType.Identifier, $"344/{id}", _ => objectDataDefinition.CreateFlavoredJelly(item)); break; // vegetable products case SObject.VegetableCategory: - // juice - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + id, _ => new SObject(350, 1) - { - Name = $"{item.Name} Juice", - Price = (int)(item.Price * 2.25d), - preserve = { SObject.PreserveType.Juice }, - preservedParentSheetIndex = { id } - }); - - // pickled - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => new SObject(342, 1) - { - Name = $"Pickled {item.Name}", - Price = 50 + item.Price * 2, - preserve = { SObject.PreserveType.Pickle }, - preservedParentSheetIndex = { id } - }); + yield return this.TryCreate(itemType.Identifier, $"350/{id}", _ => objectDataDefinition.CreateFlavoredJuice(item)); + yield return this.TryCreate(itemType.Identifier, $"342/{id}", _ => objectDataDefinition.CreateFlavoredPickle(item)); break; // flower honey case SObject.flowersCategory: - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => - { - SObject honey = new(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false) - { - Name = $"{item.Name} Honey", - preservedParentSheetIndex = { id } - }; - honey.Price += item.Price * 2; - return honey; - }); + yield return this.TryCreate(itemType.Identifier, $"340/{id}", _ => objectDataDefinition.CreateFlavoredHoney(item)); break; // roe and aged roe (derived from FishPond.GetFishProduce) - case SObject.sellAtFishShopCategory when id == 812: + case SObject.sellAtFishShopCategory when item.QualifiedItemId == "(O)812": { this.GetRoeContextTagLookups(out HashSet simpleTags, out List> complexTags); - foreach (var pair in Game1.objectInformation) + foreach (string key in Game1.objectData.Keys) { // get input - SObject? input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject; + SObject? input = this.TryCreate(itemType.Identifier, key, p => new SObject(p.Id, 1))?.Item as SObject; if (input == null) continue; @@ -335,49 +210,21 @@ select item if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag)))) continue; - // yield roe - SObject? roe = null; - Color color = this.GetRoeColor(input); - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ => - { - roe = new ColoredObject(812, 1, color) - { - name = $"{input.Name} Roe", - preserve = { Value = SObject.PreserveType.Roe }, - preservedParentSheetIndex = { Value = input.ParentSheetIndex } - }; - roe.Price += input.Price / 2; - return roe; - }); - - // aged roe - if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item - { - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ => new ColoredObject(447, 1, color) - { - name = $"Aged {input.Name} Roe", - Category = -27, - preserve = { Value = SObject.PreserveType.AgedRoe }, - preservedParentSheetIndex = { Value = input.ParentSheetIndex }, - Price = roe.Price * 2 - }); - } + // create roe + SearchableItem? roe = this.TryCreate(itemType.Identifier, $"812/{input.ItemId}", _ => objectDataDefinition.CreateFlavoredRoe(input)); + yield return roe; + + // create aged roe + if (roe?.Item is SObject roeObj && input.QualifiedItemId != "(O)698") // skip aged sturgeon roe (which is a separate caviar item) + yield return this.TryCreate(itemType.Identifier, $"447/{input.ItemId}", _ => objectDataDefinition.CreateFlavoredAgedRoe(roeObj)); } } break; } - // ginger => pickled ginger - if (id == 829 && item.Category != SObject.VegetableCategory) - { - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => new SObject(342, 1) - { - Name = $"Pickled {item.Name}", - Price = 50 + item.Price * 2, - preserve = { SObject.PreserveType.Pickle }, - preservedParentSheetIndex = { id } - }); - } + // by context tag + if (item.HasContextTag("preserves_pickle") && item.Category != SObject.VegetableCategory) + yield return this.TryCreate(itemType.Identifier, $"342/{id}", _ => objectDataDefinition.CreateFlavoredPickle(item)); } /// Get optimized lookups to match items which produce roe in a fish pond. @@ -390,7 +237,7 @@ private void GetRoeContextTagLookups(out HashSet simpleTags, out List
  • >("Data\\FishPondData")) { - if (data.ProducedItems.All(p => p.ItemID != 812)) + if (data.ProducedItems.All(p => p.ItemId is not ("812" or "(O)812"))) continue; // doesn't produce roe if (data.RequiredTags.Count == 1 && !data.RequiredTags[0].StartsWith("!")) @@ -420,14 +267,18 @@ private Dictionary TryLoad(string assetName) /// Create a searchable item if valid. /// The item type. - /// The unique ID (if different from the item's parent sheet index). + /// The locally unique item key. /// Create an item instance. - private SearchableItem? TryCreate(ItemType type, int id, Func createItem) + private SearchableItem? TryCreate(string type, string key, Func createItem) { try { - var item = new SearchableItem(type, id, createItem); + SearchableItem item = new SearchableItem(type, key, createItem); item.Item.getDescription(); // force-load item data, so it crashes here if it's invalid + + if (item.Item.Name is null or "Error Item") + return null; + return item; } catch @@ -435,53 +286,5 @@ private Dictionary TryLoad(string assetName) return null; // if some item data is invalid, just don't include it } } - - /// Get the color to use a given fish's roe. - /// The fish whose roe to color. - /// Derived from . - private Color GetRoeColor(SObject fish) - { - return fish.ParentSheetIndex == 698 // sturgeon - ? new Color(61, 55, 42) - : (TailoringMenu.GetDyeColor(fish) ?? Color.Orange); - } - - /// Get valid shirt IDs. - /// - /// Shirts have a possible range of 1000–1999, but not all of those IDs are valid. There are two sets of IDs: - /// - /// - /// - /// Shirts which exist in . - /// - /// - /// Shirts with a dynamic ID and no entry in . These automatically - /// use the generic shirt entry with ID -1 and are mapped to a calculated position in the - /// Characters/Farmer/shirts spritesheet. There's no constant we can use, but some known valid - /// ranges are 1000–1111 (used in for the customization screen and - /// 1000–1127 (used in and ). - /// Based on the spritesheet, the max valid ID is 1299. - /// - /// - /// - private IEnumerable GetShirtIds() - { - // defined shirt items - foreach (int id in Game1.clothingInformation.Keys) - { - if (id < 0) - continue; // placeholder data for character customization clothing below - - yield return id; - } - - // dynamic shirts - HashSet clothingIds = new HashSet(Game1.clothingInformation.Keys); - for (int id = 1000; id <= 1299; id++) - { - if (!clothingIds.Contains(id)) - yield return id; - } - } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/SearchableItem.cs similarity index 73% rename from src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs rename to src/SMAPI.Mods.ConsoleCommands/Framework/SearchableItem.cs index 3675a9631..a931d2065 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/SearchableItem.cs @@ -1,7 +1,8 @@ using System; using StardewValley; +using StardewValley.ItemTypeDefinitions; -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework { /// A game item with metadata. internal class SearchableItem @@ -9,8 +10,8 @@ internal class SearchableItem /********* ** Accessors *********/ - /// The item type. - public ItemType Type { get; } + /// The value for the item type. + public string Type { get; } /// A sample item instance. public Item Item { get; } @@ -18,8 +19,11 @@ internal class SearchableItem /// Create an item instance. public Func CreateItem { get; } - /// The item's unique ID for its type. - public int ID { get; } + /// The unqualified item ID. + public string Id { get; } + + /// The qualified item ID. + public string QualifiedItemId { get; } /// The item's default name. public string Name => this.Item.Name; @@ -33,12 +37,13 @@ internal class SearchableItem *********/ /// Construct an instance. /// The item type. - /// The unique ID (if different from the item's parent sheet index). + /// The unqualified item ID. /// Create an item instance. - public SearchableItem(ItemType type, int id, Func createItem) + public SearchableItem(string type, string id, Func createItem) { this.Type = type; - this.ID = id; + this.Id = id; + this.QualifiedItemId = this.Type + this.Id; this.CreateItem = () => createItem(this); this.Item = createItem(this); } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index cb8610dd8..c79c971ee 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -50,7 +50,6 @@ using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode; using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix; using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -464,10 +463,6 @@ private void InitializeBeforeFirstAssetLoaded() /// Raised after the game finishes initializing. private void OnGameInitialized() { - // validate XNB integrity - if (!this.ValidateContentIntegrity()) - this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); - // start SMAPI console if (this.Settings.ListenForConsoleInput) { @@ -1355,61 +1350,6 @@ private SGame GetCurrentGameInstance() ?? throw new InvalidOperationException("The current game instance wasn't created by SMAPI."); } - /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. - /// Returns whether all integrity checks passed. - private bool ValidateContentIntegrity() - { - this.Monitor.Log("Detecting common issues..."); - bool issuesFound = false; - - // object format (commonly broken by outdated files) - { - // detect issues - bool hasObjectIssues = false; - void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue})."); - foreach ((int id, string? fieldsStr) in Game1.objectInformation) - { - // must not be empty - if (string.IsNullOrWhiteSpace(fieldsStr)) - { - LogIssue(id, "entry is empty"); - hasObjectIssues = true; - continue; - } - - // require core fields - string[] fields = fieldsStr.Split('/'); - if (fields.Length < SObject.objectInfoDescriptionIndex + 1) - { - LogIssue(id, "too few fields for an object"); - hasObjectIssues = true; - continue; - } - - // check min length for specific types - switch (fields[SObject.objectInfoTypeIndex].Split(' ', 2)[0]) - { - case "Cooking": - if (fields.Length < SObject.objectInfoBuffDurationIndex + 1) - { - LogIssue(id, "too few fields for a cooking item"); - hasObjectIssues = true; - } - break; - } - } - - // log error - if (hasObjectIssues) - { - issuesFound = true; - this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); - } - } - - return !issuesFound; - } - /// Set the titles for the game and console windows. private void UpdateWindowTitles() { diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b48b6f96b..7cc6d0467 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -14,7 +14,9 @@ using StardewValley.BellsAndWhistles; using StardewValley.Buildings; using StardewValley.Characters; +using StardewValley.GameData.BigCraftables; using StardewValley.GameData.Movies; +using StardewValley.GameData.Objects; using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Objects; @@ -294,12 +296,14 @@ static ISet GetWarpSet(GameLocation location) Game1.achievements = content.Load>(key); return true; - case "data/bigcraftablesinformation": // Game1.LoadContent - Game1.bigCraftablesInformation = content.Load>(key); + case "data/bigcraftables": // Game1.LoadContent + Game1.bigCraftableData = content.Load>(key); + ItemRegistry.ResetCache(); return true; case "data/clothinginformation": // Game1.LoadContent - Game1.clothingInformation = content.Load>(key); + Game1.clothingInformation = content.Load>(key); + ItemRegistry.ResetCache(); return true; case "data/concessions": // MovieTheater.GetConcessions @@ -342,8 +346,9 @@ static ISet GetWarpSet(GameLocation location) Game1.objectContextTags = content.Load>(key); return true; - case "data/objectinformation": // Game1.LoadContent - Game1.objectInformation = content.Load>(key); + case "data/objects": // Game1.LoadContent + Game1.objectData = content.Load>(key); + ItemRegistry.ResetCache(); return true; /**** @@ -550,6 +555,7 @@ static ISet GetWarpSet(GameLocation location) case "tilesheets/furniture": // Game1.LoadContent Furniture.furnitureTexture = content.Load(key); + ItemRegistry.ResetCache(); return true; case "tilesheets/furniturefront": // Game1.LoadContent @@ -728,7 +734,7 @@ private bool UpdateFarmAnimalSprites(IAssetName assetName) string expectedKey = animal.age.Value < animal.ageWhenMature.Value ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}" : animal.type.Value; - if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value <= 0) + if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value == null) expectedKey = $"Sheared{expectedKey}"; expectedKey = $"Animals/{expectedKey}"; From 65ee55fa487633916511da8687b58082f1db1621 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jan 2022 18:41:35 -0500 Subject: [PATCH 21/84] update for tree changes in SDV 1.6 --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 35 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 7cc6d0467..b6ac44df9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -550,8 +550,7 @@ static ISet GetWarpSet(GameLocation location) return true; case "tilesheets/fruittrees": // FruitTree - FruitTree.texture = content.Load(key); - return true; + return this.UpdateDefaultFruitTreeTexture(content, assetName); case "tilesheets/furniture": // Game1.LoadContent Furniture.furnitureTexture = content.Load(key); @@ -605,27 +604,27 @@ static ISet GetWarpSet(GameLocation location) return true; case "terrainfeatures/mushroom_tree": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree)); + return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree.ToString())); case "terrainfeatures/tree_palm": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.palmTree)); + return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.palmTree.ToString())); case "terrainfeatures/tree1_fall": // from Tree case "terrainfeatures/tree1_spring": // from Tree case "terrainfeatures/tree1_summer": // from Tree case "terrainfeatures/tree1_winter": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree)); + return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree.ToString())); case "terrainfeatures/tree2_fall": // from Tree case "terrainfeatures/tree2_spring": // from Tree case "terrainfeatures/tree2_summer": // from Tree case "terrainfeatures/tree2_winter": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree)); + return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree.ToString())); case "terrainfeatures/tree3_fall": // from Tree case "terrainfeatures/tree3_spring": // from Tree case "terrainfeatures/tree3_winter": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.pineTree)); + return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.pineTree.ToString())); } /**** @@ -1026,6 +1025,28 @@ private bool UpdateSuspensionBridges(LocalizedContentManager content, IAssetName return texture.IsValueCreated; } + /// Update fruit trees which use the default texture. + /// The content manager through which to reload the asset. + /// The asset name to reload. + /// Returns whether any textures were reloaded. + private bool UpdateDefaultFruitTreeTexture(LocalizedContentManager content, IAssetName assetName) + { + FruitTree[] trees = this.GetLocations() + .SelectMany(p => p.terrainFeatures.Values.OfType()) + .Where(p => assetName.IsEquivalentTo(p.textureOverride.Value)) + .ToArray(); + + if (trees.Any()) + { + Texture2D texture = content.Load(assetName.Name); + foreach (FruitTree tree in trees) + tree.texture = texture; + return true; + } + + return false; + } + /// Update tree textures. /// The type to update. /// Returns whether any references were updated. From 83ffe2f72f23458fe921995012b5acc705f7e665 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jan 2022 19:57:52 -0500 Subject: [PATCH 22/84] update for removed BuildableGameLocation in SDV 1.6 --- src/SMAPI/Framework/StateTracking/LocationTracker.cs | 2 +- src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs | 6 ++---- src/SMAPI/Metadata/CoreAssetPropagator.cs | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs index 790c71ddb..47fe90a2e 100644 --- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs +++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs @@ -70,7 +70,7 @@ public LocationTracker(GameLocation location) this.Location = location; // init watchers - this.BuildingsWatcher = location is BuildableGameLocation buildableLocation ? WatcherFactory.ForNetCollection($"{this.Name}.{nameof(buildableLocation.buildings)}", buildableLocation.buildings) : WatcherFactory.ForImmutableCollection(); + this.BuildingsWatcher = WatcherFactory.ForNetCollection($"{this.Name}.{nameof(location.buildings)}", location.buildings); this.DebrisWatcher = WatcherFactory.ForNetCollection($"{this.Name}.{nameof(location.debris)}", location.debris); this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection($"{this.Name}.{nameof(location.largeTerrainFeatures)}", location.largeTerrainFeatures); this.NpcsWatcher = WatcherFactory.ForNetCollection($"{this.Name}.{nameof(location.characters)}", location.characters); diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs index ca6988adc..8684444b9 100644 --- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs +++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -214,8 +214,7 @@ public void Add(GameLocation? location) this.LocationDict[location] = new LocationTracker(location); // add buildings - if (location is BuildableGameLocation buildableLocation) - this.Add(buildableLocation.buildings); + this.Add(location.buildings); } /// Remove the given building. @@ -244,8 +243,7 @@ public void Remove(GameLocation? location) // remove this.LocationDict.Remove(location); watcher.Dispose(); - if (location is BuildableGameLocation buildableLocation) - this.Remove(buildableLocation.buildings); + this.Remove(location.buildings); } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b6ac44df9..ab853977f 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -760,7 +760,6 @@ private bool UpdateBuildings(IAssetName assetName) // get buildings Building[] buildings = this.GetLocations(buildingInteriors: false) - .OfType() .SelectMany(p => p.buildings) .Where(p => p.buildingType.Value == type) .ToArray(); @@ -1346,7 +1345,7 @@ private IEnumerable GetLocationsWithInfo(bool buildingInteriors = // get child locations if (buildingInteriors) { - foreach (BuildableGameLocation location in locations.Select(p => p.Location).OfType().ToArray()) + foreach (GameLocation location in locations.Select(p => p.Location).ToArray()) { foreach (Building building in location.buildings) { From caaa4a7d4e311be726754a2c6898ffd51075990f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 Feb 2022 17:55:58 -0500 Subject: [PATCH 23/84] update for removed Woods.clump in SDV 1.6 --- .../Framework/Commands/World/ClearCommand.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 4905b89ad..9dd2c8bd4 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -3,7 +3,6 @@ using System.Linq; using Microsoft.Xna.Framework; using StardewValley; -using StardewValley.Locations; using StardewValley.Objects; using StardewValley.TerrainFeatures; using SObject = StardewValley.Object; @@ -233,15 +232,6 @@ private int RemoveResourceClumps(GameLocation location, Func Date: Thu, 20 Jan 2022 18:41:45 -0500 Subject: [PATCH 24/84] update draw logic for SDV 1.6 --- src/SMAPI/Framework/SGame.cs | 208 ++++++++++++++--------------------- 1 file changed, 85 insertions(+), 123 deletions(-) diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index feb0988a1..a3d60a3be 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -247,7 +247,10 @@ protected override void _draw(GameTime gameTime, RenderTarget2D target_screen) /// A snapshot of the game timing state. /// The render target, if any. /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "ForCanBeConvertedToForeach", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")] @@ -260,8 +263,10 @@ protected override void _draw(GameTime gameTime, RenderTarget2D target_screen) [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "ReturnValueOfPureMethodIsNotUsed", Justification = "copied from game code as-is")] [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Deliberate to minimize chance of errors when copying event calls into new versions of this code.")] private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) @@ -418,11 +423,7 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) base.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); events.Rendering.RaiseEmpty(); - string addOn = ""; - for (int i = 0; (double)i < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; i++) - { - addOn += "."; - } + string addOn = "".PadRight((int)Math.Ceiling(gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0), '.'); string text = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); string msg = text + addOn; string largestMessage = text + "... "; @@ -437,13 +438,10 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) Game1.PopUIMode(); return; } - - byte batchOpens = 0; // used for rendering event if (Game1.gameMode == 0) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (++batchOpens == 1) - events.Rendering.RaiseEmpty(); + events.Rendering.RaiseEmpty(); } else { @@ -452,60 +450,15 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) //base.Draw(gameTime); return; } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + bool renderingRaised = false; if (Game1.drawLighting) { - Game1.SetRenderTarget(Game1.lightmap); - base.GraphicsDevice.Clear(Color.White * 0f); - Matrix lighting_matrix = Matrix.Identity; - if (this.useUnscaledLighting) - { - lighting_matrix = Matrix.CreateScale(Game1.options.zoomLevel); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, lighting_matrix); - if (++batchOpens == 1) - events.Rendering.RaiseEmpty(); - Color lighting = ((Game1.currentLocation.Name.StartsWith("UndergroundMine") && Game1.currentLocation is MineShaft) ? (Game1.currentLocation as MineShaft).getLightingColor(gameTime) : ((Game1.ambientLight.Equals(Color.White) || (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors)) ? Game1.outdoorLight : Game1.ambientLight)); - float light_multiplier = 1f; - if (Game1.player.hasBuff(26)) - { - if (lighting == Color.White) - { - lighting = new Color(0.75f, 0.75f, 0.75f); - } - else - { - lighting.R = (byte)Utility.Lerp((int)lighting.R, 255f, 0.5f); - lighting.G = (byte)Utility.Lerp((int)lighting.G, 255f, 0.5f); - lighting.B = (byte)Utility.Lerp((int)lighting.B, 255f, 0.5f); - } - light_multiplier = 0.33f; - } - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, lighting); - foreach (LightSource lightSource in Game1.currentLightSources) - { - if ((Game1.IsRainingHere() || Game1.isDarkOut()) && lightSource.lightContext.Value == LightSource.LightContext.WindowLight) - { - continue; - } - if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID) - { - Farmer farmer = Game1.getFarmerMaybeOffline(lightSource.PlayerID); - if (farmer == null || (farmer.currentLocation != null && farmer.currentLocation.Name != Game1.currentLocation.Name) || (bool)farmer.hidden) - { - continue; - } - } - if (Utility.isOnScreen(lightSource.position, (int)((float)lightSource.radius * 64f * 4f))) - { - Game1.spriteBatch.Draw(lightSource.lightTexture, Game1.GlobalToLocal(Game1.viewport, lightSource.position) / (Game1.options.lightingQuality / 2), lightSource.lightTexture.Bounds, lightSource.color.Value * light_multiplier, 0f, new Vector2(lightSource.lightTexture.Bounds.Width / 2, lightSource.lightTexture.Bounds.Height / 2), (float)lightSource.radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); - } - } - Game1.spriteBatch.End(); - Game1.SetRenderTarget(target_screen); + this.DrawLighting(gameTime, target_screen, out renderingRaised); } base.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (++batchOpens == 1) + if (!renderingRaised) events.Rendering.RaiseEmpty(); events.RenderingWorld.RaiseEmpty(); if (Game1.background != null) @@ -513,8 +466,14 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) Game1.background.draw(Game1.spriteBatch); } Game1.currentLocation.drawBackground(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); + Game1.spriteBatch.End(); + for (int i = 0; i < Game1.currentLocation.backgroundLayers.Count; i++) + { + Game1.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, SamplerState.PointClamp); + Game1.currentLocation.backgroundLayers[i].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, -1f); + Game1.spriteBatch.End(); + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); Game1.currentLocation.drawWater(Game1.spriteBatch); Game1.spriteBatch.End(); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); @@ -546,21 +505,21 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) { if (Game1.CurrentEvent == null) { - foreach (NPC k in Game1.currentLocation.characters) + foreach (NPC n in Game1.currentLocation.characters) { - if (!k.swimming && !k.HideShadow && !k.IsInvisible && !this.checkCharacterTilesForShadowDrawFlag(k)) + if (!n.swimming && !n.HideShadow && !n.IsInvisible && !this.checkCharacterTilesForShadowDrawFlag(n)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, k.GetShadowOffset() + k.Position + new Vector2((float)(k.GetSpriteWidthForPositioning() * 4) / 2f, k.GetBoundingBox().Height + ((!k.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)k.yJumpOffset / 40f) * (float)k.scale), SpriteEffects.None, Math.Max(0f, (float)k.getStandingY() / 10000f) - 1E-06f); + n.DrawShadow(Game1.spriteBatch); } } } else { - foreach (NPC l in Game1.CurrentEvent.actors) + foreach (NPC n2 in Game1.CurrentEvent.actors) { - if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(l)) && !l.swimming && !l.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(l)) + if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n2)) && !n2.swimming && !n2.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(n2)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, l.GetShadowOffset() + l.Position + new Vector2((float)(l.GetSpriteWidthForPositioning() * 4) / 2f, l.GetBoundingBox().Height + ((!l.IsMonster) ? ((l.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, 4f + (float)l.yJumpOffset / 40f) * (float)l.scale, SpriteEffects.None, Math.Max(0f, (float)l.getStandingY() / 10000f) - 1E-06f); + n2.DrawShadow(Game1.spriteBatch); } } } @@ -568,43 +527,51 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) { if (!Game1.multiplayer.isDisconnecting(f3.UniqueMultiplayerID) && !f3.swimming && !f3.isRidingHorse() && !f3.IsSitting() && (Game1.currentLocation == null || !this.checkCharacterTilesForShadowDrawFlag(f3))) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f3.GetShadowOffset() + f3.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f3.running || f3.UsingTool) && f3.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f3.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); + f3.DrawShadow(Game1.spriteBatch); } } } + float layer_sub_sort = 0.1f; + for (int j = 0; j < Game1.currentLocation.buildingLayers.Count; j++) + { + float layer = 0f; + if (Game1.currentLocation.buildingLayers.Count > 1) + { + layer = (float)j / (float)(Game1.currentLocation.buildingLayers.Count - 1); + } + Game1.currentLocation.buildingLayers[j].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, layer_sub_sort * layer); + } Layer building_layer = Game1.currentLocation.Map.GetLayer("Buildings"); - building_layer.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); - Game1.mapDisplayDevice.EndScene(); Game1.spriteBatch.End(); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); if (!Game1.currentLocation.shouldHideCharacters()) { if (Game1.CurrentEvent == null) { - foreach (NPC m in Game1.currentLocation.characters) + foreach (NPC n3 in Game1.currentLocation.characters) { - if (!m.swimming && !m.HideShadow && !m.isInvisible && this.checkCharacterTilesForShadowDrawFlag(m)) + if (!n3.swimming && !n3.HideShadow && !n3.isInvisible && this.checkCharacterTilesForShadowDrawFlag(n3)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, m.GetShadowOffset() + m.Position + new Vector2((float)(m.GetSpriteWidthForPositioning() * 4) / 2f, m.GetBoundingBox().Height + ((!m.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)m.yJumpOffset / 40f) * (float)m.scale), SpriteEffects.None, Math.Max(0f, (float)m.getStandingY() / 10000f) - 1E-06f); + n3.DrawShadow(Game1.spriteBatch); } } } else { - foreach (NPC n in Game1.CurrentEvent.actors) + foreach (NPC n4 in Game1.CurrentEvent.actors) { - if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n)) && !n.swimming && !n.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n)) + if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n4)) && !n4.swimming && !n4.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n4)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, n.GetShadowOffset() + n.Position + new Vector2((float)(n.GetSpriteWidthForPositioning() * 4) / 2f, n.GetBoundingBox().Height + ((!n.IsMonster) ? 12 : 0))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), Math.Max(0f, (4f + (float)n.yJumpOffset / 40f) * (float)n.scale), SpriteEffects.None, Math.Max(0f, (float)n.getStandingY() / 10000f) - 1E-06f); + n4.DrawShadow(Game1.spriteBatch); } } } foreach (Farmer f4 in this._farmerShadows) { - float draw_layer = Math.Max(0.0001f, f4.getDrawLayer() + 0.00011f) - 0.0001f; + Math.Max(0.0001f, f4.getDrawLayer() + 0.00011f); if (!f4.swimming && !f4.isRidingHorse() && !f4.IsSitting() && Game1.currentLocation != null && this.checkCharacterTilesForShadowDrawFlag(f4)) { - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(f4.GetShadowOffset() + f4.Position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((f4.running || f4.UsingTool) && f4.FarmerSprite.currentAnimationIndex > 1) ? ((float)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[f4.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, draw_layer); + f4.DrawShadow(Game1.spriteBatch); } } } @@ -612,10 +579,6 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) { Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); } - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - { - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f); - } Game1.currentLocation.draw(Game1.spriteBatch); foreach (Vector2 tile_position in Game1.crabPotOverlayTiles.Keys) { @@ -627,18 +590,10 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) Game1.mapDisplayDevice.DrawTile(tile, draw_location, (tile_position.Y * 64f - 1f) / 10000f); } } - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - _ = Game1.currentLocation.currentEvent.messageToScreen; - } if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool)) { Game1.drawTool(Game1.player); } - if (Game1.currentLocation.Name.Equals("Farm")) - { - this.drawFarmBuildings(); - } if (Game1.tvStation >= 0) { Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); @@ -651,18 +606,40 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); } } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); - Game1.mapDisplayDevice.EndScene(); + for (int l = 0; l < Game1.currentLocation.frontLayers.Count; l++) + { + float layer2 = 0f; + if (Game1.currentLocation.frontLayers.Count > 1) + { + layer2 = (float)l / (float)(Game1.currentLocation.frontLayers.Count - 1); + } + Game1.currentLocation.frontLayers[l].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, 64f + layer_sub_sort * layer2); + } Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); Game1.spriteBatch.End(); + for (int m = 0; m < Game1.currentLocation.alwaysFrontLayers.Count; m++) + { + Game1.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, SamplerState.PointClamp); + Game1.currentLocation.alwaysFrontLayers[m].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, -1f); + Game1.spriteBatch.End(); + } + Game1.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, SamplerState.PointClamp); + if (!Game1.IsFakedBlackScreen()) + { + this.drawWeather(gameTime, target_screen); + } + Game1.spriteBatch.End(); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000) { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4); - Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); } + if (Game1.screenGlow) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + } + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); if (Game1.toolHold > 400f && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) { Color barColor = Color.White; @@ -684,22 +661,6 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Color.Black); Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0), (int)(Game1.toolHold % 600f * 0.08f), 8), barColor); } - if (!Game1.IsFakedBlackScreen()) - { - this.drawWeather(gameTime, target_screen); - } - if (Game1.farmEvent != null) - { - Game1.farmEvent.draw(Game1.spriteBatch); - } - if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - } - if (Game1.screenGlow) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - } Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0f || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)) { @@ -709,29 +670,30 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); if (Game1.eventUp && Game1.currentLocation.currentEvent != null) { - foreach (NPC n2 in Game1.currentLocation.currentEvent.actors) + foreach (NPC n5 in Game1.currentLocation.currentEvent.actors) { - if (n2.isEmoting) + if (n5.isEmoting) { - Vector2 emotePosition = n2.getLocalPosition(Game1.viewport); - if (n2.NeedsBirdieEmoteHack()) + Vector2 emotePosition = n5.getLocalPosition(Game1.viewport); + if (n5.NeedsBirdieEmoteHack()) { emotePosition.X += 64f; } emotePosition.Y -= 140f; - if (n2.Age == 2) + if (n5.Age == 2) { emotePosition.Y += 32f; } - else if (n2.Gender == 1) + else if (n5.Gender == 1) { emotePosition.Y += 10f; } - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(n2.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, n2.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)n2.getStandingY() / 10000f); + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(n5.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, n5.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)n5.getStandingY() / 10000f); } } } Game1.spriteBatch.End(); + Game1.mapDisplayDevice.EndScene(); if (Game1.drawLighting && !Game1.IsFakedBlackScreen()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp); @@ -744,7 +706,7 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) render_zoom /= Game1.options.zoomLevel; } Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Color.White, 0f, Vector2.Zero, render_zoom, SpriteEffects.None, 1f); - if (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + if (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors) { Game1.spriteBatch.Draw(Game1.staminaRect, vp.Bounds, Color.OrangeRed * 0.45f); } @@ -795,9 +757,9 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) } if (Game1.hudMessages.Count > 0 && !this.takingMapScreenshot) { - for (int j = Game1.hudMessages.Count - 1; j >= 0; j--) + for (int k = Game1.hudMessages.Count - 1; k >= 0; k--) { - Game1.hudMessages[j].draw(Game1.spriteBatch, j); + Game1.hudMessages[k].draw(Game1.spriteBatch, k); } } Game1.spriteBatch.End(); @@ -827,7 +789,7 @@ private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) { Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); } - if (!Game1.IsFakedBlackScreen() && Game1.IsRainingHere() && Game1.currentLocation != null && (bool)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + if (!Game1.IsFakedBlackScreen() && Game1.IsRainingHere() && Game1.currentLocation != null && (bool)Game1.currentLocation.isOutdoors) { Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); } From 690fda6c682cc65b96c59142d431bb3b102bc0cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Nov 2023 00:59:41 -0400 Subject: [PATCH 25/84] update asset propagation for SDV 1.6 --- src/SMAPI/Framework/ContentCoordinator.cs | 6 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 1092 +++++---------------- 2 files changed, 233 insertions(+), 865 deletions(-) diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 71495d7e2..19c346d40 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; @@ -395,7 +396,9 @@ public IEnumerable InvalidateCache(Func InvalidateCache(Func p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded, out IDictionary propagated, diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index ab853977f..6fdb456fb 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -9,21 +9,27 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; -using StardewModdingAPI.Toolkit.Utilities; using StardewValley; -using StardewValley.BellsAndWhistles; using StardewValley.Buildings; -using StardewValley.Characters; using StardewValley.GameData.BigCraftables; -using StardewValley.GameData.Movies; +using StardewValley.GameData.Buildings; +using StardewValley.GameData.Characters; +using StardewValley.GameData.Crops; +using StardewValley.GameData.FarmAnimals; +using StardewValley.GameData.FloorsAndPaths; +using StardewValley.GameData.FruitTrees; +using StardewValley.GameData.LocationContexts; using StardewValley.GameData.Objects; +using StardewValley.GameData.Pants; +using StardewValley.GameData.Pets; +using StardewValley.GameData.Shirts; +using StardewValley.GameData.Tools; +using StardewValley.GameData.Weapons; using StardewValley.Locations; -using StardewValley.Menus; -using StardewValley.Objects; -using StardewValley.Projectiles; +using StardewValley.Pathfinding; using StardewValley.TerrainFeatures; +using StardewValley.WorldMaps; using xTile; -using xTile.Tiles; namespace StardewModdingAPI.Metadata { @@ -51,19 +57,6 @@ internal class CoreAssetPropagator /// Parse a raw asset name. private readonly Func ParseAssetName; - /// Optimized bucket categories for batch reloading assets. - private enum AssetBucket - { - /// NPC overworld sprites. - Sprite, - - /// Villager dialogue portraits. - Portrait, - - /// Any other asset. - Other - }; - /// A cache of world data fetched for the current tick. private readonly TickCacheDictionary WorldCache = new(); @@ -89,11 +82,12 @@ public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManag } /// Reload one of the game's core assets (if applicable). + /// The content managers whose assets to update. /// The asset keys and types to reload. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// A lookup of asset names to whether they've been propagated. /// Whether the NPC pathfinding warp route cache was reloaded. - public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool changedWarpRoutes) + public void Propagate(IList contentManagers, IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool changedWarpRoutes) { // get base name lookup propagatedAssets = assets @@ -101,95 +95,141 @@ public void Propagate(IDictionary assets, bool ignoreWorld, ou .Distinct() .ToDictionary(name => name, _ => false); - // group into optimized lists - var buckets = assets.GroupBy(p => + // edit textures in-place { - if (p.Key.IsDirectlyUnderPath("Characters") || p.Key.IsDirectlyUnderPath("Characters/Monsters")) - return AssetBucket.Sprite; + IAssetName[] textureAssets = assets + .Where(p => typeof(Texture2D).IsAssignableFrom(p.Value)) + .Select(p => p.Key) + .ToArray(); - if (p.Key.IsDirectlyUnderPath("Portraits")) - return AssetBucket.Portrait; + if (textureAssets.Any()) + { + var defaultLanguage = this.MainContentManager.GetCurrentLanguage(); - return AssetBucket.Other; - }); + foreach (IAssetName assetName in textureAssets) + { + bool changed = this.PropagateTexture(assetName, assetName.LanguageCode ?? defaultLanguage, contentManagers, ignoreWorld); + if (changed) + propagatedAssets[assetName] = true; + } - // reload assets + foreach (IAssetName assetName in textureAssets) + assets.Remove(assetName); + } + } + + // reload other assets changedWarpRoutes = false; - foreach (var bucket in buckets) + foreach (var entry in assets) { - switch (bucket.Key) + bool changed = false; + bool curChangedMapRoutes = false; + try { - case AssetBucket.Sprite: - if (!ignoreWorld) - this.UpdateNpcSprites(propagatedAssets); - break; - - case AssetBucket.Portrait: - if (!ignoreWorld) - this.UpdateNpcPortraits(propagatedAssets); - break; - - default: - foreach (var entry in bucket) - { - bool changed = false; - bool curChangedMapRoutes = false; - try - { - changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapRoutes); - } - catch (Exception ex) - { - this.Monitor.Log($"An error occurred while propagating asset changes. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - propagatedAssets[entry.Key] = changed; - changedWarpRoutes = changedWarpRoutes || curChangedMapRoutes; - } - break; + changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapRoutes); } + catch (Exception ex) + { + this.Monitor.Log($"An error occurred while propagating asset changes. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + propagatedAssets[entry.Key] = changed; + changedWarpRoutes = changedWarpRoutes || curChangedMapRoutes; } // reload NPC pathfinding cache if any map routes changed if (changedWarpRoutes) - NPC.populateRoutesFromLocationToLocationList(); + WarpPathfindingCache.PopulateCache(); } /********* ** Private methods *********/ - /// Reload one of the game's core assets (if applicable). + /// Propagate changes to a cached texture asset. /// The asset name to reload. - /// The asset type to reload. + /// The language for which to get assets. + /// The content managers whose assets to update. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. - /// Whether the locations reachable by warps from this location changed as part of this propagation. - /// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true. + /// Returns whether an asset was loaded. [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarpRoutes) + private bool PropagateTexture(IAssetName assetName, LocalizedContentManager.LanguageCode language, IList contentManagers, bool ignoreWorld) { - var content = this.MainContentManager; - string key = assetName.BaseName; - changedWarpRoutes = false; - bool changed = false; - /**** - ** Special case: current map tilesheet - ** We only need to do this for the current location, since tilesheets are reloaded when you enter a location. - ** Just in case, we should still propagate by key even if a tilesheet is matched. + ** Update textures in-place ****/ - if (!ignoreWorld && Game1.currentLocation?.map?.TileSheets != null) + assetName = assetName.GetBaseAssetName(); + bool changed = false; { - foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) + Lazy newTexture = new(() => this.DisposableContentManager.LoadLocalized(assetName, language, useCache: false)); + + foreach (IContentManager contentManager in contentManagers) { - if (this.IsSameBaseName(assetName, tilesheet.ImageSource)) + if (contentManager.IsLoaded(assetName)) { - Game1.mapDisplayDevice.LoadTileSheet(tilesheet); changed = true; + Texture2D texture = contentManager.LoadLocalized(assetName, language, useCache: true); + texture.CopyFromTexture(newTexture.Value); } } + + if (newTexture.IsValueCreated) + newTexture.Value.Dispose(); } + /**** + ** Update game state if needed + ****/ + if (changed) + { + switch (assetName.Name.ToLower().Replace("\\", "/")) // normalized key so we can compare statically + { + /**** + ** Content\Characters\Farmer + ****/ + case "characters/farmer/farmer_base": // Farmer + case "characters/farmer/farmer_base_bald": + case "characters/farmer/farmer_girl_base": + case "characters/farmer/farmer_girl_base_bald": + if (ignoreWorld) + this.UpdatePlayerSprites(assetName); + break; + + /**** + ** Content\TileSheets + ****/ + case "tilesheets/tools": // Game1.ResetToolSpriteSheet + Game1.ResetToolSpriteSheet(); + break; + + default: + if (!ignoreWorld) + { + if (assetName.IsDirectlyUnderPath("Buildings") && assetName.BaseName.EndsWith("_PaintMask")) + return this.UpdateBuildingPaintMask(assetName); + } + + break; + } + } + + return changed; + } + + /// Reload one of the game's core assets (if applicable). + /// The asset name to reload. + /// The asset type to reload. + /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. + /// Whether the locations reachable by warps from this location changed as part of this propagation. + /// Returns whether an asset was loaded. + [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] + private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarpRoutes) + { + bool changed = false; + var content = this.MainContentManager; + string key = assetName.BaseName; + changedWarpRoutes = false; + /**** ** Propagate map changes ****/ @@ -237,58 +277,6 @@ static ISet GetWarpSet(GameLocation location) ****/ switch (assetName.BaseName.ToLower().Replace("\\", "/")) // normalized key so we can compare statically { - /**** - ** Animals - ****/ - case "animals/horse": - return changed | (!ignoreWorld && this.UpdatePetOrHorseSprites(assetName)); - - /**** - ** Buildings - ****/ - case "buildings/houses": // Farm - Farm.houseTextures = this.LoadTexture(key); - return true; - - case "buildings/houses_paintmask": // Farm - { - bool removedFromCache = this.RemoveFromPaintMaskCache(assetName); - - Farm farm = Game1.getFarm(); - farm?.ApplyHousePaint(); - - return changed | (removedFromCache || farm != null); - } - - /**** - ** Content\Characters\Farmer - ****/ - case "characters/farmer/accessories": // Game1.LoadContent - FarmerRenderer.accessoriesTexture = this.LoadTexture(key); - return true; - - case "characters/farmer/farmer_base": // Farmer - case "characters/farmer/farmer_base_bald": - case "characters/farmer/farmer_girl_base": - case "characters/farmer/farmer_girl_base_bald": - return changed | (!ignoreWorld && this.UpdatePlayerSprites(assetName)); - - case "characters/farmer/hairstyles": // Game1.LoadContent - FarmerRenderer.hairStylesTexture = this.LoadTexture(key); - return true; - - case "characters/farmer/hats": // Game1.LoadContent - FarmerRenderer.hatsTexture = this.LoadTexture(key); - return true; - - case "characters/farmer/pants": // Game1.LoadContent - FarmerRenderer.pantsTexture = this.LoadTexture(key); - return true; - - case "characters/farmer/shirts": // Game1.LoadContent - FarmerRenderer.shirtsTexture = this.LoadTexture(key); - return true; - /**** ** Content\Data ****/ @@ -296,24 +284,43 @@ static ISet GetWarpSet(GameLocation location) Game1.achievements = content.Load>(key); return true; + case "data/audiochanges": + Game1.CueModification.OnStartup(); // reload file and reapply changes + return true; + case "data/bigcraftables": // Game1.LoadContent Game1.bigCraftableData = content.Load>(key); ItemRegistry.ResetCache(); return true; - case "data/clothinginformation": // Game1.LoadContent - Game1.clothingInformation = content.Load>(key); + case "data/boots": // BootsDataDefinition ItemRegistry.ResetCache(); return true; + case "data/buildings": // Game1.LoadContent + Game1.buildingData = content.Load>(key); + if (!ignoreWorld) + { + Utility.ForEachBuilding(building => + { + building.ReloadBuildingData(); + return true; + }); + } + return true; + + case "data/characters": // Game1.LoadContent + Game1.characterData = content.Load>(key); + if (!ignoreWorld) + this.UpdateCharacterData(); + return true; + case "data/concessions": // MovieTheater.GetConcessions MovieTheater.ClearCachedLocalizedData(); return true; case "data/concessiontastes": // MovieTheater.GetConcessionTasteForCharacter - this.Reflection - .GetField>(typeof(MovieTheater), "_concessionTastes") - .SetValue(content.Load>(key)); + MovieTheater.ClearCachedConcessionTastes(); return true; case "data/cookingrecipes": // CraftingRecipe.InitShared @@ -324,188 +331,101 @@ static ISet GetWarpSet(GameLocation location) CraftingRecipe.craftingRecipes = content.Load>(key); return true; - case "data/farmanimals": // FarmAnimal constructor - return changed | (!ignoreWorld && this.UpdateFarmAnimalData()); - - case "data/hairdata": // Farmer.GetHairStyleMetadataFile - return changed | this.UpdateHairData(); - - case "data/movies": // MovieTheater.GetMovieData - case "data/moviesreactions": // MovieTheater.GetMovieReactions - MovieTheater.ClearCachedLocalizedData(); + case "data/crops": // Game1.LoadContent + Game1.cropData = content.Load>(key); return true; - case "data/npcdispositions": // NPC constructor - return changed | (!ignoreWorld && this.UpdateNpcDispositions(content, assetName)); - - case "data/npcgifttastes": // Game1.LoadContent - Game1.NPCGiftTastes = content.Load>(key); + case "data/farmanimals": // FarmAnimal constructor + Game1.farmAnimalData = content.Load>(key); + if (!ignoreWorld) + this.UpdateFarmAnimalData(); return true; - case "data/objectcontexttags": // Game1.LoadContent - Game1.objectContextTags = content.Load>(key); + case "data/floorsandpaths": // Game1.LoadContent + Game1.floorPathData = content.Load>("Data\\FloorsAndPaths"); return true; - case "data/objects": // Game1.LoadContent - Game1.objectData = content.Load>(key); + case "data/furniture": // FurnitureDataDefinition ItemRegistry.ResetCache(); return true; - /**** - ** Content\Fonts - ****/ - case "fonts/spritefont1": // Game1.LoadContent - Game1.dialogueFont = content.Load(key); - return true; - - case "fonts/smallfont": // Game1.LoadContent - Game1.smallFont = content.Load(key); - return true; - - case "fonts/tinyfont": // Game1.LoadContent - Game1.tinyFont = content.Load(key); + case "data/fruittrees": // Game1.LoadContent + Game1.fruitTreeData = content.Load>("Data\\FruitTrees"); return true; - case "fonts/tinyfontborder": // Game1.LoadContent - Game1.tinyFontBorder = content.Load(key); - return true; - - /**** - ** Content\LooseSprites\Lighting - ****/ - case "loosesprites/lighting/greenlight": // Game1.LoadContent - Game1.cauldronLight = content.Load(key); - return true; - - case "loosesprites/lighting/indoorwindowlight": // Game1.LoadContent - Game1.indoorWindowLight = content.Load(key); - return true; - - case "loosesprites/lighting/lantern": // Game1.LoadContent - Game1.lantern = content.Load(key); - return true; - - case "loosesprites/lighting/sconcelight": // Game1.LoadContent - Game1.sconceLight = content.Load(key); - return true; + case "data/hairdata": // Farmer.GetHairStyleMetadataFile + return changed | this.UpdateHairData(); - case "loosesprites/lighting/windowlight": // Game1.LoadContent - Game1.windowLight = content.Load(key); + case "data/hats": // HatDataDefinition + ItemRegistry.ResetCache(); return true; - /**** - ** Content\LooseSprites - ****/ - case "loosesprites/birds": // Game1.LoadContent - Game1.birdsSpriteSheet = content.Load(key); + case "data/locationcontexts": // Game1.LoadContent + Game1.locationContextData = content.Load>("Data\\LocationContexts"); return true; - case "loosesprites/chatbox": // ChatBox constructor - if (Game1.chatBox?.chatBox != null) - { - Texture2D texture = content.Load(key); - - this.Reflection.GetField(Game1.chatBox.chatBox, "_textBoxTexture").SetValue(texture); - this.Reflection.GetField(Game1.chatBox.emojiMenu, "chatBoxTexture").SetValue(texture); - - return true; - } - return false; - - case "loosesprites/concessions": // Game1.LoadContent - Game1.concessionsSpriteSheet = content.Load(key); + case "data/movies": // MovieTheater.GetMovieData + case "data/moviesreactions": // MovieTheater.GetMovieReactions + MovieTheater.ClearCachedLocalizedData(); return true; - case "loosesprites/controllermaps": // Game1.LoadContent - Game1.controllerMaps = content.Load(key); + case "data/npcgifttastes": // Game1.LoadContent + Game1.NPCGiftTastes = content.Load>(key); return true; - case "loosesprites/cursors": // Game1.LoadContent - Game1.mouseCursors = content.Load(key); - foreach (DayTimeMoneyBox menu in Game1.onScreenMenus.OfType()) - { - foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton }) - button.texture = Game1.mouseCursors; - } - - if (!ignoreWorld) - this.UpdateDoorSprites(content, assetName); + case "data/objects": // Game1.LoadContent + Game1.objectData = content.Load>(key); + ItemRegistry.ResetCache(); return true; - case "loosesprites/cursors2": // Game1.LoadContent - Game1.mouseCursors2 = content.Load(key); + case "data/pants": // Game1.LoadContent + Game1.pantsData = content.Load>(key); + ItemRegistry.ResetCache(); return true; - case "loosesprites/daybg": // Game1.LoadContent - Game1.daybg = content.Load(key); + case "data/pets": // Game1.LoadContent + Game1.petData = content.Load>(key); + ItemRegistry.ResetCache(); return true; - case "loosesprites/emojis": // ChatBox constructor - if (Game1.chatBox != null) - { - Texture2D texture = content.Load(key); - - this.Reflection.GetField(Game1.chatBox.emojiMenu, "emojiTexture").SetValue(texture); - Game1.chatBox.emojiMenuIcon.texture = texture; - - return true; - } - return false; - - case "loosesprites/font_bold": // Game1.LoadContent - SpriteText.spriteTexture = content.Load(key); + case "data/shirts": // Game1.LoadContent + Game1.shirtData = content.Load>(key); + ItemRegistry.ResetCache(); return true; - case "loosesprites/font_colored": // Game1.LoadContent - SpriteText.coloredTexture = content.Load(key); + case "data/tools": // Game1.LoadContent + Game1.toolData = content.Load>(key); + ItemRegistry.ResetCache(); return true; - case "loosesprites/giftbox": // Game1.LoadContent - Game1.giftboxTexture = content.Load(key); + case "data/weapons": // Game1.LoadContent + Game1.weaponData = Game1.content.Load>(@"Data\Weapons"); + ItemRegistry.ResetCache(); return true; - case "loosesprites/nightbg": // Game1.LoadContent - Game1.nightbg = content.Load(key); + case "data/wildtrees": // Tree + Tree.ClearCache(); return true; - case "loosesprites/shadow": // Game1.LoadContent - Game1.shadowTexture = content.Load(key); + case "data/worldmap": // WorldMapManager + WorldMapManager.ReloadData(); return true; - case "loosesprites/suspensionbridge": // SuspensionBridge constructor - return changed | (!ignoreWorld && this.UpdateSuspensionBridges(content, assetName)); - /**** - ** Content\Maps + ** Content\Fonts ****/ - case "maps/menutiles": // Game1.LoadContent - Game1.menuTexture = content.Load(key); + case "fonts/spritefont1": // Game1.LoadContent + Game1.dialogueFont = content.Load(key); return true; - case "maps/menutilesuncolored": // Game1.LoadContent - Game1.uncoloredMenuTexture = content.Load(key); + case "fonts/smallfont": // Game1.LoadContent + Game1.smallFont = content.Load(key); return true; - case "maps/springobjects": // Game1.LoadContent - Game1.objectSpriteSheet = content.Load(key); + case "fonts/tinyfont": // Game1.LoadContent + Game1.tinyFont = content.Load(key); return true; - /**** - ** Content\Minigames - ****/ - case "minigames/clouds": // TitleMenu - { - if (Game1.activeClickableMenu is TitleMenu titleMenu) - { - titleMenu.cloudsTexture = content.Load(key); - return true; - } - } - return changed; - - case "minigames/titlebuttons": // TitleMenu - return changed | this.UpdateTitleButtons(content, assetName); - /**** ** Content\Strings ****/ @@ -513,152 +433,20 @@ static ISet GetWarpSet(GameLocation location) return changed | this.UpdateStringsFromCsFiles(content); /**** - ** Content\TileSheets + ** Dynamic keys ****/ - case "tilesheets/animations": // Game1.LoadContent - Game1.animations = content.Load(key); - return true; - - case "tilesheets/buffsicons": // Game1.LoadContent - Game1.buffsIcons = content.Load(key); - return true; - - case "tilesheets/bushes": // new Bush() - Bush.texture = new Lazy(() => content.Load(key)); - return true; - - case "tilesheets/chairtiles": // Game1.LoadContent - return this.UpdateChairTiles(content, assetName, ignoreWorld); - - case "tilesheets/craftables": // Game1.LoadContent - Game1.bigCraftableSpriteSheet = content.Load(key); - return true; - - case "tilesheets/critters": // Critter constructor - return changed | (!ignoreWorld && this.UpdateCritterTextures(assetName)); - - case "tilesheets/crops": // Game1.LoadContent - Game1.cropSpriteSheet = content.Load(key); - return true; - - case "tilesheets/debris": // Game1.LoadContent - Game1.debrisSpriteSheet = content.Load(key); - return true; - - case "tilesheets/emotes": // Game1.LoadContent - Game1.emoteSpriteSheet = content.Load(key); - return true; - - case "tilesheets/fruittrees": // FruitTree - return this.UpdateDefaultFruitTreeTexture(content, assetName); - - case "tilesheets/furniture": // Game1.LoadContent - Furniture.furnitureTexture = content.Load(key); - ItemRegistry.ResetCache(); - return true; - - case "tilesheets/furniturefront": // Game1.LoadContent - Furniture.furnitureFrontTexture = content.Load(key); - return true; - - case "tilesheets/projectiles": // Game1.LoadContent - Projectile.projectileSheet = content.Load(key); - return true; - - case "tilesheets/rain": // Game1.LoadContent - Game1.rainTexture = content.Load(key); - return true; - - case "tilesheets/tools": // Game1.ResetToolSpriteSheet - Game1.ResetToolSpriteSheet(); - return true; - - case "tilesheets/weapons": // Game1.LoadContent - Tool.weaponsTexture = content.Load(key); - return true; - - /**** - ** Content\TerrainFeatures - ****/ - case "terrainfeatures/flooring": // from Flooring - Flooring.floorsTexture = content.Load(key); - return true; - - case "terrainfeatures/flooring_winter": // from Flooring - Flooring.floorsTextureWinter = content.Load(key); - return true; - - case "terrainfeatures/grass": // from Grass - return !ignoreWorld && this.UpdateGrassTextures(content, assetName); - - case "terrainfeatures/hoedirt": // from HoeDirt - HoeDirt.lightTexture = content.Load(key); - return true; - - case "terrainfeatures/hoedirtdark": // from HoeDirt - HoeDirt.darkTexture = content.Load(key); - return true; - - case "terrainfeatures/hoedirtsnow": // from HoeDirt - HoeDirt.snowTexture = content.Load(key); - return true; - - case "terrainfeatures/mushroom_tree": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree.ToString())); - - case "terrainfeatures/tree_palm": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.palmTree.ToString())); - - case "terrainfeatures/tree1_fall": // from Tree - case "terrainfeatures/tree1_spring": // from Tree - case "terrainfeatures/tree1_summer": // from Tree - case "terrainfeatures/tree1_winter": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree.ToString())); - - case "terrainfeatures/tree2_fall": // from Tree - case "terrainfeatures/tree2_spring": // from Tree - case "terrainfeatures/tree2_summer": // from Tree - case "terrainfeatures/tree2_winter": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree.ToString())); - - case "terrainfeatures/tree3_fall": // from Tree - case "terrainfeatures/tree3_spring": // from Tree - case "terrainfeatures/tree3_winter": // from Tree - return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.pineTree.ToString())); - } - - /**** - ** Dynamic assets - ****/ - if (!ignoreWorld) - { - // dynamic textures - if (assetName.IsDirectlyUnderPath("Animals")) - { - if (assetName.StartsWith("animals/cat")) - return changed | this.UpdatePetOrHorseSprites(assetName); - - if (assetName.StartsWith("animals/dog")) - return changed | this.UpdatePetOrHorseSprites(assetName); - - return changed | this.UpdateFarmAnimalSprites(assetName); - } - - if (assetName.IsDirectlyUnderPath("Buildings")) - return changed | this.UpdateBuildings(assetName); - - if (assetName.StartsWith("LooseSprites/Fence")) - return changed | this.UpdateFenceTextures(assetName); + default: + if (!ignoreWorld) + { + if (assetName.IsDirectlyUnderPath("Characters/Dialogue")) + return changed | this.UpdateNpcDialogue(assetName); - // dynamic data - if (assetName.IsDirectlyUnderPath("Characters/Dialogue")) - return changed | this.UpdateNpcDialogue(assetName); + if (assetName.IsDirectlyUnderPath("Characters/schedules")) + return changed | this.UpdateNpcSchedules(assetName); + } - if (assetName.IsDirectlyUnderPath("Characters/schedules")) - return changed | this.UpdateNpcSchedules(assetName); + return false; } - - return false; } @@ -668,314 +456,34 @@ static ISet GetWarpSet(GameLocation location) /**** ** Update texture methods ****/ - /// Update buttons on the title screen. - /// The content manager through which to update the asset. - /// The asset name to update. - /// Returns whether any references were updated. - /// Derived from the constructor and . - private bool UpdateTitleButtons(LocalizedContentManager content, IAssetName assetName) - { - if (Game1.activeClickableMenu is TitleMenu titleMenu) - { - Texture2D texture = content.Load(assetName.BaseName); - - titleMenu.titleButtonsTexture = texture; - titleMenu.backButton.texture = texture; - titleMenu.aboutButton.texture = texture; - titleMenu.languageButton.texture = texture; - foreach (ClickableTextureComponent button in titleMenu.buttons) - button.texture = texture; - foreach (TemporaryAnimatedSprite bird in titleMenu.birds) - bird.texture = texture; - - return true; - } - - return false; - } - - /// Update the sprites for matching pets or horses. - /// The animal type. - /// The asset name to update. - /// Returns whether any references were updated. - private bool UpdatePetOrHorseSprites(IAssetName assetName) - where TAnimal : NPC - { - // find matches - TAnimal[] animals = this.GetCharacters() - .OfType() - .Where(p => this.IsSameBaseName(assetName, p.Sprite?.spriteTexture?.Name)) - .ToArray(); - - // update sprites - bool changed = false; - foreach (TAnimal animal in animals) - changed |= this.MarkSpriteDirty(animal.Sprite); - return changed; - } - - /// Update the sprites for matching farm animals. + /// Update building paint mask textures. /// The asset name to update. - /// Returns whether any references were updated. - /// Derived from . - private bool UpdateFarmAnimalSprites(IAssetName assetName) + /// Returns whether any textures were updated. + private bool UpdateBuildingPaintMask(IAssetName assetName) { - // find matches - FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); - if (!animals.Any()) - return false; - - // update sprites - bool changed = true; - foreach (FarmAnimal animal in animals) - { - // get expected key - string expectedKey = animal.age.Value < animal.ageWhenMature.Value - ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}" - : animal.type.Value; - if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value == null) - expectedKey = $"Sheared{expectedKey}"; - expectedKey = $"Animals/{expectedKey}"; - - // reload asset - if (this.IsSameBaseName(assetName, expectedKey)) - changed |= this.MarkSpriteDirty(animal.Sprite); - } - return changed; - } - - /// Update building textures. - /// The asset name to update. - /// Returns whether any references were updated. - private bool UpdateBuildings(IAssetName assetName) - { - // get paint mask info - const string paintMaskSuffix = "_PaintMask"; - bool isPaintMask = assetName.BaseName.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase); - - // get building type - string type = Path.GetFileName(assetName.BaseName); - if (isPaintMask) - type = type[..^paintMaskSuffix.Length]; - - // get buildings - Building[] buildings = this.GetLocations(buildingInteriors: false) - .SelectMany(p => p.buildings) - .Where(p => p.buildingType.Value == type) - .ToArray(); - // remove from paint mask cache - bool removedFromCache = this.RemoveFromPaintMaskCache(assetName); - - // reload textures - if (buildings.Any()) - { - foreach (Building building in buildings) - building.resetTexture(); - - return true; - } - - return removedFromCache; - } - - /// Update map seat textures. - /// The content manager through which to reload the asset. - /// The asset name to update. - /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. - /// Returns whether any references were updated. - private bool UpdateChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld) - { - MapSeat.mapChairTexture = content.Load(assetName.BaseName); - - if (!ignoreWorld) - { - foreach (GameLocation location in this.GetLocations()) - { - foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) - { - if (this.IsSameBaseName(assetName, seat._loadedTextureFile)) - seat._loadedTextureFile = null; - } - } - } - - return true; - } - - /// Update critter textures. - /// The asset name to update. - /// Returns whether any references were updated. - private bool UpdateCritterTextures(IAssetName assetName) - { - // get critters - Critter[] critters = - ( - from location in this.GetLocations() - where location.critters != null - from Critter critter in location.critters - where this.IsSameBaseName(assetName, critter.sprite?.spriteTexture?.Name) - select critter - ) - .ToArray(); - - // update sprites - bool changed = false; - foreach (Critter entry in critters) - changed |= this.MarkSpriteDirty(entry.sprite); - return changed; - } - - /// Update the sprites for interior doors. - /// The content manager through which to reload the asset. - /// The asset name to update. - /// Returns whether any references were updated. - private void UpdateDoorSprites(LocalizedContentManager content, IAssetName assetName) - { - Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); - - foreach (GameLocation location in this.GetLocations()) - { - IEnumerable? doors = location.interiorDoors?.Doors; - if (doors == null) - continue; - - foreach (InteriorDoor? door in doors) - { - if (door?.Sprite == null) - continue; - - string? curKey = this.Reflection.GetField(door.Sprite, "textureName").GetValue(); - if (this.IsSameBaseName(assetName, curKey)) - door.Sprite.texture = texture.Value; - } - } - } + bool removedFromCache = BuildingPainter.paintMaskLookup.Remove(assetName.BaseName); - /// Update the sprites for a fence type. - /// The asset name to update. - /// Returns whether any references were updated. - private bool UpdateFenceTextures(IAssetName assetName) - { - // get fence type (e.g. LooseSprites/Fence3 => 3) - if (!int.TryParse(this.GetSegments(assetName.BaseName)[1]["Fence".Length..], out int fenceType)) - return false; - - // get fences - Fence[] fences = - ( - from location in this.GetLocations() - from fence in location.Objects.Values.OfType() - where - fence.whichType.Value == fenceType - || (fence.isGate.Value && fenceType == 1) // gates are hardcoded to draw fence type 1 - select fence - ) - .ToArray(); - - // update fence textures - bool changed = false; - foreach (Fence fence in fences) - { - if (fence.fenceTexture.IsValueCreated) - { - fence.fenceTexture = new Lazy(fence.loadFenceTexture); - changed = true; - } - } - return changed; - } - - /// Update tree textures. - /// The content manager through which to reload the asset. - /// The asset name to update. - /// Returns whether any references were updated. - private bool UpdateGrassTextures(LocalizedContentManager content, IAssetName assetName) - { - Grass[] grasses = - ( - from grass in this.GetTerrainFeatures().OfType() - where this.IsSameBaseName(assetName, grass.textureName()) - select grass - ) - .ToArray(); - - bool changed = false; - foreach (Grass grass in grasses) - { - if (grass.texture.IsValueCreated) - { - grass.texture = new Lazy(() => content.Load(assetName.BaseName)); - changed = true; - } - } - return changed; - } - - /// Update the sprites for matching NPCs. - /// The asset names being propagated. - private void UpdateNpcSprites(IDictionary propagated) - { - // get NPCs - var characters = - ( - from npc in this.GetCharacters() - let key = this.ParseAssetNameOrNull(npc.Sprite?.spriteTexture?.Name)?.GetBaseAssetName() - where key != null && propagated.ContainsKey(key) - select new { Npc = npc, AssetName = key } - ) - .ToArray(); - - // update sprite - foreach (var target in characters) - { - if (this.MarkSpriteDirty(target.Npc.Sprite)) - propagated[target.AssetName] = true; - } - } - - /// Update the portraits for matching NPCs. - /// The asset names being propagated. - private void UpdateNpcPortraits(IDictionary propagated) - { - // get NPCs - var characters = - ( - from npc in this.GetCharacters() - where npc.isVillager() - - let key = this.ParseAssetNameOrNull(npc.Portrait?.Name)?.GetBaseAssetName() - where key != null && propagated.ContainsKey(key) - select new { Npc = npc, AssetName = key } - ) - .ToList(); - - // special case: Gil is a private NPC field on the AdventureGuild class (only used for the portrait) + // reload building textures + bool anyReloaded = false; + foreach (GameLocation location in this.GetLocations(buildingInteriors: false)) { - IAssetName gilKey = this.ParseAssetName("Portraits/Gil"); - if (propagated.ContainsKey(gilKey)) + foreach (Building building in location.buildings) { - GameLocation adventureGuild = Game1.getLocationFromName("AdventureGuild"); - if (adventureGuild != null) + if (building.paintedTexture != null && assetName.IsEquivalentTo(building.textureName() + "_PaintMask")) { - NPC? gil = this.Reflection.GetField(adventureGuild, "Gil").GetValue(); - if (gil != null) - characters.Add(new { Npc = gil, AssetName = gilKey }); + anyReloaded = true; + building.resetTexture(); } } } - // update portrait - foreach (var target in characters) - { - target.Npc.resetPortrait(); - propagated[target.AssetName] = true; - } + return removedFromCache || anyReloaded; } /// Update the sprites for matching players. /// The asset name to update. - private bool UpdatePlayerSprites(IAssetName assetName) + private void UpdatePlayerSprites(IAssetName assetName) { Farmer[] players = ( @@ -987,99 +495,10 @@ select player foreach (Farmer player in players) { - var recolorOffsets = this.Reflection.GetField>>?>(typeof(FarmerRenderer), "_recolorOffsets").GetValue(); - recolorOffsets?.Clear(); + FarmerRenderer.recolorOffsets?.Clear(); player.FarmerRenderer.MarkSpriteDirty(); } - - return players.Any(); - } - - /// Update suspension bridge textures. - /// The content manager through which to reload the asset. - /// The asset name to update. - /// Returns whether any references were updated. - private bool UpdateSuspensionBridges(LocalizedContentManager content, IAssetName assetName) - { - Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); - - foreach (GameLocation location in this.GetLocations(buildingInteriors: false)) - { - // get suspension bridges field - var field = this.Reflection.GetField?>(location, nameof(IslandNorth.suspensionBridges), required: false); - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract -- field is nullable when required: false - if (field == null || !typeof(IEnumerable).IsAssignableFrom(field.FieldInfo.FieldType)) - continue; - - // update textures - IEnumerable? bridges = field.GetValue(); - if (bridges != null) - { - foreach (SuspensionBridge bridge in bridges) - this.Reflection.GetField(bridge, "_texture").SetValue(texture.Value); - } - } - - return texture.IsValueCreated; - } - - /// Update fruit trees which use the default texture. - /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any textures were reloaded. - private bool UpdateDefaultFruitTreeTexture(LocalizedContentManager content, IAssetName assetName) - { - FruitTree[] trees = this.GetLocations() - .SelectMany(p => p.terrainFeatures.Values.OfType()) - .Where(p => assetName.IsEquivalentTo(p.textureOverride.Value)) - .ToArray(); - - if (trees.Any()) - { - Texture2D texture = content.Load(assetName.Name); - foreach (FruitTree tree in trees) - tree.texture = texture; - return true; - } - - return false; - } - - /// Update tree textures. - /// The type to update. - /// Returns whether any references were updated. - private bool UpdateTreeTextures(int type) - { - Tree[] trees = this - .GetTerrainFeatures() - .OfType() - .Where(tree => tree.treeType.Value == type) - .ToArray(); - - bool changed = false; - foreach (Tree tree in trees) - { - if (tree.texture.IsValueCreated) - { - this.Reflection.GetMethod(tree, "resetTexture").Invoke(); - changed = true; - } - } - return changed; - } - - /// Mark an animated sprite's texture dirty, so it's reloaded next time it's rendered. - /// The animated sprite to change. - /// Returns whether the sprite was changed. - private bool MarkSpriteDirty(AnimatedSprite sprite) - { - if (sprite.loadedTexture is null && sprite.spriteTexture is null) - return false; - - sprite.loadedTexture = null; - sprite.spriteTexture = null; - return true; } /**** @@ -1088,16 +507,14 @@ private bool MarkSpriteDirty(AnimatedSprite sprite) /// Update the data for matching farm animals. /// Returns whether any farm animals were updated. /// Derived from the constructor. - private bool UpdateFarmAnimalData() + private void UpdateFarmAnimalData() { - bool changed = false; foreach (FarmAnimal animal in this.GetFarmAnimals()) { - animal.reloadData(); - changed = true; + var data = animal.GetAnimalData(); + if (data != null) + animal.buildingTypeILiveIn.Value = data.House; } - - return changed; } /// Update hair style metadata. @@ -1145,24 +562,14 @@ private bool UpdateNpcDialogue(IAssetName assetName) return true; } - /// Update the disposition data for matching NPCs. - /// The content manager through which to reload the asset. - /// The asset name to update. - /// Returns whether any NPCs were updated. - private bool UpdateNpcDispositions(LocalizedContentManager content, IAssetName assetName) + /// Update the character data for matching NPCs. + private void UpdateCharacterData() { - IDictionary data = content.Load>(assetName.BaseName); - bool changed = false; foreach (NPC npc in this.GetCharacters()) { - if (npc.isVillager() && data.ContainsKey(npc.Name)) - { + if (npc.isVillager()) npc.reloadData(); - changed = true; - } } - - return changed; } /// Update the schedules for matching NPCs. @@ -1182,7 +589,7 @@ private bool UpdateNpcSchedules(IAssetName assetName) // reload schedule this.Reflection.GetField(villager, "_hasLoadedMasterScheduleData").SetValue(false); this.Reflection.GetField?>(villager, "_masterScheduleData").SetValue(null); - villager.Schedule = villager.getSchedule(Game1.dayOfMonth); + villager.TryLoadSchedule(); // switch to new schedule if needed if (villager.Schedule != null) @@ -1296,14 +703,9 @@ private IEnumerable GetFarmAnimals() foreach (GameLocation location in this.GetLocations()) { - if (location is Farm farm) + if (location.animals.Length > 0) { - foreach (FarmAnimal animal in farm.animals.Values) - animals.Add(animal); - } - else if (location is AnimalHouse animalHouse) - { - foreach (FarmAnimal animal in animalHouse.animals.Values) + foreach (FarmAnimal animal in location.animals.Values) animals.Add(animal); } } @@ -1360,15 +762,6 @@ private IEnumerable GetLocationsWithInfo(bool buildingInteriors = }); } - /// Get all terrain features in the game. - private IEnumerable GetTerrainFeatures() - { - return this.WorldCache.GetOrSet( - $"{nameof(this.GetTerrainFeatures)}", - () => this.GetLocations().SelectMany(p => p.terrainFeatures.Values).ToArray() - ); - } - /// Get whether two asset names are equivalent if you ignore the locale code. /// The first value to compare. /// The second value to compare. @@ -1402,35 +795,6 @@ private bool IsSameBaseName(IAssetName? left, IAssetName? right) return this.ParseAssetName(path); } - /// Get the segments in a path (e.g. 'a/b' is 'a' and 'b'). - /// The path to check. - private string[] GetSegments(string? path) - { - return path != null - ? PathUtilities.GetSegments(path) - : Array.Empty(); - } - - /// Load a texture from the main content manager. - /// The asset key to load. - private Texture2D LoadTexture(string key) - { - return this.MainContentManager.Load(key); - } - - /// Remove a case-insensitive key from the paint mask cache. - /// The paint mask asset name. - private bool RemoveFromPaintMaskCache(IAssetName assetName) - { - // make cache case-insensitive - // This is needed for cache invalidation since mods may specify keys with a different capitalization - if (!object.ReferenceEquals(BuildingPainter.paintMaskLookup.Comparer, StringComparer.OrdinalIgnoreCase)) - BuildingPainter.paintMaskLookup = new Dictionary>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase); - - // remove key from cache - return BuildingPainter.paintMaskLookup.Remove(assetName.BaseName); - } - /// Metadata about a location used in asset propagation. /// The location instance. /// The building which contains the location, if any. From 377936090593fe3bd9417d1824fb1ec31c2e1f1b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 Mar 2022 22:38:47 -0500 Subject: [PATCH 26/84] update for content pipeline changes in SDV 1.6 --- src/SMAPI/Framework/ContentCoordinator.cs | 2 +- .../ContentManagers/BaseContentManager.cs | 107 +++++++----------- .../ContentManagers/GameContentManager.cs | 7 +- .../ContentManagers/IContentManager.cs | 2 +- .../ContentManagers/ModContentManager.cs | 6 +- 5 files changed, 51 insertions(+), 73 deletions(-) diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 19c346d40..6db79f0c7 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -531,7 +531,7 @@ public TilesheetReference[] GetVanillaTilesheetIds(string assetName) if (language == LocalizedContentManager.LanguageCode.mod && LocalizedContentManager.CurrentModLanguage == null) return null; - return this.MainContentManager.LanguageCodeString(language); + return this.MainContentManager.GetLocale(language); } /// Dispose held resources. diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 1e9f4ffe3..6ec15a8f7 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Content; @@ -48,6 +49,12 @@ internal abstract class BaseContentManager : LocalizedContentManager, IContentMa /// This should be kept empty to avoid keeping disposable assets referenced forever, which prevents garbage collection when they're unused. Disposable assets are tracked by instead, which avoids a hard reference. private readonly List BaseDisposableReferences; + /// A cache of proxy wrappers for the method. + private readonly Dictionary BaseLoadProxyCache = new(); + + /// Whether to check the game folder in the base implementation. + protected bool CheckGameFolderForAssetExists; + /********* ** Accessors @@ -97,36 +104,24 @@ protected BaseContentManager(string name, IServiceProvider serviceProvider, stri } /// - public virtual bool DoesAssetExist(IAssetName assetName) - where T : notnull + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Inherited from base method.")] + public sealed override bool DoesAssetExist(string? localized_asset_name) { - return this.Cache.ContainsKey(assetName.Name); - } + if (string.IsNullOrWhiteSpace(localized_asset_name)) + return false; - /// - [Obsolete("This method is implemented for the base game and should not be used directly. To load an asset from the underlying content manager directly, use " + nameof(BaseContentManager.RawLoad) + " instead.")] - public sealed override T LoadBase(string assetName) - { - return this.Load(assetName, LanguageCode.en); + IAssetName assetName = this.Coordinator.ParseAssetName(this.PrenormalizeRawAssetName(localized_asset_name), allowLocales: this.TryLocalizeKeys); + return this.DoesAssetExist(assetName); } /// - public sealed override string LoadBaseString(string path) + public virtual bool DoesAssetExist(IAssetName assetName) + where T : notnull { - try - { - // copied as-is from LocalizedContentManager.LoadBaseString - // This is only changed to call this.Load instead of base.Load, to support mod assets - this.ParseStringPath(path, out string assetName, out string key); - Dictionary? strings = this.Load?>(assetName, LanguageCode.en); - return strings != null && strings.ContainsKey(key) - ? this.GetString(strings, key) - : path; - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed loading string path '{path}' from '{this.Name}'.", ex); - } + if (this.CheckGameFolderForAssetExists && base.DoesAssetExist(assetName.Name)) + return true; + + return this.Cache.ContainsKey(assetName.Name); } /// @@ -136,11 +131,11 @@ public sealed override T Load(string assetName) } /// - public sealed override T Load(string assetName, LanguageCode language) + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Inherited from base method.")] + public sealed override T LoadImpl(string base_asset_name, string localized_asset_name, LanguageCode language_code) { - assetName = this.PrenormalizeRawAssetName(assetName); - IAssetName parsedName = this.Coordinator.ParseAssetName(assetName, allowLocales: this.TryLocalizeKeys); - return this.LoadLocalized(parsedName, language, useCache: true); + IAssetName assetName = this.Coordinator.ParseAssetName(this.PrenormalizeRawAssetName(localized_asset_name), allowLocales: this.TryLocalizeKeys); + return this.LoadExact(assetName, useCache: true); } /// @@ -156,7 +151,7 @@ public T LoadLocalized(IAssetName assetName, LanguageCode language, bool useC Dictionary localizedAssetNames = this.Coordinator.LocalizedAssetNames.Value; if (!localizedAssetNames.TryGetValue(assetName.Name, out _)) { - string localeCode = this.LanguageCodeString(language); + string localeCode = this.GetLocale(language); IAssetName localizedName = new AssetName(baseName: assetName.BaseName, localeCode: localeCode, languageCode: language); try @@ -212,13 +207,15 @@ public string AssertAndNormalizeAssetName(string? assetName) /// public string GetLocale() { - return this.GetLocale(this.GetCurrentLanguage()); + return LocalizedContentManager.CurrentLanguageString; } /// public string GetLocale(LanguageCode language) { - return this.LanguageCodeString(language); + return language == LocalizedContentManager.CurrentLanguageCode + ? LocalizedContentManager.CurrentLanguageString + : LocalizedContentManager.LanguageCodeString(language); } /// @@ -321,9 +318,22 @@ public override void Unload() /// Whether to read/write the loaded asset to the asset cache. protected virtual T RawLoad(IAssetName assetName, bool useCache) { - return useCache - ? base.LoadBase(assetName.Name) - : this.ReadAsset(assetName.Name, disposable => this.Disposables.Add(new WeakReference(disposable))); + if (useCache) + { + if (!this.BaseLoadProxyCache.TryGetValue(typeof(T), out object? cacheEntry)) + { + MethodInfo method = typeof(ContentManager).GetMethod(nameof(ContentManager.Load)) ?? throw new InvalidOperationException($"Can't get required method '{nameof(ContentManager)}.{nameof(ContentManager.Load)}'."); + method = method.MakeGenericMethod(typeof(T)); + IntPtr pointer = method.MethodHandle.GetFunctionPointer(); + this.BaseLoadProxyCache[typeof(T)] = cacheEntry = Activator.CreateInstance(typeof(Func), this, pointer) ?? throw new InvalidOperationException($"Can't proxy required method '{nameof(ContentManager)}.{nameof(ContentManager.Load)}'."); + } + + Func baseLoad = (Func)cacheEntry; + + return baseLoad(assetName.Name); + } + + return this.ReadAsset(assetName.Name, disposable => this.Disposables.Add(new WeakReference(disposable))); } /// Add tracking data to an asset and add it to the cache. @@ -349,34 +359,5 @@ protected virtual void TrackAsset(IAssetName assetName, T value, bool useCach // avoid hard disposable references; see remarks on the field this.BaseDisposableReferences.Clear(); } - - /**** - ** Private methods copied from the game code - ****/ -#pragma warning disable CS1574 // can't be resolved: the reference is valid but private - /// Parse a string path like assetName:key. - /// The string path. - /// The extracted asset name. - /// The extracted entry key. - /// The string path is not in a valid format. - /// This is copied as-is from . - private void ParseStringPath(string path, out string assetName, out string key) - { - int length = path.IndexOf(':'); - assetName = length != -1 ? path.Substring(0, length) : throw new ContentLoadException("Unable to parse string path: " + path); - key = path.Substring(length + 1, path.Length - length - 1); - } - - /// Get a string value from a dictionary asset. - /// The asset to read. - /// The string key to find. - /// This is copied as-is from . - private string GetString(Dictionary strings, string key) - { - return strings.TryGetValue(key + ".desktop", out string? str) - ? str - : strings[key]; - } -#pragma warning restore CS1574 } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 4d92db0f6..7244a20e9 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; @@ -56,6 +55,8 @@ public GameContentManager(string name, IServiceProvider serviceProvider, string { this.OnLoadingFirstAsset = onLoadingFirstAsset; this.OnAssetLoaded = onAssetLoaded; + + this.CheckGameFolderForAssetExists = true; } /// @@ -64,10 +65,6 @@ public override bool DoesAssetExist(IAssetName assetName) if (base.DoesAssetExist(assetName)) return true; - // vanilla asset - if (File.Exists(Path.Combine(this.RootDirectory, $"{assetName.Name}.xnb"))) - return true; - // managed asset if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath)) return this.Coordinator.DoesManagedAssetExist(contentManagerID, relativePath); diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index f2e3b9f05..8b146ddac 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -57,7 +57,7 @@ T LoadExact(IAssetName assetName, bool useCache) /// Get the current content locale. string GetLocale(); - /// The locale for a language. + /// Get the locale for a language. /// The language. string GetLocale(LocalizedContentManager.LanguageCode language); diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index dd4061acf..d19ce4329 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -86,13 +86,13 @@ public override bool DoesAssetExist(IAssetName assetName) /// public override T LoadExact(IAssetName assetName, bool useCache) { - // disable caching + // + // Note: caching is ignored for mod content. // This is necessary to avoid assets being shared between content managers, which can // cause changes to an asset through one content manager affecting the same asset in // others (or even fresh content managers). See https://www.patreon.com/posts/27247161 // for more background info. - if (useCache) - throw new InvalidOperationException("Mod content managers don't support asset caching."); + // // resolve managed asset key { From 7787e5a3f9b2256956876e3cabc5fda357092d63 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 19 Mar 2022 17:22:00 -0400 Subject: [PATCH 27/84] update for save load changes in SDV 1.6 --- .../Commands/Other/ApplySaveFixCommand.cs | 21 ++++++++-- src/SMAPI/Framework/SCore.cs | 1 + src/SMAPI/Framework/SModHooks.cs | 20 ++++++++- src/SMAPI/Patches/Game1Patcher.cs | 41 +------------------ 4 files changed, 38 insertions(+), 45 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs index f2194cff3..01303194f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using StardewValley; +using StardewValley.SaveMigrations; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { @@ -39,7 +40,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg } // validate fix ID - if (!Enum.TryParse(rawFixId, ignoreCase: true, out SaveGame.SaveFixes fixId)) + if (!Enum.TryParse(rawFixId, ignoreCase: true, out SaveFixes fixId)) { monitor.Log($"Invalid save ID '{rawFixId}'. Type 'help apply_save_fix' for details.", LogLevel.Error); return; @@ -50,7 +51,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg monitor.Log($"Trying to apply save fix ID: '{fixId}'.", LogLevel.Warn); try { - Game1.applySaveFix(fixId); + SaveMigrator.ApplySingleSaveFix(fixId, this.GetLoadedItems()); monitor.Log("Save fix applied.", LogLevel.Info); } catch (Exception ex) @@ -64,12 +65,24 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg /********* ** Private methods *********/ + /// Get all item instances in the world. + private List GetLoadedItems() + { + List loadedItems = new(); + Utility.ForEachItem(item => + { + loadedItems.Add(item); + return true; + }); + return loadedItems; + } + /// Get the valid save fix IDs. private IEnumerable GetSaveIds() { - foreach (SaveGame.SaveFixes id in Enum.GetValues(typeof(SaveGame.SaveFixes))) + foreach (SaveFixes id in Enum.GetValues(typeof(SaveFixes))) { - if (id == SaveGame.SaveFixes.MAX) + if (id == SaveFixes.MAX) continue; yield return id.ToString(); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c79c971ee..254cd4bb8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -258,6 +258,7 @@ public void RunInteractively() modHooks: new SModHooks( parent: new ModHooks(), beforeNewDayAfterFade: this.OnNewDayAfterFade, + onStageChanged: this.OnLoadStageChanged, monitor: this.Monitor ), multiplayer: this.Multiplayer, diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs index ac4f242cb..8ec3e506f 100644 --- a/src/SMAPI/Framework/SModHooks.cs +++ b/src/SMAPI/Framework/SModHooks.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using StardewModdingAPI.Enums; using StardewModdingAPI.Utilities; using StardewValley; @@ -17,6 +18,9 @@ internal class SModHooks : DelegatingModHooks /// Writes messages to the console. private readonly IMonitor Monitor; + /// A callback to invoke when the load stage changes. + private readonly Action OnStageChanged; + /********* ** Public methods @@ -24,11 +28,13 @@ internal class SModHooks : DelegatingModHooks /// Construct an instance. /// The underlying hooks to call by default. /// A callback to invoke before runs. + /// A callback to invoke when the load stage changes. /// Writes messages to the console. - public SModHooks(ModHooks parent, Action beforeNewDayAfterFade, IMonitor monitor) + public SModHooks(ModHooks parent, Action beforeNewDayAfterFade, Action onStageChanged, IMonitor monitor) : base(parent) { this.BeforeNewDayAfterFade = beforeNewDayAfterFade; + this.OnStageChanged = onStageChanged; this.Monitor = monitor; } @@ -56,5 +62,17 @@ public override Task StartTask(Task task, string id) this.Monitor.Log(" task complete."); return task; } + + /// A hook invoked when creating a new save slot, after the game has added the location instances but before it fully initializes them. + public override void CreatedInitialLocations() + { + this.OnStageChanged(LoadStage.CreatedInitialLocations); + } + + /// A hook invoked when loading a save slot, after the game has added the location instances but before it restores their save data. Not applicable when connecting to a multiplayer host. + public override void SaveAddedLocations() + { + this.OnStageChanged(LoadStage.SaveAddedLocations); + } } } diff --git a/src/SMAPI/Patches/Game1Patcher.cs b/src/SMAPI/Patches/Game1Patcher.cs index 8f8067900..195e00f6f 100644 --- a/src/SMAPI/Patches/Game1Patcher.cs +++ b/src/SMAPI/Patches/Game1Patcher.cs @@ -25,9 +25,6 @@ internal class Game1Patcher : BasePatcher /// A callback to invoke when the load stage changes. private static Action OnStageChanged = null!; // initialized in constructor - /// Whether the game is running running the code in . - private static bool IsInLoadForNewGame; - /********* ** Public methods @@ -44,16 +41,9 @@ public Game1Patcher(Reflector reflection, Action onStageChanged) /// public override void Apply(Harmony harmony, IMonitor monitor) { - // detect CreatedInitialLocations and SaveAddedLocations - harmony.Patch( - original: this.RequireMethod(nameof(Game1.AddModNPCs)), - prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_AddModNpcs)) - ); - - // detect CreatedLocations, and track IsInLoadForNewGame + // detect CreatedLocations harmony.Patch( original: this.RequireMethod(nameof(Game1.loadForNewGame)), - prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_LoadForNewGame)), postfix: this.GetHarmonyMethod(nameof(Game1Patcher.After_LoadForNewGame)) ); @@ -68,24 +58,6 @@ public override void Apply(Harmony harmony, IMonitor monitor) /********* ** Private methods *********/ - /// The method to call before . - /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_AddModNpcs() - { - // When this method is called from Game1.loadForNewGame, it happens right after adding the vanilla - // locations but before initializing them. - if (Game1Patcher.IsInLoadForNewGame) - { - Game1Patcher.OnStageChanged(Game1Patcher.IsCreating() - ? LoadStage.CreatedInitialLocations - : LoadStage.SaveAddedLocations - ); - } - - return true; - } - /// The method to call before . /// Returns whether to execute the original method. /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. @@ -95,21 +67,10 @@ private static bool Before_CleanupReturningToTitle() return true; } - /// The method to call before . - /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_LoadForNewGame() - { - Game1Patcher.IsInLoadForNewGame = true; - return true; - } - /// The method to call after . /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. private static void After_LoadForNewGame() { - Game1Patcher.IsInLoadForNewGame = false; - if (Game1Patcher.IsCreating()) Game1Patcher.OnStageChanged(LoadStage.CreatedLocations); } From 5483094c3e805833ed50ebca1c0cb49a19d52a18 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 20:25:01 -0400 Subject: [PATCH 28/84] update for new render steps & mod hooks in SDV 1.6 --- src/SMAPI/Framework/SCore.cs | 117 +++- src/SMAPI/Framework/SGame.cs | 697 +--------------------- src/SMAPI/Framework/SGameRunner.cs | 27 +- src/SMAPI/Framework/SModHooks.cs | 55 +- src/SMAPI/Utilities/DelegatingModHooks.cs | 56 ++ 5 files changed, 251 insertions(+), 701 deletions(-) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 254cd4bb8..f443aa215 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; #endif @@ -44,6 +45,7 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; +using StardewValley.Mods; using StardewValley.Objects; using StardewValley.SDKs; using xTile.Display; @@ -145,6 +147,10 @@ internal class SCore : IDisposable /// A list of commands to execute on each screen. private readonly PerScreen> ScreenCommandQueue = new(() => new List()); + /// The last for which display events were raised. + private readonly PerScreen LastRenderEventTick = new(); + + /********* ** Accessors *********/ @@ -254,11 +260,12 @@ public void RunInteractively() this.Game = new SGameRunner( monitor: this.Monitor, reflection: this.Reflection, - eventManager: this.EventManager, modHooks: new SModHooks( parent: new ModHooks(), beforeNewDayAfterFade: this.OnNewDayAfterFade, onStageChanged: this.OnLoadStageChanged, + onRenderingStep: this.OnRenderingStep, + onRenderedStep: this.OnRenderedStep, monitor: this.Monitor ), multiplayer: this.Multiplayer, @@ -267,6 +274,7 @@ public void RunInteractively() onGameContentLoaded: this.OnInstanceContentLoaded, onGameUpdating: this.OnGameUpdating, onPlayerInstanceUpdating: this.OnPlayerInstanceUpdating, + onPlayerInstanceRendered: this.OnRendered, onGameExiting: this.OnGameExiting ); StardewValley.GameRunner.instance = this.Game; @@ -1182,6 +1190,113 @@ internal void OnLoadStageChanged(LoadStage newStage) events.ReturnedToTitle.RaiseEmpty(); } + /// Raised when the game starts a render step in the draw loop. + /// The render step being started. + /// The sprite batch being drawn (which might not always be open yet). + /// The render target being drawn. + private void OnRenderingStep(RenderSteps step, SpriteBatch spriteBatch, RenderTarget2D renderTarget) + { + EventManager events = this.EventManager; + + // raise 'Rendering' before first event + if (this.LastRenderEventTick.Value != SCore.TicksElapsed) + { + this.RaiseRenderEvent(events.Rendering, spriteBatch, renderTarget); + this.LastRenderEventTick.Value = SCore.TicksElapsed; + } + + // raise other events + switch (step) + { + case RenderSteps.World: + this.RaiseRenderEvent(events.RenderingWorld, spriteBatch, renderTarget); + break; + + case RenderSteps.Menu: + this.RaiseRenderEvent(events.RenderingActiveMenu, spriteBatch, renderTarget); + break; + + case RenderSteps.HUD: + this.RaiseRenderEvent(events.RenderingHud, spriteBatch, renderTarget); + break; + } + } + + /// Raised when the game finishes a render step in the draw loop. + /// The render step being started. + /// The sprite batch being drawn (which might not always be open yet). + /// The render target being drawn. + private void OnRenderedStep(RenderSteps step, SpriteBatch spriteBatch, RenderTarget2D renderTarget) + { + var events = this.EventManager; + + switch (step) + { + case RenderSteps.World: + this.RaiseRenderEvent(events.RenderedWorld, spriteBatch, renderTarget); + break; + + case RenderSteps.Menu: + this.RaiseRenderEvent(events.RenderedActiveMenu, spriteBatch, renderTarget); + break; + + case RenderSteps.HUD: + this.RaiseRenderEvent(events.RenderedHud, spriteBatch, renderTarget); + break; + } + } + + /// Raised after an instance finishes a draw loop. + /// The render target being drawn to the screen. + private void OnRendered(RenderTarget2D renderTarget) + { + this.RaiseRenderEvent(this.EventManager.Rendered, Game1.spriteBatch, renderTarget); + } + + /// Raise a rendering/rendered event, temporarily opening the given sprite batch if needed to let mods draw to it. + /// The event args type to construct. + /// The event to raise. + /// The sprite batch being drawn to the screen. + /// The render target being drawn to the screen. + private void RaiseRenderEvent(ManagedEvent @event, SpriteBatch spriteBatch, RenderTarget2D renderTarget) + where TEventArgs : EventArgs, new() + { + if (!@event.HasListeners) + return; + + bool wasOpen = spriteBatch.IsOpen(this.Reflection); + bool hadRenderTarget = Game1.graphics.GraphicsDevice.RenderTargetCount > 0; + + if (!hadRenderTarget && !Game1.IsOnMainThread()) + return; // can't set render target on background thread + + try + { + if (!wasOpen) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); + + if (!hadRenderTarget) + { + renderTarget ??= Game1.game1.uiScreen?.IsDisposed != true + ? Game1.game1.uiScreen + : Game1.nonUIRenderTarget; + + if (renderTarget != null) + Game1.SetRenderTarget(renderTarget); + } + + @event.RaiseEmpty(); + } + finally + { + if (!wasOpen) + spriteBatch.End(); + + if (!hadRenderTarget && renderTarget != null) + Game1.SetRenderTarget(null); + } + } + /// A callback invoked before runs. protected void OnNewDayAfterFade() { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a3d60a3be..822935c6d 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1,11 +1,9 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; -using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Snapshots; @@ -13,13 +11,6 @@ using StardewModdingAPI.Internal; using StardewModdingAPI.Utilities; using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Locations; -using StardewValley.Menus; -using StardewValley.Tools; -using xTile.Dimensions; -using xTile.Layers; -using xTile.Tiles; namespace StardewModdingAPI.Framework { @@ -32,9 +23,6 @@ internal class SGame : Game1 /// Encapsulates monitoring and logging for SMAPI. private readonly Monitor Monitor; - /// Manages SMAPI events for mods. - private readonly EventManager Events; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new(60); // 60 ticks = roughly one second @@ -56,6 +44,9 @@ internal class SGame : Game1 /// Raised after the instance finishes loading its initial content. private readonly Action OnContentLoaded; + /// Raised after the instance finishes a draw loop. + private readonly Action OnRendered; + /********* ** Accessors @@ -99,14 +90,14 @@ internal class SGame : Game1 /// The instance index. /// Encapsulates monitoring and logging for SMAPI. /// Simplifies access to private game code. - /// Manages SMAPI events for mods. /// Manages the game's input state. /// Handles mod hooks provided by the game. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised when the instance is updating its state (roughly 60 times per second). /// Raised after the game finishes loading its initial content. - public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflector reflection, EventManager eventManager, SInputState input, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onUpdating, Action onContentLoaded) + /// Raised after the instance finishes a draw loop. + public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflector reflection, SInputState input, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onUpdating, Action onContentLoaded, Action onRendered) : base(playerIndex, instanceIndex) { // init XNA @@ -120,11 +111,11 @@ public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflec // init SMAPI this.Monitor = monitor; - this.Events = eventManager; this.Reflection = reflection; this.ExitGameImmediately = exitGameImmediately; this.OnUpdating = onUpdating; this.OnContentLoaded = onContentLoaded; + this.OnRendered = onRendered; } /// Get the current input state for a button. @@ -206,13 +197,14 @@ protected override void _draw(GameTime gameTime, RenderTarget2D target_screen) Context.IsInDrawLoop = true; try { - this.DrawImpl(gameTime, target_screen); + base._draw(gameTime, target_screen); + this.OnRendered(target_screen); this.DrawCrashTimer.Reset(); } catch (Exception ex) { // log error - this.Monitor.Log($"An error occurred in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"An error occurred in the game's draw loop: {ex.GetLogSummary()}", LogLevel.Error); // exit if irrecoverable if (!this.DrawCrashTimer.Decrement()) @@ -241,676 +233,5 @@ protected override void _draw(GameTime gameTime, RenderTarget2D target_screen) } Context.IsInDrawLoop = false; } - -#nullable disable - /// Replicate the game's draw logic with some changes for SMAPI. - /// A snapshot of the game timing state. - /// The render target, if any. - /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. - [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "ForCanBeConvertedToForeach", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "PossibleNullReferenceException", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")] - [SuppressMessage("ReSharper", "ReturnValueOfPureMethodIsNotUsed", Justification = "copied from game code as-is")] - [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] - [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")] - [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] - - [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Deliberate to minimize chance of errors when copying event calls into new versions of this code.")] - private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) - { - var events = this.Events; - - Game1.showingHealthBar = false; - if (Game1._newDayTask != null || this.isLocalMultiplayerNewDayActive) - { - base.GraphicsDevice.Clear(Game1.bgColor); - return; - } - if (target_screen != null) - { - Game1.SetRenderTarget(target_screen); - } - if (this.IsSaving) - { - base.GraphicsDevice.Clear(Game1.bgColor); - Game1.PushUIMode(); - IClickableMenu menu = Game1.activeClickableMenu; - if (menu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - events.Rendering.RaiseEmpty(); - try - { - events.RenderingActiveMenu.RaiseEmpty(); - menu.draw(Game1.spriteBatch); - events.RenderedActiveMenu.RaiseEmpty(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - activeClickableMenu.exitThisMenu(); - } - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - Game1.PopUIMode(); - return; - } - base.GraphicsDevice.Clear(Game1.bgColor); - if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot) - { - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - - events.Rendering.RaiseEmpty(); - IClickableMenu curMenu = null; - try - { - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - events.RenderingActiveMenu.RaiseEmpty(); - for (curMenu = Game1.activeClickableMenu; curMenu != null; curMenu = curMenu.GetChildMenu()) - { - curMenu.draw(Game1.spriteBatch); - } - events.RenderedActiveMenu.RaiseEmpty(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {curMenu.GetMenuChainLabel()} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - events.Rendered.RaiseEmpty(); - if (Game1.specialCurrencyDisplay != null) - { - Game1.specialCurrencyDisplay.Draw(Game1.spriteBatch); - } - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - Game1.PopUIMode(); - return; - } - if (Game1.gameMode == 11) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - events.Rendering.RaiseEmpty(); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, 255, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - return; - } - if (Game1.currentMinigame != null) - { - if (events.Rendering.HasListeners) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - events.Rendering.RaiseEmpty(); - Game1.spriteBatch.End(); - } - - Game1.currentMinigame.draw(Game1.spriteBatch); - if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); - Game1.PopUIMode(); - } - Game1.PushUIMode(); - this.drawOverlays(Game1.spriteBatch); - Game1.PopUIMode(); - if (events.Rendered.HasListeners) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - } - Game1.SetRenderTarget(target_screen); - return; - } - if (Game1.showingEndOfNightStuff) - { - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - events.Rendering.RaiseEmpty(); - if (Game1.activeClickableMenu != null) - { - IClickableMenu curMenu = null; - try - { - events.RenderingActiveMenu.RaiseEmpty(); - for (curMenu = Game1.activeClickableMenu; curMenu != null; curMenu = curMenu.GetChildMenu()) - { - curMenu.draw(Game1.spriteBatch); - } - events.RenderedActiveMenu.RaiseEmpty(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {curMenu.GetMenuChainLabel()} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - Game1.PopUIMode(); - return; - } - if (Game1.gameMode == 6 || (Game1.gameMode == 3 && Game1.currentLocation == null)) - { - Game1.PushUIMode(); - base.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - events.Rendering.RaiseEmpty(); - string addOn = "".PadRight((int)Math.Ceiling(gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0), '.'); - string text = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string msg = text + addOn; - string largestMessage = text + "... "; - int msgw = SpriteText.getWidthOfString(largestMessage); - int msgh = 64; - int msgx = 64; - int msgy = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - msgh; - SpriteText.drawString(Game1.spriteBatch, msg, msgx, msgy, 999999, msgw, msgh, 1f, 0.88f, junimoText: false, 0, largestMessage); - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - Game1.PopUIMode(); - return; - } - if (Game1.gameMode == 0) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - events.Rendering.RaiseEmpty(); - } - else - { - if (Game1.gameMode == 3 && Game1.dayOfMonth == 0 && Game1.newDay) - { - //base.Draw(gameTime); - return; - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - bool renderingRaised = false; - if (Game1.drawLighting) - { - this.DrawLighting(gameTime, target_screen, out renderingRaised); - } - base.GraphicsDevice.Clear(Game1.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (!renderingRaised) - events.Rendering.RaiseEmpty(); - events.RenderingWorld.RaiseEmpty(); - if (Game1.background != null) - { - Game1.background.draw(Game1.spriteBatch); - } - Game1.currentLocation.drawBackground(Game1.spriteBatch); - Game1.spriteBatch.End(); - for (int i = 0; i < Game1.currentLocation.backgroundLayers.Count; i++) - { - Game1.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.currentLocation.backgroundLayers[i].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, -1f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.currentLocation.drawWater(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.currentLocation.drawFloorDecorations(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - this._farmerShadows.Clear(); - if (Game1.currentLocation.currentEvent != null && !Game1.currentLocation.currentEvent.isFestival && Game1.currentLocation.currentEvent.farmerActors.Count > 0) - { - foreach (Farmer f in Game1.currentLocation.currentEvent.farmerActors) - { - if ((f.IsLocalPlayer && Game1.displayFarmer) || !f.hidden) - { - this._farmerShadows.Add(f); - } - } - } - else - { - foreach (Farmer f2 in Game1.currentLocation.farmers) - { - if ((f2.IsLocalPlayer && Game1.displayFarmer) || !f2.hidden) - { - this._farmerShadows.Add(f2); - } - } - } - if (!Game1.currentLocation.shouldHideCharacters()) - { - if (Game1.CurrentEvent == null) - { - foreach (NPC n in Game1.currentLocation.characters) - { - if (!n.swimming && !n.HideShadow && !n.IsInvisible && !this.checkCharacterTilesForShadowDrawFlag(n)) - { - n.DrawShadow(Game1.spriteBatch); - } - } - } - else - { - foreach (NPC n2 in Game1.CurrentEvent.actors) - { - if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n2)) && !n2.swimming && !n2.HideShadow && !this.checkCharacterTilesForShadowDrawFlag(n2)) - { - n2.DrawShadow(Game1.spriteBatch); - } - } - } - foreach (Farmer f3 in this._farmerShadows) - { - if (!Game1.multiplayer.isDisconnecting(f3.UniqueMultiplayerID) && !f3.swimming && !f3.isRidingHorse() && !f3.IsSitting() && (Game1.currentLocation == null || !this.checkCharacterTilesForShadowDrawFlag(f3))) - { - f3.DrawShadow(Game1.spriteBatch); - } - } - } - float layer_sub_sort = 0.1f; - for (int j = 0; j < Game1.currentLocation.buildingLayers.Count; j++) - { - float layer = 0f; - if (Game1.currentLocation.buildingLayers.Count > 1) - { - layer = (float)j / (float)(Game1.currentLocation.buildingLayers.Count - 1); - } - Game1.currentLocation.buildingLayers[j].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, layer_sub_sort * layer); - } - Layer building_layer = Game1.currentLocation.Map.GetLayer("Buildings"); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); - if (!Game1.currentLocation.shouldHideCharacters()) - { - if (Game1.CurrentEvent == null) - { - foreach (NPC n3 in Game1.currentLocation.characters) - { - if (!n3.swimming && !n3.HideShadow && !n3.isInvisible && this.checkCharacterTilesForShadowDrawFlag(n3)) - { - n3.DrawShadow(Game1.spriteBatch); - } - } - } - else - { - foreach (NPC n4 in Game1.CurrentEvent.actors) - { - if ((Game1.CurrentEvent == null || !Game1.CurrentEvent.ShouldHideCharacter(n4)) && !n4.swimming && !n4.HideShadow && this.checkCharacterTilesForShadowDrawFlag(n4)) - { - n4.DrawShadow(Game1.spriteBatch); - } - } - } - foreach (Farmer f4 in this._farmerShadows) - { - Math.Max(0.0001f, f4.getDrawLayer() + 0.00011f); - if (!f4.swimming && !f4.isRidingHorse() && !f4.IsSitting() && Game1.currentLocation != null && this.checkCharacterTilesForShadowDrawFlag(f4)) - { - f4.DrawShadow(Game1.spriteBatch); - } - } - } - if ((Game1.eventUp || Game1.killScreen) && !Game1.killScreen && Game1.currentLocation.currentEvent != null) - { - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - } - Game1.currentLocation.draw(Game1.spriteBatch); - foreach (Vector2 tile_position in Game1.crabPotOverlayTiles.Keys) - { - Tile tile = building_layer.Tiles[(int)tile_position.X, (int)tile_position.Y]; - if (tile != null) - { - Vector2 vector_draw_position = Game1.GlobalToLocal(Game1.viewport, tile_position * 64f); - Location draw_location = new((int)vector_draw_position.X, (int)vector_draw_position.Y); - Game1.mapDisplayDevice.DrawTile(tile, draw_location, (tile_position.Y * 64f - 1f) / 10000f); - } - } - if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool)) - { - Game1.drawTool(Game1.player); - } - if (Game1.tvStation >= 0) - { - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - } - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f); - foreach (Warp w in Game1.currentLocation.warps) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(w.X * 64 - Game1.viewport.X, w.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); - } - } - for (int l = 0; l < Game1.currentLocation.frontLayers.Count; l++) - { - float layer2 = 0f; - if (Game1.currentLocation.frontLayers.Count > 1) - { - layer2 = (float)l / (float)(Game1.currentLocation.frontLayers.Count - 1); - } - Game1.currentLocation.frontLayers[l].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, 64f + layer_sub_sort * layer2); - } - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - for (int m = 0; m < Game1.currentLocation.alwaysFrontLayers.Count; m++) - { - Game1.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.currentLocation.alwaysFrontLayers[m].Key.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, wrapAround: false, 4, -1f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Texture, BlendState.AlphaBlend, SamplerState.PointClamp); - if (!Game1.IsFakedBlackScreen()) - { - this.drawWeather(gameTime, target_screen); - } - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - } - if (Game1.screenGlow) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - } - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (Game1.toolHold > 400f && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) - { - Color barColor = Color.White; - switch ((int)(Game1.toolHold / 600f) + 2) - { - case 1: - barColor = Tool.copperColor; - break; - case 2: - barColor = Tool.steelColor; - break; - case 3: - barColor = Tool.goldColor; - break; - case 4: - barColor = Tool.iridiumColor; - break; - } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, 12), Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - ((!Game1.player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0), (int)(Game1.toolHold % 600f * 0.08f), 8), barColor); - } - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0f || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)) - { - Game1.player.CurrentTool.draw(Game1.spriteBatch); - } - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - foreach (NPC n5 in Game1.currentLocation.currentEvent.actors) - { - if (n5.isEmoting) - { - Vector2 emotePosition = n5.getLocalPosition(Game1.viewport); - if (n5.NeedsBirdieEmoteHack()) - { - emotePosition.X += 64f; - } - emotePosition.Y -= 140f; - if (n5.Age == 2) - { - emotePosition.Y += 32f; - } - else if (n5.Gender == 1) - { - emotePosition.Y += 10f; - } - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, emotePosition, new Microsoft.Xna.Framework.Rectangle(n5.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, n5.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)n5.getStandingY() / 10000f); - } - } - } - Game1.spriteBatch.End(); - Game1.mapDisplayDevice.EndScene(); - if (Game1.drawLighting && !Game1.IsFakedBlackScreen()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp); - Viewport vp = base.GraphicsDevice.Viewport; - vp.Bounds = target_screen?.Bounds ?? base.GraphicsDevice.PresentationParameters.Bounds; - base.GraphicsDevice.Viewport = vp; - float render_zoom = Game1.options.lightingQuality / 2; - if (this.useUnscaledLighting) - { - render_zoom /= Game1.options.zoomLevel; - } - Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Color.White, 0f, Vector2.Zero, render_zoom, SpriteEffects.None, 1f); - if (Game1.IsRainingHere() && (bool)Game1.currentLocation.isOutdoors) - { - Game1.spriteBatch.Draw(Game1.staminaRect, vp.Bounds, Color.OrangeRed * 0.45f); - } - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - events.RenderedWorld.RaiseEmpty(); - if (Game1.drawGrid) - { - int startingX = -Game1.viewport.X % 64; - float startingY = -Game1.viewport.Y % 64; - for (int x = startingX; x < Game1.graphics.GraphicsDevice.Viewport.Width; x += 64) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x, (int)startingY, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); - } - for (float y = startingY; y < (float)Game1.graphics.GraphicsDevice.Viewport.Height; y += 64f) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(startingX, (int)y, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); - } - } - if (Game1.ShouldShowOnscreenUsernames() && Game1.currentLocation != null) - { - Game1.currentLocation.DrawFarmerUsernames(Game1.spriteBatch); - } - if (Game1.currentBillboard != 0 && !this.takingMapScreenshot) - { - this.drawBillboard(); - } - if (!Game1.eventUp && Game1.farmEvent == null && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !this.takingMapScreenshot && Game1.isOutdoorMapSmallerThanViewport()) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -Game1.viewport.X, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64, 0, Game1.graphics.GraphicsDevice.Viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64), Game1.graphics.GraphicsDevice.Viewport.Height), Color.Black); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, Game1.graphics.GraphicsDevice.Viewport.Width, -Game1.viewport.Y), Color.Black); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, -Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64, Game1.graphics.GraphicsDevice.Viewport.Width, Game1.graphics.GraphicsDevice.Viewport.Height - (-Game1.viewport.Y + Game1.currentLocation.map.Layers[0].LayerHeight * 64)), Color.Black); - } - Game1.spriteBatch.End(); - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if ((Game1.displayHUD || Game1.eventUp) && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !Game1.freezeControls && !Game1.panMode && !Game1.HostPaused && !this.takingMapScreenshot) - { - events.RenderingHud.RaiseEmpty(); - this.drawHUD(); - events.RenderedHud.RaiseEmpty(); - } - else if (Game1.activeClickableMenu == null) - { - _ = Game1.farmEvent; - } - if (Game1.hudMessages.Count > 0 && !this.takingMapScreenshot) - { - for (int k = Game1.hudMessages.Count - 1; k >= 0; k--) - { - Game1.hudMessages[k].draw(Game1.spriteBatch, k); - } - } - Game1.spriteBatch.End(); - Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - } - if (Game1.farmEvent != null) - { - Game1.farmEvent.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - } - Game1.PushUIMode(); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)) && !this.takingMapScreenshot) - { - this.drawDialogueBox(); - } - if (Game1.progressBar && !this.takingMapScreenshot) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(Game1.pauseAccumulator / Game1.pauseTime * (float)Game1.dialogueWidth), 32), Color.DimGray); - } - Game1.spriteBatch.End(); - Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) - { - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - } - if (!Game1.IsFakedBlackScreen() && Game1.IsRainingHere() && Game1.currentLocation != null && (bool)Game1.currentLocation.isOutdoors) - { - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); - } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause) && !this.takingMapScreenshot) - { - Game1.spriteBatch.End(); - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); - Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - } - else if (Game1.flashAlpha > 0f && !this.takingMapScreenshot) - { - if (Game1.options.screenFlash) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha)); - } - Game1.flashAlpha -= 0.1f; - } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp && !this.takingMapScreenshot) - { - this.drawDialogueBox(); - } - if (!this.takingMapScreenshot) - { - foreach (TemporaryAnimatedSprite screenOverlayTempSprite in Game1.screenOverlayTempSprites) - { - screenOverlayTempSprite.draw(Game1.spriteBatch, localPosition: true); - } - Game1.spriteBatch.End(); - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - foreach (TemporaryAnimatedSprite uiOverlayTempSprite in Game1.uiOverlayTempSprites) - { - uiOverlayTempSprite.draw(Game1.spriteBatch, localPosition: true); - } - Game1.spriteBatch.End(); - Game1.PopUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - } - if (Game1.debugMode) - { - StringBuilder sb = Game1._debugStringBuilder; - sb.Clear(); - if (Game1.panMode) - { - sb.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64); - sb.Append(","); - sb.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64); - } - else - { - sb.Append("player: "); - sb.Append(Game1.player.getStandingX() / 64); - sb.Append(", "); - sb.Append(Game1.player.getStandingY() / 64); - } - sb.Append(" mouseTransparency: "); - sb.Append(Game1.mouseCursorTransparency); - sb.Append(" mousePosition: "); - sb.Append(Game1.getMouseX()); - sb.Append(","); - sb.Append(Game1.getMouseY()); - sb.Append(Environment.NewLine); - sb.Append(" mouseWorldPosition: "); - sb.Append(Game1.getMouseX() + Game1.viewport.X); - sb.Append(","); - sb.Append(Game1.getMouseY() + Game1.viewport.Y); - sb.Append(" debugOutput: "); - sb.Append(Game1.debugOutput); - Game1.spriteBatch.DrawString(Game1.smallFont, sb, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8), Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - } - Game1.spriteBatch.End(); - Game1.PushUIMode(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); - if (Game1.showKeyHelp && !this.takingMapScreenshot) - { - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? (192 + (Game1.isQuestion ? (Game1.questionChoices.Count * 64) : 0)) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - } - if (Game1.activeClickableMenu != null && !this.takingMapScreenshot) - { - IClickableMenu curMenu = null; - try - { - events.RenderingActiveMenu.RaiseEmpty(); - for (curMenu = Game1.activeClickableMenu; curMenu != null; curMenu = curMenu.GetChildMenu()) - { - curMenu.draw(Game1.spriteBatch); - } - events.RenderedActiveMenu.RaiseEmpty(); - } - catch (Exception ex) - { - this.Monitor.Log($"The {curMenu.GetMenuChainLabel()} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - else if (Game1.farmEvent != null) - { - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - } - if (Game1.specialCurrencyDisplay != null) - { - Game1.specialCurrencyDisplay.Draw(Game1.spriteBatch); - } - if (Game1.emoteMenu != null && !this.takingMapScreenshot) - { - Game1.emoteMenu.draw(Game1.spriteBatch); - } - if (Game1.HostPaused && !this.takingMapScreenshot) - { - string msg2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); - SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, msg2, 96, 32); - } - events.Rendered.RaiseEmpty(); - Game1.spriteBatch.End(); - this.drawOverlays(Game1.spriteBatch); - Game1.PopUIMode(); - } -#nullable enable } } diff --git a/src/SMAPI/Framework/SGameRunner.cs b/src/SMAPI/Framework/SGameRunner.cs index 213fe561f..9344ceafa 100644 --- a/src/SMAPI/Framework/SGameRunner.cs +++ b/src/SMAPI/Framework/SGameRunner.cs @@ -3,7 +3,6 @@ using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -19,9 +18,6 @@ internal class SGameRunner : GameRunner /// Encapsulates monitoring and logging for SMAPI. private readonly Monitor Monitor; - /// Manages SMAPI events for mods. - private readonly EventManager Events; - /// Simplifies access to private game code. private readonly Reflector Reflection; @@ -46,6 +42,9 @@ internal class SGameRunner : GameRunner /// Raised before the game exits. private readonly Action OnGameExiting; + /// Raised after an instance finishes a draw loop. + private readonly Action OnPlayerInstanceRendered; + /********* ** Public methods @@ -60,15 +59,15 @@ internal class SGameRunner : GameRunner /// Construct an instance. /// Encapsulates monitoring and logging for SMAPI. /// Simplifies access to private game code. - /// Manages SMAPI events for mods. /// Handles mod hooks provided by the game. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised after the game finishes loading its initial content. /// Raised when XNA is updating its state (roughly 60 times per second). /// Raised when the game instance for a local split-screen player is updating (once per per player). + /// Raised after an instance finishes a draw loop. /// Raised before the game exits. - public SGameRunner(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting) + public SGameRunner(Monitor monitor, Reflector reflection, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting, Action onPlayerInstanceRendered) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -78,13 +77,13 @@ public SGameRunner(Monitor monitor, Reflector reflection, EventManager eventMana // init SMAPI this.Monitor = monitor; - this.Events = eventManager; this.Reflection = reflection; this.Multiplayer = multiplayer; this.ExitGameImmediately = exitGameImmediately; this.OnGameContentLoaded = onGameContentLoaded; this.OnGameUpdating = onGameUpdating; this.OnPlayerInstanceUpdating = onPlayerInstanceUpdating; + this.OnPlayerInstanceRendered = onPlayerInstanceRendered; this.OnGameExiting = onGameExiting; } @@ -94,7 +93,19 @@ public SGameRunner(Monitor monitor, Reflector reflection, EventManager eventMana public override Game1 CreateGameInstance(PlayerIndex playerIndex = PlayerIndex.One, int instanceIndex = 0) { SInputState inputState = new(); - return new SGame(playerIndex, instanceIndex, this.Monitor, this.Reflection, this.Events, inputState, this.ModHooks, this.Multiplayer, this.ExitGameImmediately, this.OnPlayerInstanceUpdating, this.OnGameContentLoaded); + return new SGame( + playerIndex: playerIndex, + instanceIndex: instanceIndex, + monitor: this.Monitor, + reflection: this.Reflection, + input: inputState, + modHooks: this.ModHooks, + multiplayer: this.Multiplayer, + exitGameImmediately: this.ExitGameImmediately, + onUpdating: this.OnPlayerInstanceUpdating, + onContentLoaded: this.OnGameContentLoaded, + onRendered: this.OnPlayerInstanceRendered + ); } /// diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs index 8ec3e506f..fe1b1c630 100644 --- a/src/SMAPI/Framework/SModHooks.cs +++ b/src/SMAPI/Framework/SModHooks.cs @@ -1,12 +1,19 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Enums; +using StardewModdingAPI.Internal; using StardewModdingAPI.Utilities; using StardewValley; +using StardewValley.Menus; +using StardewValley.Mods; namespace StardewModdingAPI.Framework { /// Invokes callbacks for mod hooks provided by the game. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Inherited from the game code.")] internal class SModHooks : DelegatingModHooks { /********* @@ -21,6 +28,12 @@ internal class SModHooks : DelegatingModHooks /// A callback to invoke when the load stage changes. private readonly Action OnStageChanged; + /// A callback to invoke when the game starts a render step in the draw loop. + private readonly Action OnRenderingStep; + + /// A callback to invoke when the game finishes a render step in the draw loop. + private readonly Action OnRenderedStep; + /********* ** Public methods @@ -29,13 +42,17 @@ internal class SModHooks : DelegatingModHooks /// The underlying hooks to call by default. /// A callback to invoke before runs. /// A callback to invoke when the load stage changes. + /// A callback to invoke when the game starts a render step in the draw loop. + /// A callback to invoke when the game finishes a render step in the draw loop. /// Writes messages to the console. - public SModHooks(ModHooks parent, Action beforeNewDayAfterFade, Action onStageChanged, IMonitor monitor) + public SModHooks(ModHooks parent, Action beforeNewDayAfterFade, Action onStageChanged, Action onRenderingStep, Action onRenderedStep, IMonitor monitor) : base(parent) { + this.Monitor = monitor; this.BeforeNewDayAfterFade = beforeNewDayAfterFade; this.OnStageChanged = onStageChanged; - this.Monitor = monitor; + this.OnRenderingStep = onRenderingStep; + this.OnRenderedStep = onRenderedStep; } /// @@ -63,16 +80,46 @@ public override Task StartTask(Task task, string id) return task; } - /// A hook invoked when creating a new save slot, after the game has added the location instances but before it fully initializes them. + /// public override void CreatedInitialLocations() { this.OnStageChanged(LoadStage.CreatedInitialLocations); } - /// A hook invoked when loading a save slot, after the game has added the location instances but before it restores their save data. Not applicable when connecting to a multiplayer host. + /// public override void SaveAddedLocations() { this.OnStageChanged(LoadStage.SaveAddedLocations); } + + /// + public override bool OnRendering(RenderSteps step, SpriteBatch sb, GameTime time, RenderTarget2D target_screen) + { + this.OnRenderingStep(step, sb, target_screen); + + return true; + } + + /// + public override void OnRendered(RenderSteps step, SpriteBatch sb, GameTime time, RenderTarget2D target_screen) + { + this.OnRenderedStep(step, sb, target_screen); + } + + /// + public override bool TryDrawMenu(IClickableMenu menu, Action draw_menu_action) + { + try + { + draw_menu_action(); + return true; + } + catch (Exception ex) + { + this.Monitor.Log($"The {menu.GetMenuChainLabel()} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + return false; + } + } } } diff --git a/src/SMAPI/Utilities/DelegatingModHooks.cs b/src/SMAPI/Utilities/DelegatingModHooks.cs index 3ebcf997a..c13a3b61e 100644 --- a/src/SMAPI/Utilities/DelegatingModHooks.cs +++ b/src/SMAPI/Utilities/DelegatingModHooks.cs @@ -1,15 +1,21 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewValley; using StardewValley.Events; +using StardewValley.Menus; +using StardewValley.Mods; namespace StardewModdingAPI.Utilities { /// An implementation of which automatically calls the parent instance for any method that's not overridden. /// The mod hooks are primarily meant for SMAPI to use. Using this directly in mods is a last resort, since it's very easy to break SMAPI this way. This class requires that SMAPI is present in the parent chain. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Inherited from the game code.")] public class DelegatingModHooks : ModHooks { /********* @@ -94,6 +100,56 @@ public override FarmEvent OnUtility_PickFarmEvent(Func action) return this.Parent.OnUtility_PickFarmEvent(action); } + /// Raised after the player crosses a mutex barrier in the new-day initialization before saving. + /// The barrier ID set in the new-day code. + public override void AfterNewDayBarrier(string barrier_id) + { + this.Parent.AfterNewDayBarrier(barrier_id); + } + + /// Raised when creating a new save slot, after the game has added the location instances but before it fully initializes them. + public override void CreatedInitialLocations() + { + this.Parent.CreatedInitialLocations(); + } + + /// Raised when loading a save slot, after the game has added the location instances but before it restores their save data. Not applicable when connecting to a multiplayer host. + public override void SaveAddedLocations() + { + this.Parent.SaveAddedLocations(); + } + + /// Raised before the game renders content to the screen in the draw loop. + /// The render step being started. + /// The sprite batch being drawn (which might not always be open yet). + /// A snapshot of the game timing state. + /// The render target, if any. + /// Returns whether to continue with the render step. + public override bool OnRendering(RenderSteps step, SpriteBatch sb, GameTime time, RenderTarget2D target_screen) + { + return this.Parent.OnRendering(step, sb, time, target_screen); + } + + /// Raised after the game renders content to the screen in the draw loop. + /// The render step being started. + /// The sprite batch being drawn (which might not always be open yet). + /// A snapshot of the game timing state. + /// The render target, if any. + /// Returns whether to continue with the render step. + public override void OnRendered(RenderSteps step, SpriteBatch sb, GameTime time, RenderTarget2D target_screen) + { + this.Parent.OnRendered(step, sb, time, target_screen); + } + + /// Draw a menu (or child menu) if possible. + /// The menu to draw. + /// The action which draws the menu. + /// Returns whether the menu was successfully drawn. + public override bool TryDrawMenu(IClickableMenu menu, Action draw_menu_action) + { + return this.Parent.TryDrawMenu(menu, draw_menu_action); + } + /// Start an asynchronous task for the game. /// The task to start. /// A unique key which identifies the task. From ce0074c53dea6051d3f85af109a29271290cf78f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 May 2022 21:43:06 -0400 Subject: [PATCH 29/84] update for world state change in SDV 1.6 --- .../Framework/Commands/Other/RegenerateBundles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs index 159d7c4af..e239ed3cf 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs @@ -63,7 +63,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg } // get private fields - IWorldState state = Game1.netWorldState.Value; + NetWorldState state = Game1.netWorldState.Value; var bundleData = state.GetType().GetField("_bundleData", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)?.GetValue(state) as IDictionary ?? throw new InvalidOperationException("Can't access '_bundleData' field on world state."); var netBundleData = state.GetType().GetField("netBundleData", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)?.GetValue(state) as NetStringDictionary From 14e48c74238078c8ad05172b84c71d5aee02e83e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Aug 2022 00:49:52 -0400 Subject: [PATCH 30/84] update for accessibility changes in SDV 1.6 --- src/SMAPI/Framework/SCore.cs | 6 +++--- src/SMAPI/Framework/SGame.cs | 2 +- src/SMAPI/Framework/SMultiplayer.cs | 33 ++++++----------------------- src/SMAPI/Patches/Game1Patcher.cs | 10 ++------- 4 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f443aa215..8d503fdb8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -255,7 +255,7 @@ public void RunInteractively() LocalizedContentManager.OnLanguageChange += _ => this.OnLocaleChanged(); // override game - this.Multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); + this.Multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.OnModMessageReceived, this.Settings.LogNetworkTraffic); SGame.CreateContentManagerImpl = this.CreateContentManager; // must be static since the game accesses it before the SGame constructor is called this.Game = new SGameRunner( monitor: this.Monitor, @@ -282,7 +282,7 @@ public void RunInteractively() // apply game patches MiniMonoModHotfix.Apply(); HarmonyPatcher.Apply("SMAPI", this.Monitor, - new Game1Patcher(this.Reflection, this.OnLoadStageChanged), + new Game1Patcher(this.OnLoadStageChanged), new TitleMenuPatcher(this.OnLoadStageChanged) ); @@ -662,7 +662,7 @@ private void OnPlayerInstanceUpdating(SGame instance, GameTime gameTime, Action if (Game1.currentLoader != null) { this.Monitor.Log("Game loader synchronizing..."); - this.Reflection.GetMethod(Game1.game1, "UpdateTitleScreen").Invoke(Game1.currentGameTime); // run game logic to change music on load, etc + Game1.game1.UpdateTitleScreen(Game1.currentGameTime); // run game logic to change music on load, etc // ReSharper disable once ConstantConditionalAccessQualifier -- may become null within the loop while (Game1.currentLoader?.MoveNext() == true) { diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 822935c6d..711a34f5a 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -143,7 +143,7 @@ protected override void LoadContent() /// Construct a content manager to read game content files. /// The service provider to use to locate services. /// The root directory to search for content. - protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) + protected internal override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { if (SGame.CreateContentManagerImpl == null) throw new InvalidOperationException($"The {nameof(SGame)}.{nameof(SGame.CreateContentManagerImpl)} must be set."); diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 441a50ef8..7300f4c9e 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -2,13 +2,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Galaxy.Api; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Utilities; @@ -44,9 +42,6 @@ internal class SMultiplayer : Multiplayer /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; - /// Simplifies access to private code. - private readonly Reflector Reflection; - /// Manages SMAPI events. private readonly EventManager EventManager; @@ -85,16 +80,14 @@ public MultiplayerPeer? HostPeer /// Manages SMAPI events. /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. - /// Simplifies access to private code. /// A callback to invoke when a mod message is received. /// Whether to log network traffic. - public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action onModMessageReceived, bool logNetworkTraffic) + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Action onModMessageReceived, bool logNetworkTraffic) { this.Monitor = monitor; this.EventManager = eventManager; this.JsonHelper = jsonHelper; this.ModRegistry = modRegistry; - this.Reflection = reflection; this.OnModMessageReceived = onModMessageReceived; this.LogNetworkTraffic = logNetworkTraffic; } @@ -112,17 +105,11 @@ public override Client InitClient(Client client) { switch (client) { - case LidgrenClient: - { - string address = this.Reflection.GetField(client, "address").GetValue() ?? throw new InvalidOperationException("Can't initialize base networking client: no valid address found."); - return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); - } + case LidgrenClient lidgrenClient: + return new SLidgrenClient(lidgrenClient.address, this.OnClientProcessingMessage, this.OnClientSendingMessage); - case GalaxyNetClient: - { - GalaxyID address = this.Reflection.GetField(client, "lobbyId").GetValue() ?? throw new InvalidOperationException("Can't initialize GOG networking client: no valid address found."); - return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); - } + case GalaxyNetClient galaxyClient: + return new SGalaxyNetClient(galaxyClient.lobbyId, this.OnClientProcessingMessage, this.OnClientSendingMessage); default: this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}"); @@ -137,16 +124,10 @@ public override Server InitServer(Server server) switch (server) { case LidgrenServer: - { - IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue() ?? throw new InvalidOperationException("Can't initialize base networking client: the required 'gameServer' field wasn't found."); - return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage); - } + return new SLidgrenServer(server.gameServer, this, this.OnServerProcessingMessage); case GalaxyNetServer: - { - IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue() ?? throw new InvalidOperationException("Can't initialize GOG networking client: the required 'gameServer' field wasn't found."); - return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage); - } + return new SGalaxyNetServer(server.gameServer, this, this.OnServerProcessingMessage); default: this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}"); diff --git a/src/SMAPI/Patches/Game1Patcher.cs b/src/SMAPI/Patches/Game1Patcher.cs index 195e00f6f..fab5f9924 100644 --- a/src/SMAPI/Patches/Game1Patcher.cs +++ b/src/SMAPI/Patches/Game1Patcher.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using HarmonyLib; using StardewModdingAPI.Enums; -using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Internal.Patching; using StardewValley; using StardewValley.Menus; @@ -19,9 +18,6 @@ internal class Game1Patcher : BasePatcher /********* ** Fields *********/ - /// Simplifies access to private code. - private static Reflector Reflection = null!; // initialized in constructor - /// A callback to invoke when the load stage changes. private static Action OnStageChanged = null!; // initialized in constructor @@ -30,11 +26,9 @@ internal class Game1Patcher : BasePatcher ** Public methods *********/ /// Construct an instance. - /// Simplifies access to private code. /// A callback to invoke when the load stage changes. - public Game1Patcher(Reflector reflection, Action onStageChanged) + public Game1Patcher(Action onStageChanged) { - Game1Patcher.Reflection = reflection; Game1Patcher.OnStageChanged = onStageChanged; } @@ -80,7 +74,7 @@ private static bool IsCreating() { return (Game1.currentMinigame is Intro) // creating save with intro - || (Game1.activeClickableMenu is TitleMenu menu && Game1Patcher.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue()); // creating save, skipped intro + || (Game1.activeClickableMenu is TitleMenu menu && menu.transitioningCharacterCreationMenu); // creating save, skipped intro } } } From 091526d3c8a84659f1c515538251c933a8e20f0a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Aug 2022 00:36:37 -0400 Subject: [PATCH 31/84] update for new game logger in SDV 1.6 --- docs/release-notes.md | 1 + .../Logging/InterceptingTextWriter.cs | 98 ------------- src/SMAPI/Framework/Logging/LogManager.cs | 132 ++---------------- src/SMAPI/Framework/Monitor.cs | 9 +- src/SMAPI/Framework/SCore.cs | 1 + src/SMAPI/Framework/SGame.cs | 5 +- src/SMAPI/Framework/SGameLogger.cs | 78 +++++++++++ src/SMAPI/Framework/SGameRunner.cs | 9 +- src/SMAPI/Metadata/InstructionMetadata.cs | 6 +- 9 files changed, 105 insertions(+), 234 deletions(-) delete mode 100644 src/SMAPI/Framework/Logging/InterceptingTextWriter.cs create mode 100644 src/SMAPI/Framework/SGameLogger.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 68df5af68..1c7e19ac1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * For mod authors: * Updated to .NET 6. * Removed all deprecated APIs. + * SMAPI no longer intercepts output written to the console. Mods which directly access `Console` will be listed under mod warnings. ## Upcoming release * For players: diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs deleted file mode 100644 index 9ecc16269..000000000 --- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace StardewModdingAPI.Framework.Logging -{ - /// A text writer which allows intercepting output. - internal class InterceptingTextWriter : TextWriter - { - /********* - ** Fields - *********/ - /// The event raised when a message is written to the console directly. - private readonly Action OnMessageIntercepted; - - - /********* - ** Accessors - *********/ - /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) - public const char IgnoreChar = '\u200B'; - - /// The underlying console output. - public TextWriter Out { get; } - - /// - public override Encoding Encoding => this.Out.Encoding; - - /// Whether the text writer should ignore the next input if it's a newline. - /// This is used when log output is suppressed from the console, since Console.WriteLine writes the trailing newline as a separate call. - public bool IgnoreNextIfNewline { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying output writer. - /// The event raised when a message is written to the console directly. - public InterceptingTextWriter(TextWriter output, Action onMessageIntercepted) - { - this.Out = output; - this.OnMessageIntercepted = onMessageIntercepted; - } - - /// - public override void Write(char[] buffer, int index, int count) - { - // track newline skip - bool ignoreIfNewline = this.IgnoreNextIfNewline; - this.IgnoreNextIfNewline = false; - - // get first character if valid - if (count == 0 || index < 0 || index >= buffer.Length) - { - this.Out.Write(buffer, index, count); - return; - } - char firstChar = buffer[index]; - - // handle output - if (firstChar == InterceptingTextWriter.IgnoreChar) - this.Out.Write(buffer, index + 1, count - 1); - else if (char.IsControl(firstChar) && firstChar is not ('\r' or '\n')) - this.Out.Write(buffer, index, count); - else if (this.IsEmptyOrNewline(buffer)) - { - if (!ignoreIfNewline) - this.Out.Write(buffer, index, count); - } - else - this.OnMessageIntercepted(new string(buffer, index, count)); - } - - /// - public override void Write(char ch) - { - this.Out.Write(ch); - } - - - /********* - ** Private methods - *********/ - /// Get whether a buffer represents a line break. - /// The buffer to check. - private bool IsEmptyOrNewline(char[] buffer) - { - foreach (char ch in buffer) - { - if (ch != '\n' && ch != '\r') - return false; - } - - return true; - } - } -} diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index d5b332899..b9c32a597 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading; using StardewModdingAPI.Framework.Commands; using StardewModdingAPI.Framework.Models; @@ -25,48 +24,9 @@ internal class LogManager : IDisposable /// The log file to which to write messages. private readonly LogFileManager LogFile; - /// The text writer which intercepts console output. - private readonly InterceptingTextWriter ConsoleInterceptor; - - /// Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) - private const char IgnoreChar = InterceptingTextWriter.IgnoreChar; - /// Create a monitor instance given the ID and name. private readonly Func GetMonitorImpl; - /// Regex patterns which match console non-error messages to suppress from the console and log. - private readonly Regex[] SuppressConsolePatterns = - { - new(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new(@"^DebugOutput:\s+(?:added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), - new(@"^Ignoring keys: ", RegexOptions.Compiled | RegexOptions.CultureInvariant) - }; - - /// Regex patterns which match console messages to show a more friendly error for. - private readonly ReplaceLogPattern[] ReplaceConsolePatterns = - { - // Steam not loaded - new( - search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - replacement: -#if SMAPI_FOR_WINDOWS - "Oops! Steam achievements won't work because Steam isn't loaded. See 'Configure your game client' in the install guide for more info: https://smapi.io/install.", -#else - "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", -#endif - logLevel: LogLevel.Error - ), - - // save file not found error - new( - search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), - replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.", - logLevel: LogLevel.Error - ) - }; - /********* ** Accessors @@ -97,7 +57,7 @@ public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToCon this.LogFile = new LogFileManager(logPath); // init monitor - this.GetMonitorImpl = (id, name) => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog) + this.GetMonitorImpl = (id, name) => new Monitor(name, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, @@ -106,15 +66,6 @@ public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToCon this.Monitor = this.GetMonitor("SMAPI", "SMAPI"); this.MonitorForGame = this.GetMonitor("game", "game"); - // redirect direct console output - this.ConsoleInterceptor = new InterceptingTextWriter( - output: Console.Out, - onMessageIntercepted: writeToConsole - ? message => this.HandleConsoleMessage(this.MonitorForGame, message) - : _ => { } - ); - Console.SetOut(this.ConsoleInterceptor); - // enable Unicode handling on Windows // (the terminal defaults to UTF-8 on Linux/macOS) #if SMAPI_FOR_WINDOWS @@ -360,42 +311,6 @@ public void Dispose() /********* ** Protected methods *********/ - /// Redirect messages logged directly to the console to the given monitor. - /// The monitor with which to log messages as the game. - /// The message to log. - private void HandleConsoleMessage(IMonitor gameMonitor, string message) - { - // detect exception - LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; - - // ignore suppressed message - if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) - { - this.ConsoleInterceptor.IgnoreNextIfNewline = true; - return; - } - - // show friendly error if applicable - foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns) - { - string newMessage = entry.Search.Replace(message, entry.Replacement); - if (message != newMessage) - { - gameMonitor.Log(newMessage, entry.LogLevel); - gameMonitor.Log(message); - return; - } - } - - // simplify exception messages - if (level == LogLevel.Error) - message = ExceptionHelper.SimplifyExtensionMessage(message); - - // forward to monitor - gameMonitor.Log(message, level); - this.ConsoleInterceptor.IgnoreNextIfNewline = true; - } - /// Write a summary of mod warnings to the console and log. /// The loaded mods. /// The mods which could not be loaded. @@ -478,12 +393,18 @@ private void LogModWarnings(IEnumerable mods, IModMetadata[] skipp "corruption. If your game has issues, try removing these first." ); + // direct console access + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Trace, "Direct console access", + "These mods access the SMAPI console window directly. This is more fragile, and their output may not", + "be logged by SMAPI." + ); + // paranoid warnings if (logParanoidWarnings) { this.LogModWarningGroup( modsWithWarnings, - match: mod => mod.HasWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), + match: mod => mod.HasWarnings(ModWarning.AccessesFilesystem, ModWarning.AccessesShell), level: LogLevel.Debug, heading: "Direct system access", blurb: new[] @@ -495,8 +416,6 @@ private void LogModWarnings(IEnumerable mods, IModMetadata[] skipp modLabel: mod => { List labels = new List(); - if (mod.HasWarnings(ModWarning.AccessesConsole)) - labels.Add("console"); if (mod.HasWarnings(ModWarning.AccessesFilesystem)) labels.Add("files"); if (mod.HasWarnings(ModWarning.AccessesShell)) @@ -643,40 +562,5 @@ private void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLeve { this.LogModWarningGroup(mods, mod => mod.HasWarnings(warning), level, heading, blurb); } - - - /********* - ** Protected types - *********/ - /// A console log pattern to replace with a different message. - private class ReplaceLogPattern - { - /********* - ** Accessors - *********/ - /// The regex pattern matching the portion of the message to replace. - public Regex Search { get; } - - /// The replacement string. - public string Replacement { get; } - - /// The log level for the new message. - public LogLevel LogLevel { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The regex pattern matching the portion of the message to replace. - /// The replacement string. - /// The log level for the new message. - public ReplaceLogPattern(Regex search, string replacement, LogLevel logLevel) - { - this.Search = search; - this.Replacement = replacement; - this.LogLevel = logLevel; - } - } } } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 4ed2c9bbb..aaaafcbc9 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -18,9 +18,6 @@ internal class Monitor : IMonitor /// Handles writing text to the console. private readonly IConsoleWriter ConsoleWriter; - /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) - private readonly char IgnoreChar; - /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -58,12 +55,11 @@ internal class Monitor : IMonitor *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. - /// A character which indicates the message should not be intercepted if it appears as the first character of a string written to the console. The character itself is not logged in that case. /// The log file to which to write messages. /// The colors to use for text written to the SMAPI console. /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. /// Get the screen ID that should be logged to distinguish between players in split-screen mode, if any. - public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose, Func getScreenIdForLog) + public Monitor(string source, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose, Func getScreenIdForLog) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -73,7 +69,6 @@ public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSche this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig); - this.IgnoreChar = ignoreChar; this.IsVerbose = isVerbose; this.GetScreenIdForLog = getScreenIdForLog; } @@ -139,7 +134,7 @@ private void LogImpl(string source, string message, ConsoleLogLevel level) // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) - this.ConsoleWriter.WriteLine(this.IgnoreChar + consoleMessage, level); + this.ConsoleWriter.WriteLine(consoleMessage, level); // write to log file this.LogFile.WriteLine(fullMessage); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 8d503fdb8..652a753a4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -268,6 +268,7 @@ public void RunInteractively() onRenderedStep: this.OnRenderedStep, monitor: this.Monitor ), + gameLogger: new SGameLogger(this.GetMonitorForGame()), multiplayer: this.Multiplayer, exitGameImmediately: this.ExitGameImmediately, diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 711a34f5a..a6f2ab319 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Internal; using StardewModdingAPI.Utilities; using StardewValley; +using StardewValley.Logging; namespace StardewModdingAPI.Framework { @@ -92,12 +93,13 @@ internal class SGame : Game1 /// Simplifies access to private game code. /// Manages the game's input state. /// Handles mod hooks provided by the game. + /// The game log output handler. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised when the instance is updating its state (roughly 60 times per second). /// Raised after the game finishes loading its initial content. /// Raised after the instance finishes a draw loop. - public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflector reflection, SInputState input, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onUpdating, Action onContentLoaded, Action onRendered) + public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflector reflection, SInputState input, SModHooks modHooks, IGameLogger gameLogger, SMultiplayer multiplayer, Action exitGameImmediately, Action onUpdating, Action onContentLoaded, Action onRendered) : base(playerIndex, instanceIndex) { // init XNA @@ -105,6 +107,7 @@ public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflec // hook into game Game1.input = this.InitialInput = input; + Game1.log = gameLogger; Game1.multiplayer = this.InitialMultiplayer = multiplayer; Game1.hooks = modHooks; this._locations = new ObservableCollection(); diff --git a/src/SMAPI/Framework/SGameLogger.cs b/src/SMAPI/Framework/SGameLogger.cs new file mode 100644 index 000000000..1b33fdb80 --- /dev/null +++ b/src/SMAPI/Framework/SGameLogger.cs @@ -0,0 +1,78 @@ +using System; +using StardewModdingAPI.Internal; +using StardewValley.Logging; + +namespace StardewModdingAPI.Framework +{ + /// Redirects log output from the game code to a SMAPI monitor. + internal class SGameLogger : IGameLogger + { + /********* + ** Fields + *********/ + /// The monitor to which to log output. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The monitor to which to log output. + public SGameLogger(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// + public void Verbose(string message) + { + this.Monitor.Log(message); + } + + /// + public void Debug(string message) + { + this.Monitor.Log(message, LogLevel.Debug); + } + + /// + public void Info(string message) + { + this.Monitor.Log(message, LogLevel.Info); + } + + /// + public void Warn(string message) + { + this.Monitor.Log(message, LogLevel.Warn); + } + + /// + public void Error(string error, Exception? exception = null) + { + // steam not loaded + if (error == "Error connecting to Steam." && exception?.Message == "Steamworks is not initialized.") + { + this.Monitor.Log( +#if SMAPI_FOR_WINDOWS + "Oops! Steam achievements won't work because Steam isn't loaded. See 'Configure your game client' in the install guide for more info: https://smapi.io/install.", +#else + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", +#endif + LogLevel.Error + ); + } + + // any other error + else + { + string message = exception != null + ? $"{error}\n{exception.GetLogSummary()}" + : error; + + this.Monitor.Log(message, LogLevel.Error); + } + } + } +} diff --git a/src/SMAPI/Framework/SGameRunner.cs b/src/SMAPI/Framework/SGameRunner.cs index 9344ceafa..d9c79088d 100644 --- a/src/SMAPI/Framework/SGameRunner.cs +++ b/src/SMAPI/Framework/SGameRunner.cs @@ -6,6 +6,7 @@ using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewValley; +using StardewValley.Logging; namespace StardewModdingAPI.Framework { @@ -27,6 +28,9 @@ internal class SGameRunner : GameRunner /// The core SMAPI mod hooks. private readonly SModHooks ModHooks; + /// The game log output handler. + private readonly IGameLogger GameLogger; + /// The core multiplayer logic. private readonly SMultiplayer Multiplayer; @@ -60,6 +64,7 @@ internal class SGameRunner : GameRunner /// Encapsulates monitoring and logging for SMAPI. /// Simplifies access to private game code. /// Handles mod hooks provided by the game. + /// The game log output handler. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised after the game finishes loading its initial content. @@ -67,13 +72,14 @@ internal class SGameRunner : GameRunner /// Raised when the game instance for a local split-screen player is updating (once per per player). /// Raised after an instance finishes a draw loop. /// Raised before the game exits. - public SGameRunner(Monitor monitor, Reflector reflection, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting, Action onPlayerInstanceRendered) + public SGameRunner(Monitor monitor, Reflector reflection, SModHooks modHooks, IGameLogger gameLogger, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting, Action onPlayerInstanceRendered) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // hook into game this.ModHooks = modHooks; + this.GameLogger = gameLogger; // init SMAPI this.Monitor = monitor; @@ -100,6 +106,7 @@ public override Game1 CreateGameInstance(PlayerIndex playerIndex = PlayerIndex.O reflection: this.Reflection, input: inputState, modHooks: this.ModHooks, + gameLogger: this.GameLogger, multiplayer: this.Multiplayer, exitGameImmediately: this.ExitGameImmediately, onUpdating: this.OnPlayerInstanceUpdating, diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index e5b99e0b0..73ac80db2 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -68,12 +68,12 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr yield return new FieldFinder(typeof(SaveGame).FullName!, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer); yield return new EventFinder(typeof(ISpecializedEvents).FullName!, new[] { nameof(ISpecializedEvents.UnvalidatedUpdateTicked), nameof(ISpecializedEvents.UnvalidatedUpdateTicking) }, InstructionHandleResult.DetectedUnvalidatedUpdateTick); + // direct console access + yield return new TypeFinder(typeof(System.Console).FullName!, InstructionHandleResult.DetectedConsoleAccess); + // paranoid issues if (paranoidMode) { - // direct console access - yield return new TypeFinder(typeof(System.Console).FullName!, InstructionHandleResult.DetectedConsoleAccess); - // filesystem access yield return new TypeFinder( new[] From 6a552711fa6bacf580c72e553c21cfada6b2f818 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 28 Sep 2022 23:20:46 -0400 Subject: [PATCH 32/84] update for new Inventory class in SDV 1.6 --- .../Framework/StateTracking/ChestTracker.cs | 4 +- ...{NetListWatcher.cs => InventoryWatcher.cs} | 56 +++++++++---------- .../FieldWatchers/WatcherFactory.cs | 20 +++---- 3 files changed, 40 insertions(+), 40 deletions(-) rename src/SMAPI/Framework/StateTracking/FieldWatchers/{NetListWatcher.cs => InventoryWatcher.cs} (61%) diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs index 2796ad546..eed050260 100644 --- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs +++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs @@ -44,9 +44,9 @@ internal class ChestTracker : IDisposable public ChestTracker(string name, Chest chest) { this.Chest = chest; - this.InventoryWatcher = WatcherFactory.ForNetList($"{name}.{nameof(chest.items)}", chest.items); + this.InventoryWatcher = WatcherFactory.ForInventory($"{name}.{nameof(chest.Items)}", chest.Items); - this.StackSizes = this.Chest.items + this.StackSizes = this.Chest.Items .Where(n => n != null) .Distinct() .ToDictionary(n => n, n => n.Stack); diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs similarity index 61% rename from src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs rename to src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs index 5b6a3e1f4..ecb0d2c34 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs @@ -1,25 +1,24 @@ using System.Collections.Generic; -using Netcode; using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewValley; +using StardewValley.Inventories; namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { - /// A watcher which detects changes to a net list field. - /// The list value type. - internal class NetListWatcher : BaseDisposableWatcher, ICollectionWatcher - where TValue : class, INetObject + /// A watcher which detects changes to an item inventory. + internal class InventoryWatcher : BaseDisposableWatcher, ICollectionWatcher { /********* ** Fields *********/ - /// The field being watched. - private readonly NetList> Field; + /// The inventory being watched. + private readonly Inventory Inventory; /// The pairs added since the last reset. - private readonly ISet AddedImpl = new HashSet(new ObjectReferenceComparer()); + private readonly ISet AddedImpl = new HashSet(new ObjectReferenceComparer()); /// The pairs removed since the last reset. - private readonly ISet RemovedImpl = new HashSet(new ObjectReferenceComparer()); + private readonly ISet RemovedImpl = new HashSet(new ObjectReferenceComparer()); /********* @@ -32,10 +31,10 @@ internal class NetListWatcher : BaseDisposableWatcher, ICollectionWatche public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; /// - public IEnumerable Added => this.AddedImpl; + public IEnumerable Added => this.AddedImpl; /// - public IEnumerable Removed => this.RemovedImpl; + public IEnumerable Removed => this.RemovedImpl; /********* @@ -43,13 +42,14 @@ internal class NetListWatcher : BaseDisposableWatcher, ICollectionWatche *********/ /// Construct an instance. /// A name which identifies what the watcher is watching, used for troubleshooting. - /// The field to watch. - public NetListWatcher(string name, NetList> field) + /// The inventory to watch. + public InventoryWatcher(string name, Inventory inventory) { this.Name = name; - this.Field = field; - field.OnElementChanged += this.OnElementChanged; - field.OnArrayReplaced += this.OnArrayReplaced; + this.Inventory = inventory; + + inventory.OnSlotChanged += this.OnSlotChanged; + inventory.OnInventoryReplaced += this.OnInventoryReplaced; } /// @@ -70,8 +70,8 @@ public override void Dispose() { if (!this.IsDisposed) { - this.Field.OnElementChanged -= this.OnElementChanged; - this.Field.OnArrayReplaced -= this.OnArrayReplaced; + this.Inventory.OnSlotChanged -= this.OnSlotChanged; + this.Inventory.OnInventoryReplaced -= this.OnInventoryReplaced; } base.Dispose(); @@ -82,20 +82,20 @@ public override void Dispose() ** Private methods *********/ /// A callback invoked when the value list is replaced. - /// The net field whose values changed. + /// The net field whose values changed. /// The previous list of values. /// The new list of values. - private void OnArrayReplaced(NetList> list, IList oldValues, IList newValues) + private void OnInventoryReplaced(Inventory inventory, IList oldValues, IList newValues) { - ISet oldSet = new HashSet(oldValues, new ObjectReferenceComparer()); - ISet changed = new HashSet(newValues, new ObjectReferenceComparer()); + ISet oldSet = new HashSet(oldValues, new ObjectReferenceComparer()); + ISet changed = new HashSet(newValues, new ObjectReferenceComparer()); - foreach (TValue value in oldSet) + foreach (Item value in oldSet) { if (!changed.Contains(value)) this.Remove(value); } - foreach (TValue value in changed) + foreach (Item value in changed) { if (!oldSet.Contains(value)) this.Add(value); @@ -103,11 +103,11 @@ private void OnArrayReplaced(NetList> list, IList } /// A callback invoked when an entry is replaced. - /// The net field whose values changed. + /// The inventory whose values changed. /// The list index which changed. /// The previous value. /// The new value. - private void OnElementChanged(NetList> list, int index, TValue? oldValue, TValue? newValue) + private void OnSlotChanged(Inventory inventory, int index, Item? oldValue, Item? newValue) { this.Remove(oldValue); this.Add(newValue); @@ -115,7 +115,7 @@ private void OnElementChanged(NetList> list, int index, T /// Track an added item. /// The value that was added. - private void Add(TValue? value) + private void Add(Item? value) { if (value == null) return; @@ -131,7 +131,7 @@ private void Add(TValue? value) /// Track a removed item. /// The value that was removed. - private void Remove(TValue? value) + private void Remove(Item? value) { if (value == null) return; diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs index c31be1fcc..1f4728470 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -3,6 +3,8 @@ using System.Collections.ObjectModel; using Netcode; using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewValley; +using StardewValley.Inventories; namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { @@ -82,24 +84,22 @@ public static ICollectionWatcher ForImmutableCollection() return ImmutableCollectionWatcher.Instance; } - /// Get a watcher for a net collection. - /// The value type. + /// Get a watcher for an item inventory. /// A name which identifies what the watcher is watching, used for troubleshooting. - /// The net collection. - public static ICollectionWatcher ForNetCollection(string name, NetCollection collection) - where T : class, INetObject + /// The item inventory. + public static ICollectionWatcher ForInventory(string name, Inventory inventory) { - return new NetCollectionWatcher(name, collection); + return new InventoryWatcher(name, inventory); } - /// Get a watcher for a net list. + /// Get a watcher for a net collection. /// The value type. /// A name which identifies what the watcher is watching, used for troubleshooting. - /// The net list. - public static ICollectionWatcher ForNetList(string name, NetList> collection) + /// The net collection. + public static ICollectionWatcher ForNetCollection(string name, NetCollection collection) where T : class, INetObject { - return new NetListWatcher(name, collection); + return new NetCollectionWatcher(name, collection); } /// Get a watcher for a net dictionary. From 57226353f662cd796f6cb9644453a80348651161 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Mar 2023 12:09:08 -0400 Subject: [PATCH 33/84] update for season enum in SDV 1.6 --- src/SMAPI.Tests/SMAPI.Tests.csproj | 1 + src/SMAPI.Tests/Utilities/SDateTests.cs | 7 ++- src/SMAPI/Utilities/SDate.cs | 76 ++++++++++++++++--------- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 6c4e930b0..2e56ad59f 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index b9c3d202a..e2ee6238b 100644 --- a/src/SMAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -60,12 +60,17 @@ private static class Dates [Test(Description = "Assert that the constructor sets the expected values for all valid dates.")] public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.SampleSeasonValues))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year) { + // arrange + Season expectedSeason = Enum.Parse(season, ignoreCase: true); + // act SDate date = new(day, season, year); // assert Assert.AreEqual(day, date.Day); - Assert.AreEqual(season.Trim().ToLowerInvariant(), date.Season); + Assert.AreEqual(expectedSeason, date.Season); + Assert.AreEqual((int)expectedSeason, date.SeasonIndex); + Assert.AreEqual(Utility.getSeasonKey(expectedSeason), date.SeasonKey); Assert.AreEqual(year, date.Year); } diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 06ee8b911..fc0d7505d 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Newtonsoft.Json; using StardewModdingAPI.Framework; using StardewValley; @@ -32,8 +31,11 @@ public class SDate : IEquatable /// The day of month. public int Day { get; } + /// The season. + public Season Season { get; } + /// The season name. - public string Season { get; } + public string SeasonKey { get; } /// The index of the season (where 0 is spring, 1 is summer, 2 is fall, and 3 is winter). /// This is used in some game calculations (e.g. seasonal game sprites) and methods (e.g. ). @@ -62,6 +64,13 @@ public class SDate : IEquatable public SDate(int day, string season) : this(day, season, Game1.year) { } + /// Construct an instance. + /// The day of month. + /// The season name. + /// One of the arguments has an invalid value (like day 35). + public SDate(int day, Season season) + : this(day, season, Game1.year) { } + /// Construct an instance. /// The day of month. /// The season name. @@ -71,6 +80,15 @@ public SDate(int day, string season) public SDate(int day, string season, int year) : this(day, season, year, allowDayZero: false) { } + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// One of the arguments has an invalid value (like day 35). + [JsonConstructor] + public SDate(int day, Season season, int year) + : this(day, season, year, allowDayZero: false) { } + /// Get the current in-game date. public static SDate Now() { @@ -140,7 +158,7 @@ public WorldDate ToWorldDate() /// Get an untranslated string representation of the date. This is mainly intended for debugging or console messages. public override string ToString() { - return $"{this.Day:00} {this.Season} Y{this.Year}"; + return $"{this.Day:00} {this.SeasonKey} Y{this.Year}"; } /// Get a translated string representation of the date in the current game locale. @@ -246,20 +264,16 @@ public override int GetHashCode() *********/ /// Construct an instance. /// The day of month. - /// The season name. + /// The season. /// The year. /// Whether to allow 0 spring Y1 as a valid date. /// One of the arguments has an invalid value (like day 35). [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The nullability is validated in this constructor.")] - private SDate(int day, string season, int year, bool allowDayZero) + private SDate(int day, Season season, int year, bool allowDayZero) { - season = season?.Trim().ToLowerInvariant()!; // null-checked below - // validate - if (season == null) - throw new ArgumentNullException(nameof(season)); - if (!this.Seasons.Contains(season)) - throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); + if (!Enum.IsDefined(typeof(Season), season)) + throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", Enum.GetNames(typeof(Season)))}]."); if (day < 0 || day > this.DaysInSeason) throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); if (day == 0 && !(allowDayZero && this.IsDayZero(day, season, year))) @@ -270,19 +284,38 @@ private SDate(int day, string season, int year, bool allowDayZero) // initialize this.Day = day; this.Season = season; - this.SeasonIndex = this.GetSeasonIndex(season); + this.SeasonKey = Utility.getSeasonKey(season); + this.SeasonIndex = (int)season; this.Year = year; this.DayOfWeek = this.GetDayOfWeek(day); this.DaysSinceStart = this.GetDaysSinceStart(day, season, year); } + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// Whether to allow 0 spring Y1 as a valid date. + /// One of the arguments has an invalid value (like day 35). + [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The nullability is validated in this constructor.")] + private SDate(int day, string season, int year, bool allowDayZero) + : this( + day, + Utility.TryParseEnum(season, out Season parsedSeason) + ? parsedSeason + : throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", Enum.GetNames(typeof(Season)))}]."), + year, + allowDayZero + ) + { } + /// Get whether a date represents 0 spring Y1, which is the date during the in-game intro. /// The day of month. /// The normalized season name. /// The year. - private bool IsDayZero(int day, string season, int year) + private bool IsDayZero(int day, Season season, int year) { - return day == 0 && season == "spring" && year == 1; + return day == 0 && season == Season.Spring && year == 1; } /// Get the day of week for a given date. @@ -306,25 +339,14 @@ private DayOfWeek GetDayOfWeek(int day) /// The day of month. /// The season name. /// The year. - private int GetDaysSinceStart(int day, string season, int year) + private int GetDaysSinceStart(int day, Season season, int year) { // return the number of days since 01 spring Y1 (inclusively) int yearIndex = year - 1; return yearIndex * this.DaysInSeason * this.SeasonsInYear - + this.GetSeasonIndex(season) * this.DaysInSeason + + (int)season * this.DaysInSeason + day; } - - /// Get a season index. - /// The season name. - /// The current season wasn't recognized. - private int GetSeasonIndex(string season) - { - int index = Array.IndexOf(this.Seasons, season); - if (index == -1) - throw new InvalidOperationException($"The season '{season}' wasn't recognized."); - return index; - } } } From 835dd325d8e482274b1eb6ff71efb72a5eba9001 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2023 12:09:20 -0400 Subject: [PATCH 34/84] update for debug command and ArgUtility parsing in SDV 1.6 --- .../Framework/Commands/ArgumentParser.cs | 24 ++++----- .../Framework/Commands/Other/DebugCommand.cs | 14 +++--- src/SMAPI/Framework/CommandManager.cs | 50 +------------------ 3 files changed, 20 insertions(+), 68 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 66f2f1052..e42aea212 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -15,9 +15,6 @@ internal class ArgumentParser : IReadOnlyList /// The command name for errors. private readonly string CommandName; - /// The arguments to parse. - private readonly string[] Args; - /// Writes messages to the console and log file. private readonly IMonitor Monitor; @@ -25,12 +22,15 @@ internal class ArgumentParser : IReadOnlyList /********* ** Accessors *********/ + /// The arguments to parse. + public string[] Values { get; } + /// Get the number of arguments. - public int Count => this.Args.Length; + public int Count => this.Values.Length; /// Get the argument at the specified index in the list. /// The zero-based index of the element to get. - public string this[int index] => this.Args[index]; + public string this[int index] => this.Values[index]; /********* @@ -38,12 +38,12 @@ internal class ArgumentParser : IReadOnlyList *********/ /// Construct an instance. /// The command name for errors. - /// The arguments to parse. + /// The arguments to parse. /// Writes messages to the console and log file. - public ArgumentParser(string commandName, string[] args, IMonitor monitor) + public ArgumentParser(string commandName, string[] values, IMonitor monitor) { this.CommandName = commandName; - this.Args = args; + this.Values = values; this.Monitor = monitor; } @@ -58,20 +58,20 @@ public bool TryGet(int index, string name, [NotNullWhen(true)] out string? value value = null; // validate - if (this.Args.Length < index + 1) + if (this.Values.Length < index + 1) { if (required) this.LogError($"Argument {index} ({name}) is required."); return false; } - if (oneOf?.Any() == true && !oneOf.Contains(this.Args[index], StringComparer.OrdinalIgnoreCase)) + if (oneOf?.Any() == true && !oneOf.Contains(this.Values[index], StringComparer.OrdinalIgnoreCase)) { this.LogError($"Argument {index} ({name}) must be one of {string.Join(", ", oneOf)}."); return false; } // get value - value = this.Args[index]; + value = this.Values[index]; return true; } @@ -111,7 +111,7 @@ public bool TryGetInt(int index, string name, out int value, bool required = tru /// An enumerator that can be used to iterate through the collection. public IEnumerator GetEnumerator() { - return ((IEnumerable)this.Args).GetEnumerator(); + return ((IEnumerable)this.Values).GetEnumerator(); } /// Returns an enumerator that iterates through a collection. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs index cf1dcbcec..2099b0284 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs @@ -20,15 +20,13 @@ public DebugCommand() /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { - // submit command - string debugCommand = string.Join(" ", args); string oldOutput = Game1.debugOutput; - Game1.game1.parseDebugInput(debugCommand); - - // show result - monitor.Log(Game1.debugOutput != oldOutput - ? $"> {Game1.debugOutput}" - : "Sent debug command to the game, but there was no output.", LogLevel.Info); + if (DebugCommands.TryHandle(args.Values)) // if it returns false, the game will log an error itself + { + monitor.Log(Game1.debugOutput != oldOutput + ? $"> {Game1.debugOutput}" + : "Sent debug command to the game, but there was no output.", LogLevel.Info); + } } } } diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index d3b9c8ee8..b20e5ceb8 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using StardewModdingAPI.Framework.Commands; +using StardewValley; namespace StardewModdingAPI.Framework { @@ -109,7 +109,7 @@ public bool TryParse(string? input, [NotNullWhen(true)] out string? name, [NotNu } // parse input - args = this.ParseArgs(input); + args = ArgUtility.SplitBySpaceQuoteAware(input); name = this.GetNormalizedName(args[0])!; args = args.Skip(1).ToArray(); @@ -138,56 +138,10 @@ public bool TryParse(string? input, [NotNullWhen(true)] out string? name, [NotNu return this.Commands.TryGetValue(name, out command); } - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. - public bool Trigger(string? name, string[] arguments) - { - // get normalized name - name = this.GetNormalizedName(name)!; - if (string.IsNullOrWhiteSpace(name)) - return false; - - // get command - if (this.Commands.TryGetValue(name, out Command? command)) - { - command.Callback.Invoke(name, arguments); - return true; - } - - return false; - } - /********* ** Private methods *********/ - /// Parse a string into command arguments. - /// The string to parse. - private string[] ParseArgs(string input) - { - bool inQuotes = false; - IList args = new List(); - StringBuilder currentArg = new(); - foreach (char ch in input) - { - if (ch == '"') - inQuotes = !inQuotes; - else if (!inQuotes && char.IsWhiteSpace(ch)) - { - args.Add(currentArg.ToString()); - currentArg.Clear(); - } - else - currentArg.Append(ch); - } - - args.Add(currentArg.ToString()); - - return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); - } - /// Try to parse a 'screen=X' command argument, which specifies the screen that should receive the command. /// The raw argument to parse. /// The parsed screen ID, if any. From 1b4cc0b342cb1afff43207f86ec50fd443ae970d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Dec 2023 13:46:10 -0500 Subject: [PATCH 35/84] update for hookable client/server in SDV 1.6 --- .../Framework/Networking/SGalaxyNetClient.cs | 52 ------------- .../Framework/Networking/SGalaxyNetServer.cs | 74 ------------------- .../Framework/Networking/SLidgrenClient.cs | 50 ------------- .../Framework/Networking/SLidgrenServer.cs | 68 ----------------- src/SMAPI/Framework/SMultiplayer.cs | 36 ++++----- 5 files changed, 15 insertions(+), 265 deletions(-) delete mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetClient.cs delete mode 100644 src/SMAPI/Framework/Networking/SGalaxyNetServer.cs delete mode 100644 src/SMAPI/Framework/Networking/SLidgrenClient.cs delete mode 100644 src/SMAPI/Framework/Networking/SLidgrenServer.cs diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs deleted file mode 100644 index 01095c66b..000000000 --- a/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Galaxy.Api; -using StardewValley.Network; -using StardewValley.SDKs; - -namespace StardewModdingAPI.Framework.Networking -{ - /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. - internal class SGalaxyNetClient : GalaxyNetClient - { - /********* - ** Fields - *********/ - /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. - private readonly Action, Action> OnProcessingMessage; - - /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. - private readonly Action, Action> OnSendingMessage; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The remote address being connected. - /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. - /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. - public SGalaxyNetClient(GalaxyID address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) - : base(address) - { - this.OnProcessingMessage = onProcessingMessage; - this.OnSendingMessage = onSendingMessage; - } - - /// Send a message to the connected peer. - public override void sendMessage(OutgoingMessage message) - { - this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); - } - - - /********* - ** Protected methods - *********/ - /// Process an incoming network message. - /// The message to process. - protected override void processIncomingMessage(IncomingMessage message) - { - this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); - } - } -} diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs deleted file mode 100644 index 71e11576d..000000000 --- a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using Galaxy.Api; -using StardewValley.Network; -using StardewValley.SDKs; - -namespace StardewModdingAPI.Framework.Networking -{ - /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. - internal class SGalaxyNetServer : GalaxyNetServer - { - /********* - ** Fields - *********/ - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - private readonly Action, Action> OnProcessingMessage; - - /// SMAPI's implementation of the game's core multiplayer logic. - private readonly SMultiplayer Multiplayer; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying game server. - /// SMAPI's implementation of the game's core multiplayer logic. - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - public SGalaxyNetServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) - : base(gameServer) - { - this.Multiplayer = multiplayer; - this.OnProcessingMessage = onProcessingMessage; - } - - - /********* - ** Protected methods - *********/ - /// Read and process a message from the client. - /// The Galaxy peer ID. - /// The data to process. - /// This reimplements , but adds a callback to . - [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] - protected override void onReceiveMessage(GalaxyID peer, Stream messageStream) - { - using IncomingMessage message = new(); - using BinaryReader reader = new(messageStream); - - message.Read(reader); - ulong peerID = peer.ToUint64(); // note: GalaxyID instances get reused, so need to store the underlying ID instead - this.OnProcessingMessage(message, outgoing => this.SendMessageToPeerID(peerID, outgoing), () => - { - if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peerID) - this.gameServer.processIncomingMessage(message); - else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); - GalaxyID capturedPeer = new(peerID); - this.gameServer.checkFarmhandRequest(Convert.ToString(peerID), this.getConnectionId(peer), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); - } - }); - } - - /// Send a message to a remote peer. - /// The unique Galaxy ID, derived from . - /// The message to send. - private void SendMessageToPeerID(ulong peerID, OutgoingMessage message) - { - this.sendMessage(new GalaxyID(peerID), message); - } - } -} diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs deleted file mode 100644 index 398767448..000000000 --- a/src/SMAPI/Framework/Networking/SLidgrenClient.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using StardewValley.Network; - -namespace StardewModdingAPI.Framework.Networking -{ - /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. - internal class SLidgrenClient : LidgrenClient - { - /********* - ** Fields - *********/ - /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. - private readonly Action, Action> OnProcessingMessage; - - /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. - private readonly Action, Action> OnSendingMessage; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The remote address being connected. - /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. - /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. - public SLidgrenClient(string address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) - : base(address) - { - this.OnProcessingMessage = onProcessingMessage; - this.OnSendingMessage = onSendingMessage; - } - - /// Send a message to the connected peer. - public override void sendMessage(OutgoingMessage message) - { - this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); - } - - - /********* - ** Protected methods - *********/ - /// Process an incoming network message. - /// The message to process. - protected override void processIncomingMessage(IncomingMessage message) - { - this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); - } - } -} diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs deleted file mode 100644 index ff871e641..000000000 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using Lidgren.Network; -using StardewValley.Network; - -namespace StardewModdingAPI.Framework.Networking -{ - /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. - internal class SLidgrenServer : LidgrenServer - { - /********* - ** Fields - *********/ - /// SMAPI's implementation of the game's core multiplayer logic. - private readonly SMultiplayer Multiplayer; - - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - private readonly Action, Action> OnProcessingMessage; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's implementation of the game's core multiplayer logic. - /// The underlying game server. - /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. - public SLidgrenServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) - : base(gameServer) - { - this.Multiplayer = multiplayer; - this.OnProcessingMessage = onProcessingMessage; - } - - - /********* - ** Protected methods - *********/ - /// Parse a data message from a client. - /// The raw network message to parse. - [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] - protected override void parseDataMessageFromClient(NetIncomingMessage rawMessage) - { - // add hook to call multiplayer core - NetConnection peer = rawMessage.SenderConnection; - using IncomingMessage message = new(); - using Stream readStream = new NetBufferReadStream(rawMessage); - using BinaryReader reader = new(readStream); - - while (rawMessage.LengthBits - rawMessage.Position >= 8) - { - message.Read(reader); - NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused - this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () => - { - if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) - this.gameServer.processIncomingMessage(message); - else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); - this.gameServer.checkFarmhandRequest("", this.getConnectionId(rawMessage.SenderConnection), farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer); - } - }); - } - } - } -} diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 7300f4c9e..46477142d 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -12,7 +12,6 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Network; -using StardewValley.SDKs; namespace StardewModdingAPI.Framework { @@ -103,36 +102,31 @@ public void CleanupOnMultiplayerExit() /// The client to initialize. public override Client InitClient(Client client) { - switch (client) - { - case LidgrenClient lidgrenClient: - return new SLidgrenClient(lidgrenClient.address, this.OnClientProcessingMessage, this.OnClientSendingMessage); - - case GalaxyNetClient galaxyClient: - return new SGalaxyNetClient(galaxyClient.lobbyId, this.OnClientProcessingMessage, this.OnClientSendingMessage); + client = base.InitClient(client); - default: - this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}"); - return client; + if (client is IHookableClient hookClient) + { + hookClient.OnProcessingMessage = this.OnClientProcessingMessage; + hookClient.OnSendingMessage = this.OnClientSendingMessage; } + else + this.Monitor.Log($"Multiplayer client type '{client.GetType().AssemblyQualifiedName}' doesn't implement {nameof(IHookableClient)}, so SMAPI is unable to hook into it. This may cause mod issues in multiplayer."); + + return client; } /// Initialize a server before the game connects to an incoming player. /// The server to initialize. public override Server InitServer(Server server) { - switch (server) - { - case LidgrenServer: - return new SLidgrenServer(server.gameServer, this, this.OnServerProcessingMessage); + server = base.InitServer(server); - case GalaxyNetServer: - return new SGalaxyNetServer(server.gameServer, this, this.OnServerProcessingMessage); + if (server is IHookableServer hookServer) + hookServer.OnProcessingMessage = this.OnServerProcessingMessage; + else + this.Monitor.Log($"Multiplayer server type '{server.GetType().AssemblyQualifiedName}' doesn't implement {nameof(IHookableServer)}, so SMAPI is unable to hook into it. This may cause mod issues in multiplayer."); - default: - this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}"); - return server; - } + return server; } /// A callback raised when sending a message as a farmhand. From 634e4fdc441ac1bb634077baf8095d0bd4a0fd6e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2023 13:27:03 -0500 Subject: [PATCH 36/84] update for DataLoader in SDV 1.6 --- .../Commands/Player/SetFarmTypeCommand.cs | 2 +- .../Framework/ItemRepository.cs | 19 ++++--- src/SMAPI/Framework/ContentCoordinator.cs | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 50 +++++++------------ 4 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs index b2035d429..1483a577f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs @@ -210,7 +210,7 @@ private IDictionary GetCustomFarmTypes() { IDictionary farmTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (ModFarmType farmType in Game1.content.Load>("Data\\AdditionalFarms")) + foreach (ModFarmType farmType in DataLoader.AdditionalFarms(Game1.content)) { if (string.IsNullOrWhiteSpace(farmType.ID)) continue; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index d8ebe4dac..aa7185249 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -134,7 +134,7 @@ select item // get secret note IDs var ids = this - .TryLoad("Data\\SecretNotes") + .TryLoad(() => DataLoader.SecretNotes(Game1.content)) .Keys .Where(isJournalScrap ? id => (id >= GameLocation.JOURNAL_INDEX) @@ -235,7 +235,7 @@ private void GetRoeContextTagLookups(out HashSet simpleTags, out List
  • (); complexTags = new List>(); - foreach (FishPondData data in Game1.content.Load>("Data\\FishPondData")) + foreach (FishPondData data in this.TryLoad(() => DataLoader.FishPondData(Game1.content))) { if (data.ProducedItems.All(p => p.ItemId is not ("812" or "(O)812"))) continue; // doesn't produce roe @@ -247,21 +247,20 @@ private void GetRoeContextTagLookups(out HashSet simpleTags, out List
  • Try to load a data file, and return empty data if it's invalid. - /// The asset key type. - /// The asset value type. - /// The data asset name. - private Dictionary TryLoad(string assetName) - where TKey : notnull + /// Try to load a data asset, and return empty data if it's invalid. + /// The asset type. + /// A callback which loads the asset. + private TAsset TryLoad(Func load) + where TAsset : new() { try { - return Game1.content.Load>(assetName); + return load(); } catch (ContentLoadException) { // generally due to a player incorrectly replacing a data file with an XNB mod - return new Dictionary(); + return new TAsset(); } } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 6db79f0c7..c091c9ebc 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -221,7 +221,7 @@ public string GetLocale() public void OnAdditionalLanguagesInitialized() { // update locale cache for custom languages, and load it now (since languages added later won't work) - var customLanguages = this.MainContentManager.Load>("Data/AdditionalLanguages"); + var customLanguages = DataLoader.AdditionalLanguages(this.MainContentManager); this.LocaleCodes = new Lazy>(() => this.GetLocaleCodes(customLanguages)); _ = this.LocaleCodes.Value; } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 6fdb456fb..d4799caf8 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -11,20 +11,6 @@ using StardewModdingAPI.Internal; using StardewValley; using StardewValley.Buildings; -using StardewValley.GameData.BigCraftables; -using StardewValley.GameData.Buildings; -using StardewValley.GameData.Characters; -using StardewValley.GameData.Crops; -using StardewValley.GameData.FarmAnimals; -using StardewValley.GameData.FloorsAndPaths; -using StardewValley.GameData.FruitTrees; -using StardewValley.GameData.LocationContexts; -using StardewValley.GameData.Objects; -using StardewValley.GameData.Pants; -using StardewValley.GameData.Pets; -using StardewValley.GameData.Shirts; -using StardewValley.GameData.Tools; -using StardewValley.GameData.Weapons; using StardewValley.Locations; using StardewValley.Pathfinding; using StardewValley.TerrainFeatures; @@ -281,7 +267,7 @@ static ISet GetWarpSet(GameLocation location) ** Content\Data ****/ case "data/achievements": // Game1.LoadContent - Game1.achievements = content.Load>(key); + Game1.achievements = DataLoader.Achievements(content); return true; case "data/audiochanges": @@ -289,7 +275,7 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/bigcraftables": // Game1.LoadContent - Game1.bigCraftableData = content.Load>(key); + Game1.bigCraftableData = DataLoader.BigCraftables(content); ItemRegistry.ResetCache(); return true; @@ -298,7 +284,7 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/buildings": // Game1.LoadContent - Game1.buildingData = content.Load>(key); + Game1.buildingData = DataLoader.Buildings(content); if (!ignoreWorld) { Utility.ForEachBuilding(building => @@ -310,7 +296,7 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/characters": // Game1.LoadContent - Game1.characterData = content.Load>(key); + Game1.characterData = DataLoader.Characters(content); if (!ignoreWorld) this.UpdateCharacterData(); return true; @@ -324,25 +310,25 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/cookingrecipes": // CraftingRecipe.InitShared - CraftingRecipe.cookingRecipes = content.Load>(key); + CraftingRecipe.cookingRecipes = DataLoader.CookingRecipes(content); return true; case "data/craftingrecipes": // CraftingRecipe.InitShared - CraftingRecipe.craftingRecipes = content.Load>(key); + CraftingRecipe.craftingRecipes = DataLoader.CraftingRecipes(content); return true; case "data/crops": // Game1.LoadContent - Game1.cropData = content.Load>(key); + Game1.cropData = DataLoader.Crops(content); return true; case "data/farmanimals": // FarmAnimal constructor - Game1.farmAnimalData = content.Load>(key); + Game1.farmAnimalData = DataLoader.FarmAnimals(content); if (!ignoreWorld) this.UpdateFarmAnimalData(); return true; case "data/floorsandpaths": // Game1.LoadContent - Game1.floorPathData = content.Load>("Data\\FloorsAndPaths"); + Game1.floorPathData = DataLoader.FloorsAndPaths(content); return true; case "data/furniture": // FurnitureDataDefinition @@ -350,7 +336,7 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/fruittrees": // Game1.LoadContent - Game1.fruitTreeData = content.Load>("Data\\FruitTrees"); + Game1.fruitTreeData = DataLoader.FruitTrees(content); return true; case "data/hairdata": // Farmer.GetHairStyleMetadataFile @@ -361,7 +347,7 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/locationcontexts": // Game1.LoadContent - Game1.locationContextData = content.Load>("Data\\LocationContexts"); + Game1.locationContextData = DataLoader.LocationContexts(content); return true; case "data/movies": // MovieTheater.GetMovieData @@ -370,36 +356,36 @@ static ISet GetWarpSet(GameLocation location) return true; case "data/npcgifttastes": // Game1.LoadContent - Game1.NPCGiftTastes = content.Load>(key); + Game1.NPCGiftTastes = DataLoader.NpcGiftTastes(content); return true; case "data/objects": // Game1.LoadContent - Game1.objectData = content.Load>(key); + Game1.objectData = DataLoader.Objects(content); ItemRegistry.ResetCache(); return true; case "data/pants": // Game1.LoadContent - Game1.pantsData = content.Load>(key); + Game1.pantsData = DataLoader.Pants(content); ItemRegistry.ResetCache(); return true; case "data/pets": // Game1.LoadContent - Game1.petData = content.Load>(key); + Game1.petData = DataLoader.Pets(content); ItemRegistry.ResetCache(); return true; case "data/shirts": // Game1.LoadContent - Game1.shirtData = content.Load>(key); + Game1.shirtData = DataLoader.Shirts(content); ItemRegistry.ResetCache(); return true; case "data/tools": // Game1.LoadContent - Game1.toolData = content.Load>(key); + Game1.toolData = DataLoader.Tools(content); ItemRegistry.ResetCache(); return true; case "data/weapons": // Game1.LoadContent - Game1.weaponData = Game1.content.Load>(@"Data\Weapons"); + Game1.weaponData = DataLoader.Weapons(content); ItemRegistry.ResetCache(); return true; From ad06f6edbc9404bfd6008a5cade4f814919bfe62 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 7 Feb 2022 21:05:17 -0500 Subject: [PATCH 37/84] update for other changes in SDV 1.6 Specific changes: - The game now sets the invariant culture. - Added Utility.getAllVillagers(). - Replaced Character.getTileLocation() with a Tile property. - Simplified Bush.isDestroyable() method. - Pluralized fish pond data namespace. --- .../Framework/Commands/World/ClearCommand.cs | 2 +- .../Framework/Commands/World/HurryAllCommand.cs | 5 +---- src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs | 3 +-- src/SMAPI/Framework/Input/SInputState.cs | 2 +- src/SMAPI/Program.cs | 2 ++ 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs index 9dd2c8bd4..9a1f114b0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs @@ -142,7 +142,7 @@ obj.Name is "Weeds" or "Stone" this.RemoveFurniture(location, _ => true) + this.RemoveObjects(location, _ => true) + this.RemoveTerrainFeatures(location, _ => true) - + this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable(location, p.currentTileLocation)) + + this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable()) + this.RemoveResourceClumps(location, _ => true); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs index 095317204..fe4270a5a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs @@ -34,11 +34,8 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg } // hurry all NPCs - foreach (NPC npc in Utility.getAllCharacters()) + foreach (NPC npc in Utility.getAllVillagers()) // can't use Utility.ForEachVillager since they may warp mid-iteration { - if (!npc.isVillager()) - continue; - monitor.Log($"Hurrying {npc.Name}..."); try { diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index aa7185249..ab915d0fb 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -4,8 +4,7 @@ using System.Linq; using Microsoft.Xna.Framework.Content; using StardewValley; -using StardewValley.Buildings; -using StardewValley.GameData.FishPond; +using StardewValley.GameData.FishPonds; using StardewValley.ItemTypeDefinitions; using StardewValley.Objects; using SObject = StardewValley.Object; diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index fef83af7f..0844616d8 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -73,7 +73,7 @@ public void TrueUpdate() var keyboard = new KeyboardStateBuilder(base.GetKeyboardState()); var mouse = new MouseStateBuilder(base.GetMouseState()); Vector2 cursorAbsolutePos = new((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); - Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : null; + Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.Tile : null; HashSet reallyDown = new HashSet(this.GetPressedButtons(keyboard, mouse, controller)); // apply overrides diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index a6861bca0..766fd2d9e 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -30,6 +31,7 @@ internal class Program /// The command-line arguments. public static void Main(string[] args) { + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; // per StardewValley.Program.Main Console.Title = $"SMAPI {EarlyConstants.RawApiVersion}"; try From c6fae2c9678ae3017bea2459376d7f3a5caf2f3b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 Mar 2022 22:59:23 -0500 Subject: [PATCH 38/84] update mod compatibility list --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 6c8aa81e3..0f90abd3c 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -159,6 +159,15 @@ "~ | StatusReasonPhrase": "split-screen mode was added in Stardew Valley 1.5" }, + /********* + ** Broke in SDV 1.6 + *********/ + "Skip Intro": { + "ID": "Pathoschild.SkipIntro", + "~1.9.9-alpha.20220227 | Status": "AssumeBroken", + "~1.9.9-alpha.20220227 | StatusReasonDetails": "causes crash during game launch" + }, + /********* ** Broke in SMAPI 3.14.0 *********/ @@ -196,11 +205,6 @@ "~2.0.1 | Status": "AssumeBroken", "~2.0.1 | StatusReasonDetails": "requires 'Microsoft.Xna.Framework.Audio.AudioCategory' which doesn't exist in MonoGame" }, - "Skip Intro": { - "ID": "Pathoschild.SkipIntro", - "~1.9.1 | Status": "AssumeBroken", - "~1.9.1 | StatusReasonDetails": "causes freeze during game launch" - }, "Stardew Hack": { "ID": "bcmpinc.StardewHack", "~5.1.0 | Status": "AssumeBroken", From a439743589b8dcc8db486f07a04745130f24042d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Mar 2023 13:21:55 -0500 Subject: [PATCH 39/84] handle textures whose assets no longer exist during asset propagation Textures are reloaded in-place in Stardew Valley 1.6. That's much more efficient than the previous approach, but SMAPI can't take the game context into account anymore during asset propagation. That means textures which SMAPI would previously skip are now reloaded and may fail. For example, custom NPCs still exist in-memory when you return to title (since the game state isn't cleared yet), but often the mods which provide their portraits/sprites have unloaded their patches (e.g. because they depend on in-save Content Patcher tokens). This commit mitigates that by logging a small warning when it happens, instead of the giant exception trace it logs otherwise. --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index d4799caf8..4a830e54a 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -153,9 +153,18 @@ private bool PropagateTexture(IAssetName assetName, LocalizedContentManager.Lang { if (contentManager.IsLoaded(assetName)) { - changed = true; - Texture2D texture = contentManager.LoadLocalized(assetName, language, useCache: true); - texture.CopyFromTexture(newTexture.Value); + if (this.DisposableContentManager.DoesAssetExist(assetName)) + { + changed = true; + + Texture2D texture = contentManager.LoadLocalized(assetName, language, useCache: true); + texture.CopyFromTexture(newTexture.Value); + } + else + { + this.Monitor.Log($"Skipped reload for '{assetName.Name}' because the underlying asset no longer exists.", LogLevel.Warn); + break; + } } } From 451e6f7f7033796dee64bccbe20bba6cb2fc2df4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 21 Mar 2023 20:37:35 -0400 Subject: [PATCH 40/84] optimize vanilla asset load when the asset doesn't exist --- src/SMAPI/Framework/ContentCoordinator.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index c091c9ebc..4757b2fb4 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -575,14 +575,19 @@ private bool TryLoadVanillaAsset(string assetName, [NotNullWhen(true)] out T? { try { - asset = this.VanillaContentManager.Load(assetName); - return true; + if (this.VanillaContentManager.DoesAssetExist(assetName)) + { + asset = this.VanillaContentManager.Load(assetName); + return true; + } } catch { - asset = default; - return false; + // handled below } + + asset = default; + return false; } /// Get the language enums (like ) indexed by locale code (like ja-JP). From 576b12e309c0d145ac3646129946b13700c99335 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jul 2023 12:27:08 -0400 Subject: [PATCH 41/84] add render-step events --- docs/release-notes.md | 1 + src/SMAPI/Events/IDisplayEvents.cs | 6 +++ src/SMAPI/Events/RenderedStepEventArgs.cs | 49 +++++++++++++++++++ src/SMAPI/Events/RenderingStepEventArgs.cs | 49 +++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 8 +++ .../Framework/Events/ModDisplayEvents.cs | 14 ++++++ src/SMAPI/Framework/SCore.cs | 22 ++++++++- 7 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Events/RenderedStepEventArgs.cs create mode 100644 src/SMAPI/Events/RenderingStepEventArgs.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 1c7e19ac1..af573a2b9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * For mod authors: * Updated to .NET 6. + * Added `RenderingStep` and `RenderedStep` events, which let you handle a specific step in the game's render cycle. * Removed all deprecated APIs. * SMAPI no longer intercepts output written to the console. Mods which directly access `Console` will be listed under mod warnings. diff --git a/src/SMAPI/Events/IDisplayEvents.cs b/src/SMAPI/Events/IDisplayEvents.cs index dbf8d90f4..750094e0f 100644 --- a/src/SMAPI/Events/IDisplayEvents.cs +++ b/src/SMAPI/Events/IDisplayEvents.cs @@ -9,6 +9,12 @@ public interface IDisplayEvents /// Raised after a game menu is opened, closed, or replaced. event EventHandler MenuChanged; + /// Raised before the game draws a specific step in the rendering cycle. + event EventHandler RenderingStep; + + /// Raised after the game draws a specific step in the rendering cycle. + event EventHandler RenderedStep; + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. event EventHandler Rendering; diff --git a/src/SMAPI/Events/RenderedStepEventArgs.cs b/src/SMAPI/Events/RenderedStepEventArgs.cs new file mode 100644 index 000000000..2c90b4dee --- /dev/null +++ b/src/SMAPI/Events/RenderedStepEventArgs.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Mods; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedStepEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// The cached instance for each render step. + private static readonly Dictionary Instances = new(); + + + /********* + ** Accessors + *********/ + /// The current step in the render cycle. + public RenderSteps Step { get; } + + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current step in the render cycle. + public RenderedStepEventArgs(RenderSteps step) + { + this.Step = step; + } + + /// Get an instance for a render step. + /// The current step in the render cycle. + internal static RenderedStepEventArgs Instance(RenderSteps step) + { + if (!RenderedStepEventArgs.Instances.TryGetValue(step, out RenderedStepEventArgs instance)) + RenderedStepEventArgs.Instances[step] = instance = new(step); + + return instance; + } + } +} diff --git a/src/SMAPI/Events/RenderingStepEventArgs.cs b/src/SMAPI/Events/RenderingStepEventArgs.cs new file mode 100644 index 000000000..bedad203b --- /dev/null +++ b/src/SMAPI/Events/RenderingStepEventArgs.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Mods; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingStepEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// The cached instance for each render step. + private static readonly Dictionary Instances = new(); + + + /********* + ** Accessors + *********/ + /// The current step in the render cycle. + public RenderSteps Step { get; } + + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current step in the render cycle. + public RenderingStepEventArgs(RenderSteps step) + { + this.Step = step; + } + + /// Get an instance for a render step. + /// The current step in the render cycle. + internal static RenderingStepEventArgs Instance(RenderSteps step) + { + if (!RenderingStepEventArgs.Instances.TryGetValue(step, out RenderingStepEventArgs instance)) + RenderingStepEventArgs.Instances[step] = instance = new(step); + + return instance; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index b21d5c7df..41b914185 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -36,6 +36,12 @@ internal class EventManager /// public readonly ManagedEvent Rendered; + /// + public readonly ManagedEvent RenderingStep; + + /// + public readonly ManagedEvent RenderedStep; + /// public readonly ManagedEvent RenderingWorld; @@ -212,6 +218,8 @@ ManagedEvent ManageEventOf(string typeName, string event this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); + this.RenderingStep = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingStep)); + this.RenderedStep = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedStep)); this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); diff --git a/src/SMAPI/Framework/Events/ModDisplayEvents.cs b/src/SMAPI/Framework/Events/ModDisplayEvents.cs index 48f553242..145e23d1a 100644 --- a/src/SMAPI/Framework/Events/ModDisplayEvents.cs +++ b/src/SMAPI/Framework/Events/ModDisplayEvents.cs @@ -16,6 +16,20 @@ public event EventHandler MenuChanged remove => this.EventManager.MenuChanged.Remove(value); } + /// + public event EventHandler RenderingStep + { + add => this.EventManager.RenderingStep.Add(value, this.Mod); + remove => this.EventManager.RenderingStep.Remove(value); + } + + /// + public event EventHandler RenderedStep + { + add => this.EventManager.RenderedStep.Add(value, this.Mod); + remove => this.EventManager.RenderedStep.Remove(value); + } + /// public event EventHandler Rendering { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 652a753a4..014f119fd 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1221,6 +1221,10 @@ private void OnRenderingStep(RenderSteps step, SpriteBatch spriteBatch, RenderTa this.RaiseRenderEvent(events.RenderingHud, spriteBatch, renderTarget); break; } + + // raise generic rendering stage event + if (events.RenderingStep.HasListeners) + this.RaiseRenderEvent(events.RenderingStep, spriteBatch, renderTarget, RenderingStepEventArgs.Instance(step)); } /// Raised when the game finishes a render step in the draw loop. @@ -1245,6 +1249,10 @@ private void OnRenderedStep(RenderSteps step, SpriteBatch spriteBatch, RenderTar this.RaiseRenderEvent(events.RenderedHud, spriteBatch, renderTarget); break; } + + // raise generic rendering stage event + if (events.RenderedStep.HasListeners) + this.RaiseRenderEvent(events.RenderedStep, spriteBatch, renderTarget, RenderedStepEventArgs.Instance(step)); } /// Raised after an instance finishes a draw loop. @@ -1261,6 +1269,18 @@ private void OnRendered(RenderTarget2D renderTarget) /// The render target being drawn to the screen. private void RaiseRenderEvent(ManagedEvent @event, SpriteBatch spriteBatch, RenderTarget2D renderTarget) where TEventArgs : EventArgs, new() + { + this.RaiseRenderEvent(@event, spriteBatch, renderTarget, Singleton.Instance); + } + + /// Raise a rendering/rendered event, temporarily opening the given sprite batch if needed to let mods draw to it. + /// The event args type to construct. + /// The event to raise. + /// The sprite batch being drawn to the screen. + /// The render target being drawn to the screen. + /// The event arguments to pass to the event. + private void RaiseRenderEvent(ManagedEvent @event, SpriteBatch spriteBatch, RenderTarget2D renderTarget, TEventArgs eventArgs) + where TEventArgs : EventArgs { if (!@event.HasListeners) return; @@ -1286,7 +1306,7 @@ private void RaiseRenderEvent(ManagedEvent @event, Sprit Game1.SetRenderTarget(renderTarget); } - @event.RaiseEmpty(); + @event.Raise(eventArgs); } finally { From 9819e2aeb0b1276e09c1081667bf1d68fb553a61 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 9 Aug 2023 23:41:44 -0400 Subject: [PATCH 42/84] add rewriters for Stardew Valley 1.6 --- .../ModLoading/Rewriters/IRewriteFacade.cs | 5 + .../Rewriters/ReplaceReferencesRewriter.cs | 1 + .../StardewValley_1_5/AccessToolsFacade.cs | 2 +- .../HarmonyInstanceFacade.cs | 2 +- .../StardewValley_1_5/HarmonyMethodFacade.cs | 2 +- .../StardewValley_1_5/SpriteBatchFacade.cs | 2 +- .../StardewValley_1_6/BedFurnitureFacade.cs | 48 ++++ .../StardewValley_1_6/BootsFacade.cs | 31 +++ .../StardewValley_1_6/BreakableContainer.cs | 33 +++ .../Rewriters/StardewValley_1_6/BuffFacade.cs | 62 +++++ .../BuildableGameLocationFacade.cs | 91 ++++++++ .../StardewValley_1_6/BuildingFacade.cs | 33 +++ .../Rewriters/StardewValley_1_6/BushFacade.cs | 62 +++++ .../StardewValley_1_6/ButterflyFacade.cs | 34 +++ .../StardewValley_1_6/CarpenterMenuFacade.cs | 35 +++ .../Rewriters/StardewValley_1_6/CaskFacade.cs | 34 +++ .../StardewValley_1_6/CharacterFacade.cs | 63 +++++ .../StardewValley_1_6/ChestFacade.cs | 71 ++++++ .../StardewValley_1_6/ClothingFacade.cs | 32 +++ .../StardewValley_1_6/ColoredObjectFacade.cs | 32 +++ .../Rewriters/StardewValley_1_6/CropFacade.cs | 36 +++ .../StardewValley_1_6/DelayedActionFacade.cs | 33 +++ .../StardewValley_1_6/DialogueBoxFacade.cs | 34 +++ .../StardewValley_1_6/DialogueFacade.cs | 32 +++ .../StardewValley_1_6/EventFacade.cs | 33 +++ .../StardewValley_1_6/FarmAnimalFacade.cs | 34 +++ .../StardewValley_1_6/FarmerFacade.cs | 167 ++++++++++++++ .../StardewValley_1_6/FarmerTeamFacade.cs | 31 +++ .../StardewValley_1_6/FenceFacade.cs | 32 +++ .../StardewValley_1_6/ForestFacade.cs | 53 +++++ .../StardewValley_1_6/FruitTreeFacade.cs | 44 ++++ .../StardewValley_1_6/FurnitureFacade.cs | 69 ++++++ .../StardewValley_1_6/Game1Facade.cs | 109 +++++++++ .../StardewValley_1_6/GameLocationFacade.cs | 124 ++++++++++ .../Rewriters/StardewValley_1_6/HatFacade.cs | 31 +++ .../StardewValley_1_6/HoeDirtFacade.cs | 44 ++++ .../StardewValley_1_6/HudMessageFacade.cs | 68 ++++++ .../StardewValley_1_6/IClickableMenuFacade.cs | 97 ++++++++ .../Internal/InventoryToNetObjectList.cs | 110 +++++++++ .../Internal/NetRefWrapperCache.cs | 37 +++ .../Internal/ReadOnlyValueToNetString.cs | 34 +++ .../Rewriters/StardewValley_1_6/ItemFacade.cs | 33 +++ .../StardewValley_1_6/JunimoHutFacade.cs | 32 +++ .../LocalizedContentManagerFacade.cs | 32 +++ .../StardewValley_1_6/MeleeWeaponFacade.cs | 31 +++ .../StardewValley_1_6/NetFieldBaseFacade.cs | 32 +++ .../NetPausableFieldFacade.cs | 35 +++ .../Rewriters/StardewValley_1_6/NpcFacade.cs | 48 ++++ .../StardewValley_1_6/ObjectFacade.cs | 109 +++++++++ .../OverlaidDictionaryFacade.cs | 216 ++++++++++++++++++ .../PathFindControllerFacade.cs | 42 ++++ .../StardewValley_1_6/ResourceClumpFacade.cs | 30 +++ .../Rewriters/StardewValley_1_6/RingFacade.cs | 31 +++ .../StardewValley_1_6/ShopMenuFacade.cs | 62 +++++ .../StardewValley_1_6/SlingshotFacade.cs | 31 +++ .../StardewValley_1_6/SpriteTextFacade.cs | 82 +++++++ .../StorageFurnitureFacade.cs | 37 +++ .../StardewValley_1_6/TerrainFeatureFacade.cs | 50 ++++ .../Rewriters/StardewValley_1_6/TreeFacade.cs | 43 ++++ .../StardewValley_1_6/UtilityFacade.cs | 74 ++++++ .../ViewportExtensionsFacade.cs | 44 ++++ .../StardewValley_1_6/WorldDateFacade.cs | 32 +++ src/SMAPI/Metadata/InstructionMetadata.cs | 187 ++++++++++++++- 63 files changed, 3233 insertions(+), 7 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/IRewriteFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ButterflyFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ClothingFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ColoredObjectFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueBoxFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/EventFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HatFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/InventoryToNetObjectList.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/NetRefWrapperCache.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/ReadOnlyValueToNetString.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ItemFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetPausableFieldFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/OverlaidDictionaryFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/PathFindControllerFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SlingshotFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StorageFurnitureFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ViewportExtensionsFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/IRewriteFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/IRewriteFacade.cs new file mode 100644 index 000000000..0619e3c04 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/IRewriteFacade.cs @@ -0,0 +1,5 @@ +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Marker class for a rewrite facade used to validate mappings. See comments on for more info. + internal interface IRewriteFacade { } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index aeb2e1725..807b84cde 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -188,6 +188,7 @@ public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, str /// The facade type to which to point matching references. /// If the facade has a public constructor with no parameters, whether to rewrite references to empty constructors to use that one. (This is needed because .NET has no way to distinguish between an implicit and explicit constructor.) public ReplaceReferencesRewriter MapFacade(bool mapDefaultConstructor = false) + where TFacade : IRewriteFacade { return this.MapFacade(typeof(TFromType).FullName!, typeof(TFacade), mapDefaultConstructor); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/AccessToolsFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/AccessToolsFacade.cs index 76b4fdc7a..6e54e6136 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/AccessToolsFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/AccessToolsFacade.cs @@ -12,7 +12,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_5 /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See for more info. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class AccessToolsFacade + public class AccessToolsFacade : IRewriteFacade { /********* ** Public methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyInstanceFacade.cs index 7dfa5628a..36c69b4a1 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyInstanceFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyInstanceFacade.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_5 /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See for more info. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class HarmonyInstanceFacade : Harmony + public class HarmonyInstanceFacade : Harmony, IRewriteFacade { /********* ** Public methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyMethodFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyMethodFacade.cs index 3e125a0ec..47c198134 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyMethodFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/HarmonyMethodFacade.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_5 /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See for more info. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class HarmonyMethodFacade : HarmonyMethod + public class HarmonyMethodFacade : HarmonyMethod, IRewriteFacade { /********* ** Public methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/SpriteBatchFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/SpriteBatchFacade.cs index 0b36546c1..b0d5897aa 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/SpriteBatchFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_5/SpriteBatchFacade.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_5 /// Provides method signatures that can be injected into mod code for compatibility with mods written for XNA Framework before Stardew Valley 1.5.5. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class SpriteBatchFacade : SpriteBatch + public class SpriteBatchFacade : SpriteBatch, IRewriteFacade { /**** ** XNA signatures diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs new file mode 100644 index 000000000..2625c9b7f --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BedFurnitureFacade : BedFurniture, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static BedFurniture Constructor(int which, Vector2 tile, int initialRotations) + { + return new BedFurniture(which.ToString(), tile, initialRotations); + } + + public static BedFurniture Constructor(int which, Vector2 tile) + { + return new BedFurniture(which.ToString(), tile); + } + + public bool CanModifyBed(GameLocation location, Farmer who) + { + return this.CanModifyBed(who); + } + + public bool IsBeingSleptIn(GameLocation location) + { + return this.IsBeingSleptIn(); + } + + + /********* + ** Private methods + *********/ + private BedFurnitureFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs new file mode 100644 index 000000000..5c240ef18 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BootsFacade : Boots, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Boots Constructor(int which) + { + return new Boots(which.ToString()); + } + + + /********* + ** Private methods + *********/ + private BootsFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs new file mode 100644 index 000000000..b04c726f9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + public class BreakableContainerFacade : BreakableContainer, IRewriteFacade + { + /********* + ** Public methods + *********/ + public void releaseContents(GameLocation location, Farmer who) + { + this.releaseContents(who); + } + + + /********* + ** Private methods + *********/ + private BreakableContainerFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs new file mode 100644 index 000000000..8d98fb46a --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Buffs; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class BuffFacade : Buff, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Buff Constructor(string description, int millisecondsDuration, string source, int index) + { + return new Buff(index.ToString(), source, description: description, duration: millisecondsDuration, iconSheetIndex: index); + } + + public static Buff Constructor(int which) + { + return new Buff(which.ToString()); + } + + public static Buff Constructor(int farming, int fishing, int mining, int digging, int luck, int foraging, int crafting, int maxStamina, int magneticRadius, int speed, int defense, int attack, int minutesDuration, string source, string displaySource) + { + return new Buff( + null, + source, + displaySource, + duration: minutesDuration / Game1.realMilliSecondsPerGameMinute, + effects: new BuffEffects { FarmingLevel = { farming }, FishingLevel = { fishing }, MiningLevel = { mining }, LuckLevel = { luck }, ForagingLevel = { foraging }, MaxStamina = { maxStamina }, MagneticRadius = { magneticRadius }, Speed = { speed }, Defense = { defense }, Attack = { attack } } + ); + } + + public void addBuff() + { + Game1.player.buffs.Apply(this); + } + + public void removeBuff() + { + Game1.player.buffs.Remove(this.id); + } + + + /********* + ** Private methods + *********/ + private BuffFacade() + : base(null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs new file mode 100644 index 000000000..bc8cf1150 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Buildings; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's BuildableGameLocation methods to their newer form on to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BuildableGameLocationFacade : GameLocation, IRewriteFacade + { + /********* + ** Public methods + *********/ + public new bool buildStructure(Building b, Vector2 tileLocation, Farmer who, bool skipSafetyChecks = false) + { + return base.buildStructure(b, tileLocation, who, skipSafetyChecks); + } + + public new bool destroyStructure(Vector2 tile) + { + return base.destroyStructure(tile); + } + + public new bool destroyStructure(Building b) + { + return base.destroyStructure(b); + } + + public new Building getBuildingAt(Vector2 tile) + { + return base.getBuildingAt(tile); + } + + public new Building getBuildingByName(string name) + { + return base.getBuildingByName(name); + } + + public Building? getBuildingUnderConstruction() + { + foreach (Building b in this.buildings) + { + if (b.daysOfConstructionLeft > 0 || b.daysUntilUpgrade > 0) + return b; + } + + return null; + } + + public int getNumberBuildingsConstructed(string name) + { + return base.getNumberBuildingsConstructed(name); + } + + public bool isBuildable(Vector2 tileLocation) + { + return base.isBuildable(tileLocation); + } + + public new bool isPath(Vector2 tileLocation) + { + return base.isPath(tileLocation); + } + + public new bool isBuildingConstructed(string name) + { + return base.isBuildingConstructed(name); + } + + public new bool isThereABuildingUnderConstruction() + { + return base.isThereABuildingUnderConstruction(); + } + + + /********* + ** Private methods + *********/ + private BuildableGameLocationFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs new file mode 100644 index 000000000..5b8cf14e3 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley.Buildings; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BuildingFacade : Building, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetRef input => NetRefWrapperCache.GetCachedWrapperFor(this.GetBuildingChest("Input")); // Mill + public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(this.GetBuildingChest("Output")); // Mill + + + /********* + ** Private methods + *********/ + private BuildingFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs new file mode 100644 index 000000000..c0cd97b73 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BushFacade : Bush, IRewriteFacade + { + /********* + ** Public methods + *********/ + public bool inBloom(string season, int dayOfMonth) + { + // call new method if possible + if (season == Game1.currentSeason && dayOfMonth == Game1.dayOfMonth) + return this.inBloom(); + + // else mimic old behavior with 1.6 features + if (this.size == Bush.greenTeaBush) + { + return + this.getAge() >= Bush.daysToMatureGreenTeaBush + && dayOfMonth >= 22 + && (season != "winter" || this.IsSheltered()); + } + + switch (season) + { + case "spring": + return dayOfMonth > 14 && dayOfMonth < 19; + + case "fall": + return dayOfMonth > 7 && dayOfMonth < 12; + + default: + return false; + } + } + + public bool isDestroyable(GameLocation location, Vector2 tile) + { + return base.isDestroyable(); + } + + + /********* + ** Private methods + *********/ + private BushFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ButterflyFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ButterflyFacade.cs new file mode 100644 index 000000000..ff00b3627 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ButterflyFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.BellsAndWhistles; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ButterflyFacade : Butterfly, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Butterfly Constructor(Vector2 position, bool islandButterfly = false) + { + return new Butterfly(Game1.currentLocation, position, islandButterfly); + } + + + /********* + ** Private methods + *********/ + private ButterflyFacade() + : base(null, Vector2.Zero) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs new file mode 100644 index 000000000..659e58278 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class CarpenterMenuFacade : CarpenterMenu, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static CarpenterMenu Constructor(bool magicalConstruction = false) + { + return new CarpenterMenu(magicalConstruction ? Game1.builder_wizard : Game1.builder_robin); + } + + + /********* + ** Private methods + *********/ + private CarpenterMenuFacade() + : base(null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs new file mode 100644 index 000000000..a8c4c6d46 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class CaskFacade : Cask, IRewriteFacade + { + /********* + ** Public methods + *********/ + public bool IsValidCaskLocation(GameLocation location) + { + return this.IsValidCaskLocation(); + } + + + /********* + ** Private methods + *********/ + private CaskFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs new file mode 100644 index 000000000..dd069c6fe --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class CharacterFacade : Character, IRewriteFacade + { + /********* + ** Public methods + *********/ + public int getStandingX() + { + return this.StandingPixel.X; + } + + public int getStandingY() + { + return this.StandingPixel.Y; + } + + public Point getStandingXY() + { + return this.StandingPixel; + } + + public Vector2 getTileLocation() + { + return this.Tile; + } + + public Point getTileLocationPoint() + { + return this.TilePoint; + } + + public int getTileX() + { + return this.TilePoint.X; + } + + public int getTileY() + { + return this.TilePoint.Y; + } + + + /********* + ** Private methods + *********/ + private CharacterFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs new file mode 100644 index 000000000..e4abd8d37 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class ChestFacade : Chest, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetObjectList items => InventoryToNetObjectList.GetCachedWrapperFor(base.Items); + + + /********* + ** Public methods + *********/ + public static Chest Constructor(bool playerChest, Vector2 tileLocation, int parentSheetIndex = 130) + { + return new Chest(playerChest, tileLocation, parentSheetIndex.ToString()); + } + + public static Chest Constructor(bool playerChest, int parentSheedIndex = 130) + { + return new Chest(playerChest, parentSheedIndex.ToString()); + } + + public static Chest Constructor(Vector2 location) + { + return new Chest { TileLocation = location }; + } + + public ChestFacade(int parent_sheet_index, Vector2 tile_location, int starting_lid_frame, int lid_frame_count) + : base(parent_sheet_index.ToString(), tile_location, starting_lid_frame, lid_frame_count) { } + + public ChestFacade(int coins, List items, Vector2 location, bool giftbox = false, int giftboxIndex = 0) + : base(items, location, giftbox, giftboxIndex) { } + + public void destroyAndDropContents(Vector2 pointToDropAt, GameLocation location) + { + this.destroyAndDropContents(pointToDropAt); + } + + public void dumpContents(GameLocation location) + { + this.dumpContents(); + } + + + /********* + ** Private methods + *********/ + private ChestFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ClothingFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ClothingFacade.cs new file mode 100644 index 000000000..9d7bf226b --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ClothingFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ClothingFacade : Clothing, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Clothing Constructor(int item_index) + { + return new Clothing(item_index.ToString()); + } + + + /********* + ** Private methods + *********/ + private ClothingFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ColoredObjectFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ColoredObjectFacade.cs new file mode 100644 index 000000000..ce6017502 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ColoredObjectFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ColoredObjectFacade : ColoredObject, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static ColoredObject Constructor(int parentSheetIndex, int stack, Color color) + { + return new ColoredObject(parentSheetIndex.ToString(), stack, color); + } + + + /********* + ** Private methods + *********/ + private ColoredObjectFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs new file mode 100644 index 000000000..d18df7e3f --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class CropFacade : Crop, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Crop Constructor(bool forageCrop, int which, int tileX, int tileY) + { + return new Crop(forageCrop, which.ToString(), tileX, tileY, Game1.currentLocation); + } + + public static Crop Constructor(int seedIndex, int tileX, int tileY) + { + return new Crop(seedIndex.ToString(), tileX, tileY, Game1.currentLocation); + } + + + /********* + ** Private methods + *********/ + private CropFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs new file mode 100644 index 000000000..d6de9253f --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class DelayedActionFacade : DelayedAction, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static void playSoundAfterDelay(string soundName, int timer, GameLocation? location = null, int pitch = -1) + { + DelayedAction.playSoundAfterDelay(soundName, timer, location, pitch: pitch); + } + + + /********* + ** Private methods + *********/ + private DelayedActionFacade() + : base(0) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueBoxFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueBoxFacade.cs new file mode 100644 index 000000000..4c1676937 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueBoxFacade.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class DialogueBoxFacade : DialogueBox, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static DialogueBox Constructor(string dialogue, List responses, int width = 1200) + { + return new DialogueBox(dialogue, responses.ToArray(), width); + } + + + /********* + ** Private methods + *********/ + public DialogueBoxFacade() + : base(null as string) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueFacade.cs new file mode 100644 index 000000000..43ef82f0b --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DialogueFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class DialogueFacade : Dialogue, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Dialogue Constructor(string masterDialogue, NPC speaker) + { + return new Dialogue(speaker, null, masterDialogue); + } + + + /********* + ** Private methods + *********/ + private DialogueFacade() + : base(null, null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/EventFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/EventFacade.cs new file mode 100644 index 000000000..cd9f7ea0d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/EventFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class EventFacade : Event, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Event Constructor(string eventString, int eventID = -1, Farmer? farmerActor = null) + { + return new Event(eventString, null, eventID != -1 ? eventID.ToString() : null, farmerActor); + } + + + /********* + ** Private methods + *********/ + private EventFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs new file mode 100644 index 000000000..51178ea9e --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.GameData.FarmAnimals; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FarmAnimalFacade : FarmAnimal, IRewriteFacade + { + /********* + ** Public methods + *********/ + public bool isCoopDweller() + { + FarmAnimalData? data = this.GetAnimalData(); + return data?.House == "Coop"; + } + + + /********* + ** Private methods + *********/ + private FarmAnimalFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs new file mode 100644 index 000000000..8150fd04d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley; + +#pragma warning disable CS0618 // Type or member is obsolete: this is backwards-compatibility code. +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FarmerFacade : Farmer, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetObjectList items => InventoryToNetObjectList.GetCachedWrapperFor(base.Items); + + public new IList Items + { + get => base.Items; + set => base.Items.OverwriteWith(value); + } + + + /********* + ** Public methods + *********/ + public void addQuest(int questID) + { + this.addQuest(questID.ToString()); + } + + public void changePants(Color color) + { + this.changePantsColor(color); + } + + public void changePantStyle(int whichPants, bool is_customization_screen = false) + { + base.changePantStyle(whichPants.ToString()); + } + + public void changeShirt(int whichShirt, bool is_customization_screen = false) + { + this.changeShirt(whichShirt.ToString()); + } + + public void changeShoeColor(int which) + { + this.changeShoeColor(which.ToString()); + } + + public void completeQuest(int questID) + { + this.completeQuest(questID.ToString()); + } + + public bool couldInventoryAcceptThisObject(int index, int stack, int quality = 0) + { + return this.couldInventoryAcceptThisItem(index.ToString(), stack, quality); + } + + public int GetEffectsOfRingMultiplier(int ring_index) + { + return this.GetEffectsOfRingMultiplier(ring_index.ToString()); + } + + public int getItemCount(int item_index, int min_price = 0) + { + // minPrice field was always ignored + + return base.getItemCount(item_index.ToString()); + } + + public bool hasBuff(int whichBuff) + { + return this.hasBuff(whichBuff.ToString()); + } + + public bool hasItemInInventory(int itemIndex, int quantity, int minPrice = 0) + { + // minPrice field was always ignored + + switch (itemIndex) + { + case 858: + return this.QiGems >= quantity; + + case 73: + return Game1.netWorldState.Value.GoldenWalnuts >= quantity; + + default: + return this.getItemCount(ItemRegistry.type_object + itemIndex) >= quantity; + } + } + + public bool hasQuest(int id) + { + return this.hasQuest(id.ToString()); + } + + public bool isWearingRing(int ringIndex) + { + return this.isWearingRing(ringIndex.ToString()); + } + + public bool removeItemsFromInventory(int index, int stack) + { + if (this.hasItemInInventory(index, stack)) + { + switch (index) + { + case 858: + this.QiGems -= stack; + return true; + + case 73: + Game1.netWorldState.Value.GoldenWalnuts -= stack; + return true; + + default: + for (int i = 0; i < base.Items.Count; i++) + { + if (base.Items[i] is Object obj && obj.parentSheetIndex == index) + { + if (obj.Stack > stack) + { + obj.Stack -= stack; + return true; + } + + stack -= obj.Stack; + base.Items[i] = null; + } + + if (stack <= 0) + return true; + } + return false; + } + } + + return false; + } + + public void removeQuest(int questID) + { + this.removeQuest(questID.ToString()); + } + + + /********* + ** Private methods + *********/ + private FarmerFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs new file mode 100644 index 000000000..4486117ba --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FarmerTeamFacade : FarmerTeam, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetObjectList junimoChest => InventoryToNetObjectList.GetCachedWrapperFor(this.GetOrCreateGlobalInventory(FarmerTeam.GlobalInventoryId_JunimoChest)); + + + /********* + ** Private methods + *********/ + private FarmerTeamFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs new file mode 100644 index 000000000..e5c8c9f42 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FenceFacade : Fence, IRewriteFacade + { + /********* + ** Public methods + *********/ + public void toggleGate(GameLocation location, bool open, bool is_toggling_counterpart = false, Farmer? who = null) + { + this.toggleGate(open, is_toggling_counterpart, who); + } + + + /********* + ** Private methods + *********/ + private FenceFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs new file mode 100644 index 000000000..e702fe1c9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs @@ -0,0 +1,53 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Locations; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ForestFacade : Forest, IRewriteFacade + { + /********* + ** Accessors + *********/ + public ResourceClump? log + { + get + { + foreach (ResourceClump clump in this.resourceClumps) + { + if (clump.parentSheetIndex.Value == ResourceClump.hollowLogIndex && (int)clump.Tile.X == 2 && (int)clump.Tile.Y == 6) + return clump; + } + + return null; + } + set + { + // remove previous value + ResourceClump? clump = this.log; + if (clump != null) + this.resourceClumps.Remove(clump); + + // add new value + if (value != null) + this.resourceClumps.Add(value); + } + } + + + /********* + ** Private methods + *********/ + private ForestFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs new file mode 100644 index 000000000..933dc5538 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FruitTreeFacade : FruitTree, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetString fruitSeason + { + get + { + List? seasons = this.GetData()?.Seasons; + string value = seasons?.Count > 0 + ? string.Join(",", seasons) + : string.Empty; + + return new ReadOnlyValueToNetString($"{nameof(FruitTree)}.{nameof(this.fruitSeason)}", value); + } + } + + + /********* + ** Private methods + *********/ + private FruitTreeFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs new file mode 100644 index 000000000..f8ba103ce --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FurnitureFacade : Furniture, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Furniture Constructor(int which, Vector2 tile, int initialRotations) + { + return new Furniture(which.ToString(), tile, initialRotations); + } + + public static Furniture Constructor(int which, Vector2 tile) + { + return new Furniture(which.ToString(), tile); + } + + public void AddLightGlow(GameLocation location) + { + this.AddLightGlow(); + } + + public void addLights(GameLocation environment) + { + this.addLights(); + } + + public static Furniture GetFurnitureInstance(int index, Vector2? position = null) + { + return Furniture.GetFurnitureInstance(index.ToString(), position); + } + + public void removeLights(GameLocation environment) + { + this.removeLights(); + } + + public void RemoveLightGlow(GameLocation location) + { + this.RemoveLightGlow(); + } + + public void setFireplace(GameLocation location, bool playSound = true, bool broadcast = false) + { + this.setFireplace(playSound, broadcast); + } + + + /********* + ** Private methods + *********/ + private FurnitureFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs new file mode 100644 index 000000000..be532edb0 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs @@ -0,0 +1,109 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class Game1Facade : IRewriteFacade + { + /********* + ** Accessors + *********/ + [SuppressMessage("ReSharper", "ValueParameterNotUsed")] + public static bool menuUp { get; set; } // field was mostly unused and always false + + + /********* + ** Public methods + *********/ + public static bool canHaveWeddingOnDay(int day, string season) + { + return + Utility.TryParseEnum(season, out Season parsedSeason) + && Game1.canHaveWeddingOnDay(day, parsedSeason); + } + + public static NPC getCharacterFromName(string name, bool mustBeVillager = true, bool useLocationsListOnly = false) + { + return Game1.getCharacterFromName(name, mustBeVillager); + } + + public static string GetSeasonForLocation(GameLocation location) + { + Season season = Game1.GetSeasonForLocation(location); + return season.ToString(); + } + + public static void createMultipleObjectDebris(int index, int xTile, int yTile, int number) + { + Game1.createMultipleObjectDebris(index.ToString(), xTile, yTile, number); + } + + public static void createMultipleObjectDebris(int index, int xTile, int yTile, int number, GameLocation location) + { + Game1.createMultipleObjectDebris(index.ToString(), xTile, yTile, number, location); + } + + public static void createMultipleObjectDebris(int index, int xTile, int yTile, int number, float velocityMultiplier) + { + Game1.createMultipleObjectDebris(index.ToString(), xTile, yTile, number, velocityMultiplier); + } + + public static void createMultipleObjectDebris(int index, int xTile, int yTile, int number, long who) + { + Game1.createMultipleObjectDebris(index.ToString(), xTile, yTile, number, who); + } + + public static void createMultipleObjectDebris(int index, int xTile, int yTile, int number, long who, GameLocation location) + { + Game1.createMultipleObjectDebris(index.ToString(), xTile, yTile, number, who, location); + } + + public static void createObjectDebris(int objectIndex, int xTile, int yTile, long whichPlayer) + { + Game1.createObjectDebris(objectIndex.ToString(), xTile, yTile, whichPlayer); + } + + public static void createObjectDebris(int objectIndex, int xTile, int yTile, long whichPlayer, GameLocation location) + { + Game1.createObjectDebris(objectIndex.ToString(), xTile, yTile, whichPlayer, location); + } + + public static void createObjectDebris(int objectIndex, int xTile, int yTile, GameLocation location) + { + Game1.createObjectDebris(objectIndex.ToString(), xTile, yTile, location); + } + + public static void createObjectDebris(int objectIndex, int xTile, int yTile, int groundLevel = -1, int itemQuality = 0, float velocityMultiplyer = 1f, GameLocation? location = null) + { + Game1.createObjectDebris(objectIndex.ToString(), xTile, yTile, groundLevel, itemQuality, velocityMultiplyer, location); + } + + public static void drawDialogue(NPC speaker, string dialogue) + { + Game1.DrawDialogue(new Dialogue(speaker, null, dialogue)); + } + + public static void drawDialogue(NPC speaker, string dialogue, Texture2D overridePortrait) + { + Game1.DrawDialogue(new Dialogue(speaker, null, dialogue) { overridePortrait = overridePortrait }); + } + + + /********* + ** Private methods + *********/ + private Game1Facade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs new file mode 100644 index 000000000..a04095e3c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs @@ -0,0 +1,124 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Audio; +using StardewValley.Extensions; +using StardewValley.Objects; +using xTile.Dimensions; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class GameLocationFacade : GameLocation, IRewriteFacade + { + /********* + ** Public methods + *********/ + public NetCollection getCharacters() + { + return this.characters; + } + + public virtual int getExtraMillisecondsPerInGameMinuteForThisLocation() + { + return base.ExtraMillisecondsPerInGameMinute; + } + + public int getNumberBuildingsConstructed(string name) + { + return base.getNumberBuildingsConstructed(name, false); + } + + public string GetSeasonForLocation() + { + return this.GetSeasonKey(); + } + + public bool isTileLocationOpenIgnoreFrontLayers(Location tile) + { + return this.map.RequireLayer("Buildings").Tiles[tile.X, tile.Y] == null && !this.isWaterTile(tile.X, tile.Y); + } + + public bool isTileLocationTotallyClearAndPlaceable(int x, int y) + { + return this.isTileLocationTotallyClearAndPlaceable(new Vector2(x, y)); + } + + public bool isTileLocationTotallyClearAndPlaceable(Vector2 v) + { + Vector2 pixel = new Vector2((v.X * Game1.tileSize) + Game1.tileSize / 2, (v.Y * Game1.tileSize) + Game1.tileSize / 2); + foreach (Furniture f in this.furniture) + { + if (f.furniture_type != Furniture.rug && !f.isPassable() && f.GetBoundingBox().Contains((int)pixel.X, (int)pixel.Y) && !f.AllowPlacementOnThisTile((int)v.X, (int)v.Y)) + return false; + } + + return this.isTileOnMap(v) && !this.isTileOccupied(v) && this.isTilePassable(new Location((int)v.X, (int)v.Y), Game1.viewport) && base.isTilePlaceable(v); + } + + public bool isTileLocationTotallyClearAndPlaceableIgnoreFloors(Vector2 v) + { + return this.isTileOnMap(v) && !this.isTileOccupiedIgnoreFloors(v) && this.isTilePassable(new Location((int)v.X, (int)v.Y), Game1.viewport) && base.isTilePlaceable(v); + } + + public bool isTileOccupied(Vector2 tileLocation, string characterToIgnore = "", bool ignoreAllCharacters = false) + { + CollisionMask mask = ignoreAllCharacters ? CollisionMask.All & ~CollisionMask.Characters & ~CollisionMask.Farmers : CollisionMask.All; + return base.IsTileOccupiedBy(tileLocation, mask); + } + + public bool isTileOccupiedForPlacement(Vector2 tileLocation, Object? toPlace = null) + { + return base.CanItemBePlacedHere(tileLocation, toPlace != null && toPlace.isPassable()); + } + + public bool isTileOccupiedIgnoreFloors(Vector2 tileLocation, string characterToIgnore = "") + { + return base.IsTileOccupiedBy(tileLocation, CollisionMask.Buildings | CollisionMask.Furniture | CollisionMask.Objects | CollisionMask.Characters | CollisionMask.TerrainFeatures, ignorePassables: CollisionMask.Flooring); + } + + public void localSound(string audioName) + { + base.localSound(audioName); + } + + public void localSoundAt(string audioName, Vector2 position) + { + base.localSound(audioName, position); + } + + public void playSound(string audioName, SoundContext soundContext = SoundContext.Default) + { + base.playSound(audioName, context: soundContext); + } + + public void playSoundAt(string audioName, Vector2 position, SoundContext soundContext = SoundContext.Default) + { + base.playSound(audioName, position, context: soundContext); + } + + public void playSoundPitched(string audioName, int pitch, SoundContext soundContext = SoundContext.Default) + { + base.playSound(audioName, pitch: pitch, context: soundContext); + } + + + /********* + ** Private methods + *********/ + private GameLocationFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HatFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HatFacade.cs new file mode 100644 index 000000000..0f524c608 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HatFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class HatFacade : Hat, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Hat Constructor(int which) + { + return new Hat(which.ToString()); + } + + + /********* + ** Private methods + *********/ + private HatFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs new file mode 100644 index 000000000..96deb8fe3 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class HoeDirtFacade : HoeDirt, IRewriteFacade + { + /********* + ** Public methods + *********/ + public void destroyCrop(Vector2 tileLocation, bool showAnimation, GameLocation location) + { + base.destroyCrop(showAnimation); + } + + public bool paddyWaterCheck(GameLocation location, Vector2 tile_location) + { + return base.paddyWaterCheck(); + } + + public bool plant(int index, int tileX, int tileY, Farmer who, bool isFertilizer, GameLocation location) + { + return base.plant(index.ToString(), who, isFertilizer); + } + + + /********* + ** Private methods + *********/ + private HoeDirtFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs new file mode 100644 index 000000000..c23096e71 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs @@ -0,0 +1,68 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class HudMessageFacade : HUDMessage, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static HUDMessage Constructor(string message, bool achievement) + { + return HUDMessage.ForAchievement(message); + } + + public static HUDMessage Constructor(string type, int number, bool add, Color color, Item? messageSubject = null) + { + if (!add) + number = -number; + + if (type == "Hay" && messageSubject is null) + return HUDMessage.ForItemGained(ItemRegistry.Create("(O)178"), number); + + return new HUDMessage(null) + { + type = type, + timeLeft = HUDMessage.defaultTime, + number = number, + messageSubject = messageSubject + }; + } + + public static HUDMessage Constructor(string message, Color color, float timeLeft) + { + return Constructor(message, color, timeLeft, false); + } + + public static HUDMessage Constructor(string message, string leaveMeNull) + { + return HUDMessage.ForCornerTextbox(message); + } + + public static HUDMessage Constructor(string message, Color color, float timeLeft, bool fadeIn) + { + return new HUDMessage(message, timeLeft, fadeIn); + } + + + /********* + ** Private methods + *********/ + private HudMessageFacade() + : base(null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs new file mode 100644 index 000000000..82dfa10b9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class IClickableMenuFacade : IRewriteFacade + { + /********* + ** Public methods + *********/ + public static void drawHoverText(SpriteBatch b, string text, SpriteFont font, int xOffset = 0, int yOffset = 0, int moneyAmountToDisplayAtBottom = -1, string? boldTitleText = null, int healAmountToDisplay = -1, string[]? buffIconsToDisplay = null, Item? hoveredItem = null, int currencySymbol = 0, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, int overrideX = -1, int overrideY = -1, float alpha = 1f, CraftingRecipe? craftingIngredients = null, IList? additional_craft_materials = null) + { + IClickableMenu.drawHoverText( + b: b, + text: text, + font: font, + xOffset: xOffset, + yOffset: yOffset, + moneyAmountToDisplayAtBottom: moneyAmountToDisplayAtBottom, + boldTitleText: boldTitleText, + healAmountToDisplay: healAmountToDisplay, + buffIconsToDisplay: buffIconsToDisplay, + hoveredItem: hoveredItem, + currencySymbol: currencySymbol, + extraItemToShowAmount: extraItemToShowAmount, + extraItemToShowIndex: extraItemToShowIndex != -1 ? extraItemToShowAmount.ToString() : null, + overrideX: overrideX, + overrideY: overrideY, + alpha: alpha, + craftingIngredients: craftingIngredients, + additional_craft_materials: additional_craft_materials + ); + } + + public static void drawHoverText(SpriteBatch b, StringBuilder text, SpriteFont font, int xOffset = 0, int yOffset = 0, int moneyAmountToDisplayAtBottom = -1, string? boldTitleText = null, int healAmountToDisplay = -1, string[]? buffIconsToDisplay = null, Item? hoveredItem = null, int currencySymbol = 0, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, int overrideX = -1, int overrideY = -1, float alpha = 1f, CraftingRecipe? craftingIngredients = null, IList? additional_craft_materials = null) + { + IClickableMenu.drawHoverText( + b: b, + text: text, + font: font, + xOffset: xOffset, + yOffset: yOffset, + moneyAmountToDisplayAtBottom: moneyAmountToDisplayAtBottom, + boldTitleText: boldTitleText, + healAmountToDisplay: healAmountToDisplay, + buffIconsToDisplay: buffIconsToDisplay, + hoveredItem: hoveredItem, + currencySymbol: currencySymbol, + extraItemToShowAmount: extraItemToShowAmount, + extraItemToShowIndex: extraItemToShowIndex != -1 ? extraItemToShowAmount.ToString() : null, + overrideX: overrideX, + overrideY: overrideY, + alpha: alpha, + craftingIngredients: craftingIngredients, + additional_craft_materials: additional_craft_materials + ); + } + + public static void drawToolTip(SpriteBatch b, string hoverText, string hoverTitle, Item hoveredItem, bool heldItem = false, int healAmountToDisplay = -1, int currencySymbol = 0, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, CraftingRecipe? craftingIngredients = null, int moneyAmountToShowAtBottom = -1) + { + IClickableMenu.drawToolTip( + b: b, + hoverText: hoverText, + hoverTitle: hoverTitle, + hoveredItem: hoveredItem, + heldItem: heldItem, + healAmountToDisplay: healAmountToDisplay, + currencySymbol: currencySymbol, + extraItemToShowIndex: extraItemToShowIndex != -1 ? extraItemToShowAmount.ToString() : null, + extraItemToShowAmount: extraItemToShowAmount, + craftingIngredients: craftingIngredients, + moneyAmountToShowAtBottom: moneyAmountToShowAtBottom + ); + } + + + /********* + ** Private methods + *********/ + private IClickableMenuFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/InventoryToNetObjectList.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/InventoryToNetObjectList.cs new file mode 100644 index 000000000..5d22fcdfb --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/InventoryToNetObjectList.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using Netcode; +using StardewValley; +using StardewValley.Inventories; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal +{ + /// An implementation of which tracks an underlying instance. + internal class InventoryToNetObjectList : NetObjectList + { + /********* + ** Fields + *********/ + /// A cached lookup of inventory wrappers. + private static readonly Dictionary CachedWrappers = new Dictionary(ReferenceEqualityComparer.Instance); + + /// The underlying inventory to track. + private readonly Inventory Inventory; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying inventory to track. + public InventoryToNetObjectList(Inventory inventory) + { + this.Inventory = inventory; + + this.RebuildList(); + + this.Inventory.OnInventoryReplaced += this.OnInventoryReplaced; + this.Inventory.OnSlotChanged += this.OnInventorySlotChanged; + } + + /// Get a wrapper for a given inventory instance. + /// The inventory to track. + public static InventoryToNetObjectList GetCachedWrapperFor(Inventory inventory) + { + if (!CachedWrappers.TryGetValue(inventory, out InventoryToNetObjectList? wrapper)) + CachedWrappers[inventory] = wrapper = new InventoryToNetObjectList(inventory); + + return wrapper; + } + + /// + public override Item this[int index] + { + get => this.Inventory[index]; + set => this.Inventory[index] = value; + } + + /// + public override void Add(Item item) + { + this.Inventory.Add(item); + } + + /// + public override void Clear() + { + this.Inventory.Clear(); + } + + /// + public override void Insert(int index, Item item) + { + this.Inventory.Insert(index, item); + } + + /// + public override void RemoveAt(int index) + { + this.Inventory.RemoveAt(index); + } + + + /********* + ** Private methods + *********/ + /// Handle a change to the underlying inventory. + /// The inventory instance. + /// The slot index which changed. + /// The previous value. + /// The new value. + private void OnInventorySlotChanged(Inventory inventory, int index, Item before, Item after) + { + // don't use `this` to avoid re-editing the inventory + base[index] = after; + } + + /// Handle the underlying inventory getting replaced with a new list. + /// The inventory instance. + /// The previous list of values. + /// The new list of values. + private void OnInventoryReplaced(Inventory inventory, IList before, IList after) + { + this.RebuildList(); + } + + /// Rebuild the list to match the underlying inventory. + private void RebuildList() + { + // don't use `this` to avoid re-editing the inventory + base.Clear(); + foreach (Item slot in this.Inventory) + base.Add(slot); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/NetRefWrapperCache.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/NetRefWrapperCache.cs new file mode 100644 index 000000000..7e0963feb --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/NetRefWrapperCache.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.CompilerServices; +using Netcode; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal +{ + /// A cache of instances for specific values. + internal static class NetRefWrapperCache + where T : class, INetObject + { + /********* + ** Fields + *********/ + /// A cached lookup of wrappers. + private static readonly ConditionalWeakTable> CachedWrappers = new(); + + + /********* + ** Public methods + *********/ + /// Get a wrapper for a given value. + /// The value to wrap. + public static NetRef GetCachedWrapperFor(T value) + { + if (value is null) + throw new InvalidOperationException($"{nameof(NetRefWrapperCache)} doesn't support wrapping null values."); + + if (!CachedWrappers.TryGetValue(value, out NetRef? wrapper)) + { + wrapper = new NetRef(value); + CachedWrappers.AddOrUpdate(value, wrapper); + } + + return wrapper; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/ReadOnlyValueToNetString.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/ReadOnlyValueToNetString.cs new file mode 100644 index 000000000..c88e16631 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Internal/ReadOnlyValueToNetString.cs @@ -0,0 +1,34 @@ +using System; +using Netcode; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal +{ + /// An implementation of which returns a predetermined value and doesn't allow editing. + internal class ReadOnlyValueToNetString : NetString + { + /********* + ** Fields + *********/ + /// A human-readable name for the original field to show in error messages. + private readonly string FieldLabel; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the original field to show in error messages. + /// The value to set. + public ReadOnlyValueToNetString(string fieldLabel, string value) + : base(value) + { + this.FieldLabel = fieldLabel; + } + + /// + public override void Set(string newValue) + { + throw new InvalidOperationException($"The {this.FieldLabel} is no longer editable in Stardew Valley 1.6 and later."); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ItemFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ItemFacade.cs new file mode 100644 index 000000000..2107af457 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ItemFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class ItemFacade : Item, IRewriteFacade + { + /********* + ** Public methods + *********/ + public virtual bool canBePlacedHere(GameLocation l, Vector2 tile) + { + return base.canBePlacedHere(l, tile); + } + + + /********* + ** Private methods + *********/ + private ItemFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs new file mode 100644 index 000000000..d23439189 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley.Buildings; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class JunimoHutFacade : JunimoHut, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(this.GetBuildingChest("Output")); + + + /********* + ** Private methods + *********/ + private JunimoHutFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs new file mode 100644 index 000000000..41e71a278 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class LocalizedContentManagerFacade : LocalizedContentManager, IRewriteFacade + { + /********* + ** Public methods + *********/ + public new string LanguageCodeString(LanguageCode code) + { + return LocalizedContentManager.LanguageCodeString(code); + } + + + /********* + ** Private methods + *********/ + private LocalizedContentManagerFacade() + : base(null, null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs new file mode 100644 index 000000000..9c6279c9c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Tools; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class MeleeWeaponFacade : MeleeWeapon, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static MeleeWeapon Constructor(int spriteIndex) + { + return new MeleeWeapon(spriteIndex.ToString()); + } + + + /********* + ** Private methods + *********/ + private MeleeWeaponFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs new file mode 100644 index 000000000..7f2010a4d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class NetFieldBaseFacade : NetFieldBase, IRewriteFacade + where TSelf : NetFieldBase + { + /********* + ** Public methods + *********/ + public static T op_Implicit(NetFieldBase netField) + { + return netField.Value; + } + + + /********* + ** Private methods + *********/ + private NetFieldBaseFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetPausableFieldFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetPausableFieldFacade.cs new file mode 100644 index 000000000..dba0ad378 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetPausableFieldFacade.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Network; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class NetPausableFieldFacade : NetPausableField, IRewriteFacade + where TBaseField : NetFieldBase, new() + where TField : TBaseField, new() + { + /********* + ** Public methods + *********/ + public static T op_Implicit(NetPausableField field) + { + return field.Value; + } + + + /********* + ** Private methods + *********/ + private NetPausableFieldFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs new file mode 100644 index 000000000..0ddbb7394 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.BellsAndWhistles; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class NpcFacade : NPC, IRewriteFacade + { + /********* + ** Public methods + *********/ + public bool isBirthday(string season, int day) + { + // call new method if possible + if (season == Game1.currentSeason && day == Game1.dayOfMonth) + return this.isBirthday(); + + // else replicate old behavior + return + this.Birthday_Season != null + && this.Birthday_Season == season + && this.Birthday_Day == day; + } + + public void showTextAboveHead(string Text, int spriteTextColor = -1, int style = NPC.textStyle_none, int duration = 3000, int preTimer = 0) + { + Color? color = spriteTextColor != -1 ? SpriteText.getColorFromIndex(spriteTextColor) : null; + this.showTextAboveHead(Text, color, style, duration, preTimer); + } + + + /********* + ** Private methods + *********/ + private NpcFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs new file mode 100644 index 000000000..c61c9f994 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs @@ -0,0 +1,109 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using SObject = StardewValley.Object; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class ObjectFacade : SObject, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static SObject Constructor(Vector2 tileLocation, int parentSheetIndex, bool isRecipe = false) + { + return new SObject(tileLocation, parentSheetIndex.ToString(), isRecipe); + } + + public static SObject Constructor(int parentSheetIndex, int initialStack, bool isRecipe = false, int price = -1, int quality = 0) + { + return new SObject(parentSheetIndex.ToString(), initialStack, isRecipe, price, quality); + } + + public static SObject Constructor(Vector2 tileLocation, int parentSheetIndex, int initialStack) + { + SObject obj = Constructor(tileLocation, parentSheetIndex, null, true, true, false, false); + obj.stack.Value = initialStack; + return obj; + } + + public static SObject Constructor(Vector2 tileLocation, int parentSheetIndex, string? Givenname, bool canBeSetDown, bool canBeGrabbed, bool isHoedirt, bool isSpawnedObject) + { + SObject obj = new SObject(parentSheetIndex.ToString(), 1); + + if (Givenname != null && obj.name is (null or "Error Item")) + obj.name = Givenname; + + obj.tileLocation.Value = tileLocation; + obj.canBeSetDown.Value = canBeSetDown; + obj.canBeGrabbed.Value = canBeGrabbed; + obj.isSpawnedObject.Value = isSpawnedObject; + + return obj; + } + + public void ApplySprinkler(GameLocation location, Vector2 tile) + { + this.ApplySprinkler(tile); + } + + public void DayUpdate(GameLocation location) + { + this.DayUpdate(); + } + + public Rectangle getBoundingBox(Vector2 tileLocation) + { + return base.GetBoundingBoxAt((int)tileLocation.X, (int)tileLocation.Y); + } + + public bool isForage(GameLocation location) + { + return this.isForage(); + } + + public bool minutesElapsed(int minutes, GameLocation environment) + { + return this.minutesElapsed(minutes); + } + + public bool onExplosion(Farmer who, GameLocation location) + { + return this.onExplosion(who); + } + + public void performRemoveAction(Vector2 tileLocation, GameLocation environment) + { + this.performRemoveAction(); + } + + public bool performToolAction(Tool t, GameLocation location) + { + return this.performToolAction(t); + } + + public void updateWhenCurrentLocation(GameTime time, GameLocation environment) + { + this.updateWhenCurrentLocation(time); + } + + + /********* + ** Private methods + *********/ + private ObjectFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/OverlaidDictionaryFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/OverlaidDictionaryFacade.cs new file mode 100644 index 000000000..73f70daaf --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/OverlaidDictionaryFacade.cs @@ -0,0 +1,216 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Network; +using SObject = StardewValley.Object; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "StructCanBeMadeReadOnly", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class OverlaidDictionaryFacade : OverlaidDictionary, IRewriteFacade + { + /********* + ** Accessors + *********/ + public new KeysCollection Keys => new(this); + public new ValuesCollection Values => new(this); + public new PairsCollection Pairs => new(this); + + + /********* + ** Enumerator facades + *********/ + public struct KeysCollection : IEnumerable + { + private readonly OverlaidDictionary Dictionary; + + public KeysCollection(OverlaidDictionary dictionary) + { + this.Dictionary = dictionary; + } + + public int Count() + { + return this.Dictionary.Length; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + public struct Enumerator : IEnumerator + { + private readonly OverlaidDictionary Dictionary; + private Dictionary.KeyCollection.Enumerator CurEnumerator; + + public Vector2 Current => this.CurEnumerator.Current; + object IEnumerator.Current => this.CurEnumerator.Current; + + public Enumerator(OverlaidDictionary dictionary) + { + this.Dictionary = dictionary; + this.CurEnumerator = dictionary.Keys.GetEnumerator(); + } + + public bool MoveNext() + { + return this.CurEnumerator.MoveNext(); + } + + public void Dispose() + { + this.CurEnumerator.Dispose(); + } + + void IEnumerator.Reset() + { + this.CurEnumerator = this.Dictionary.Keys.GetEnumerator(); + } + } + } + + public struct PairsCollection : IEnumerable> + { + private readonly OverlaidDictionary Dictionary; + + public PairsCollection(OverlaidDictionary dictionary) + { + this.Dictionary = dictionary; + } + + public KeyValuePair ElementAt(int index) + { + return this.Dictionary.Pairs.ElementAt(index); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + public struct Enumerator : IEnumerator> + { + private readonly OverlaidDictionary Dictionary; + private IEnumerator> CurEnumerator; + + public KeyValuePair Current => this.CurEnumerator.Current; + object IEnumerator.Current => this.CurEnumerator.Current; + + public Enumerator(OverlaidDictionary dictionary) + { + this.Dictionary = dictionary; + this.CurEnumerator = dictionary.Pairs.GetEnumerator(); + } + + public bool MoveNext() + { + return this.CurEnumerator.MoveNext(); + } + + public void Dispose() + { + this.CurEnumerator.Dispose(); + } + + void IEnumerator.Reset() + { + this.CurEnumerator = this.Dictionary.Pairs.GetEnumerator(); + } + } + } + + public struct ValuesCollection : IEnumerable + { + private readonly OverlaidDictionary Dictionary; + + public ValuesCollection(OverlaidDictionary dictionary) + { + this.Dictionary = dictionary; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this.Dictionary); + } + + public struct Enumerator : IEnumerator + { + private readonly OverlaidDictionary Dictionary; + private Dictionary.ValueCollection.Enumerator CurEnumerator; + + public SObject Current => this.CurEnumerator.Current; + object IEnumerator.Current => this.CurEnumerator.Current; + + public Enumerator(OverlaidDictionary dictionary) + { + this.Dictionary = dictionary; + this.CurEnumerator = dictionary.Values.GetEnumerator(); + } + + public bool MoveNext() + { + return this.CurEnumerator.MoveNext(); + } + + public void Dispose() + { + this.CurEnumerator.Dispose(); + } + + void IEnumerator.Reset() + { + this.CurEnumerator = this.Dictionary.Values.GetEnumerator(); + } + } + } + + + /********* + ** Private methods + *********/ + private OverlaidDictionaryFacade() + : base(null, null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/PathFindControllerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/PathFindControllerFacade.cs new file mode 100644 index 000000000..aee05bbae --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/PathFindControllerFacade.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Pathfinding; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class PathFindControllerFacade : PathFindController, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static PathFindController Constructor(Character c, GameLocation location, Point endPoint, int finalFacingDirection, bool eraseOldPathController, bool clearMarriageDialogues = true) + { + return new PathFindController(c, location, endPoint, finalFacingDirection, clearMarriageDialogues); + } + + public static PathFindController Constructor(Character c, GameLocation location, isAtEnd endFunction, int finalFacingDirection, bool eraseOldPathController, endBehavior endBehaviorFunction, int limit, Point endPoint, bool clearMarriageDialogues = true) + { + return new PathFindController(c, location, endFunction, finalFacingDirection, endBehaviorFunction, limit, endPoint, clearMarriageDialogues); + } + + + /********* + ** Private methods + *********/ + private PathFindControllerFacade() + : base(null, null, Point.Zero, 0) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs new file mode 100644 index 000000000..355147f3a --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ResourceClumpFacade : ResourceClump, IRewriteFacade + { + /********* + ** Public methods + *********/ + public NetVector2 tile => this.netTile; + + + /********* + ** Private methods + *********/ + private ResourceClumpFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs new file mode 100644 index 000000000..2f891f229 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class RingFacade : Ring, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Ring Constructor(int which) + { + return new Ring(which.ToString()); + } + + + /********* + ** Private methods + *********/ + private RingFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs new file mode 100644 index 000000000..688d8495e --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ShopMenuFacade : ShopMenu, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static ShopMenu Constructor(Dictionary itemPriceAndStock, int currency = 0, string? who = null, Func? on_purchase = null, Func? on_sell = null, string? context = null) + { + return new ShopMenu(ShopMenuFacade.GetShopId(context), ShopMenuFacade.ToItemStockInformation(itemPriceAndStock), currency, who, on_purchase, on_sell, playOpenSound: true); + } + + public static ShopMenu Constructor(List itemsForSale, int currency = 0, string? who = null, Func? on_purchase = null, Func? on_sell = null, string? context = null) + { + return new ShopMenu(ShopMenuFacade.GetShopId(context), itemsForSale, currency, who, on_purchase, on_sell, playOpenSound: true); + } + + + /********* + ** Private methods + *********/ + private ShopMenuFacade() + : base(null, null, null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + + private static string GetShopId(string? context) + { + return string.IsNullOrWhiteSpace(context) + ? "legacy_mod_code_" + Guid.NewGuid().ToString("N") + : context; + } + + private static Dictionary ToItemStockInformation(Dictionary? itemPriceAndStock) + { + Dictionary stock = new(); + + if (itemPriceAndStock != null) + { + foreach (var pair in itemPriceAndStock) + stock[pair.Key] = new ItemStockInformation(pair.Value[0], pair.Value[1]); + } + + return stock; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SlingshotFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SlingshotFacade.cs new file mode 100644 index 000000000..b65b4591b --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SlingshotFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Tools; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class SlingshotFacade : Slingshot, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Slingshot Constructor(int which = 32) + { + return new Slingshot(which.ToString()); + } + + + /********* + ** Private methods + *********/ + private SlingshotFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs new file mode 100644 index 000000000..a189f6b06 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs @@ -0,0 +1,82 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.BellsAndWhistles; +using static StardewValley.BellsAndWhistles.SpriteText; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class SpriteTextFacade : IRewriteFacade + { + /********* + ** Public methods + *********/ + public static void drawString(SpriteBatch b, string s, int x, int y, int characterPosition = maxCharacter, int width = -1, int height = maxHeight, float alpha = 1f, float layerDepth = .88f, bool junimoText = false, int drawBGScroll = -1, string placeHolderScrollWidthText = "", int color = -1, ScrollTextAlignment scroll_text_alignment = ScrollTextAlignment.Left) + { + SpriteText.drawString( + b: b, + s: s, + x: x, + y: y, + characterPosition: characterPosition, + width: width, + height: height, + alpha: alpha, + layerDepth: layerDepth, + junimoText: junimoText, + drawBGScroll: drawBGScroll, + placeHolderScrollWidthText: placeHolderScrollWidthText, + color: color != -1 ? SpriteText.getColorFromIndex(color) : null, + scroll_text_alignment: scroll_text_alignment + ); + } + + public static void drawStringWithScrollCenteredAt(SpriteBatch b, string s, int x, int y, int width, float alpha = 1f, int color = -1, int scrollType = SpriteText.scrollStyle_scroll, float layerDepth = .88f, bool junimoText = false) + { + SpriteText.drawStringWithScrollCenteredAt( + b: b, + s: s, + x: x, + y: y, + width: width, + alpha: alpha, + color: color != -1 ? SpriteText.getColorFromIndex(color) : null, + scrollType: scrollType, + layerDepth: layerDepth, + junimoText: junimoText + ); + } + + public static void drawStringWithScrollCenteredAt(SpriteBatch b, string s, int x, int y, string placeHolderWidthText = "", float alpha = 1f, int color = -1, int scrollType = scrollStyle_scroll, float layerDepth = .88f, bool junimoText = false) + { + SpriteText.drawStringWithScrollCenteredAt( + b: b, + s: s, + x: x, + y: y, + placeHolderWidthText: placeHolderWidthText, + alpha: alpha, + color: color != -1 ? SpriteText.getColorFromIndex(color) : null, + scrollType: scrollType, + layerDepth: layerDepth, + junimoText: junimoText + ); + } + + + /********* + ** Private methods + *********/ + private SpriteTextFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StorageFurnitureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StorageFurnitureFacade.cs new file mode 100644 index 000000000..7949f6821 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StorageFurnitureFacade.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class StorageFurnitureFacade : StorageFurniture, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static StorageFurniture Constructor(int which, Vector2 tile, int initialRotations) + { + return new StorageFurniture(which.ToString(), tile, initialRotations); + } + + public static StorageFurniture Constructor(int which, Vector2 tile) + { + return new StorageFurniture(which.ToString(), tile); + } + + + /********* + ** Private methods + *********/ + private StorageFurnitureFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs new file mode 100644 index 000000000..959af929c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class TerrainFeatureFacade : TerrainFeature, IRewriteFacade + { + /********* + ** Public methods + *********/ + public virtual void dayUpdate(GameLocation environment, Vector2 tileLocation) + { + base.dayUpdate(); + } + + public Rectangle getBoundingBox(Vector2 tileLocation) + { + return base.getBoundingBox(); + } + + public virtual bool performToolAction(Tool t, int damage, Vector2 tileLocation, GameLocation location) + { + return base.performToolAction(t, damage, tileLocation); + } + + public virtual bool performUseAction(Vector2 tileLocation, GameLocation location) + { + return base.performUseAction(tileLocation); + } + + + /********* + ** Private methods + *********/ + private TerrainFeatureFacade() + : base(false) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs new file mode 100644 index 000000000..5ac0aca41 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class TreeFacade : Tree, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Tree Constructor(int which) + { + return new Tree(which.ToString()); + } + + public static Tree Constructor(int which, int growthStage) + { + return new Tree(which.ToString(), growthStage); + } + + public bool fertilize(GameLocation location) + { + return base.fertilize(); + } + + + /********* + ** Private methods + *********/ + private TreeFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs new file mode 100644 index 000000000..026436047 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Extensions; + +#pragma warning disable CS0618 // Type or member is obsolete: this is backwards-compatibility code. +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class UtilityFacade : IRewriteFacade + { + /********* + ** Public methods + *********/ + public static DisposableList getAllCharacters() + { + return new DisposableList(Utility.getAllCharacters()); + } + + public static List getAllCharacters(List list) + { + list.AddRange(Utility.getAllCharacters()); + return list; + } + + public static T GetRandom(List list, Random? random = null) + { + return (random ?? Game1.random).ChooseFrom(list); + } + + public static bool HasAnyPlayerSeenEvent(int event_number) + { + return Utility.HasAnyPlayerSeenEvent(event_number.ToString()); + } + + public static bool HaveAllPlayersSeenEvent(int event_number) + { + return Utility.HaveAllPlayersSeenEvent(event_number.ToString()); + } + + public static bool isFestivalDay(int day, string season) + { + return + Utility.TryParseEnum(season, out Season parsedSeason) + && Utility.isFestivalDay(day, parsedSeason); + } + + public static bool IsNormalObjectAtParentSheetIndex(Item item, int index) + { + return Utility.IsNormalObjectAtParentSheetIndex(item, index.ToString()); + } + + public static int numSilos() + { + return Game1.GetNumberBuildingsConstructed("Silo"); + } + + + /********* + ** Private methods + *********/ + private UtilityFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ViewportExtensionsFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ViewportExtensionsFacade.cs new file mode 100644 index 000000000..2e7345616 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ViewportExtensionsFacade.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Extensions; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's ViewportExtensions methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ViewportExtensionsFacade : IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Rectangle GetTitleSafeArea(Viewport vp) + { + return vp.GetTitleSafeArea(); + } + + public static Rectangle ToXna(xTile.Dimensions.Rectangle xrect) + { + return new Rectangle(xrect.X, xrect.Y, xrect.Width, xrect.Height); + } + + public static Vector2 Size(Viewport vp) + { + return new Vector2(vp.Width, vp.Height); + } + + + /********* + ** Private methods + *********/ + private ViewportExtensionsFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs new file mode 100644 index 000000000..f91490707 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class WorldDateFacade : WorldDate, IRewriteFacade + { + /********* + ** Accessors + *********/ + public new string Season + { + get => this.SeasonKey; + set => this.SeasonKey = value; + } + + + /********* + ** Private methods + *********/ + private WorldDateFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 73ac80db2..2920161c3 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -1,11 +1,29 @@ using System.Collections.Generic; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Netcode; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_5; +using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6; using StardewValley; +using StardewValley.Audio; +using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; +using StardewValley.Enchantments; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Mods; +using StardewValley.Objects; +using StardewValley.Pathfinding; +using StardewValley.SpecialOrders; +using StardewValley.SpecialOrders.Objectives; +using StardewValley.SpecialOrders.Rewards; +using StardewValley.TerrainFeatures; +using StardewValley.Tools; +using SObject = StardewValley.Object; namespace StardewModdingAPI.Metadata { @@ -40,13 +58,176 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr // specific versions yield return new ReplaceReferencesRewriter() - // Stardew Valley 1.5 (fields moved) + /**** + ** Stardew Valley 1.5 + ****/ + // fields moved .MapField("Netcode.NetCollection`1 StardewValley.Locations.DecoratableLocation::furniture", typeof(GameLocation), nameof(GameLocation.furniture)) .MapField("Netcode.NetCollection`1 StardewValley.Farm::resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)) .MapField("Netcode.NetCollection`1 StardewValley.Locations.MineShaft::resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps)) - // Stardew Valley 1.5.5 (XNA => MonoGame method changes) - .MapFacade(); + /**** + ** Stardew Valley 1.5.5 + ****/ + // XNA => MonoGame method changes + .MapFacade() + + /**** + ** Stardew Valley 1.6 + ****/ + // moved types + .MapType("StardewValley.AmethystEnchantment", typeof(AmethystEnchantment)) + .MapType("StardewValley.AquamarineEnchantment", typeof(AquamarineEnchantment)) + .MapType("StardewValley.ArchaeologistEnchantment", typeof(ArchaeologistEnchantment)) + .MapType("StardewValley.ArtfulEnchantment", typeof(ArtfulEnchantment)) + .MapType("StardewValley.AutoHookEnchantment", typeof(AutoHookEnchantment)) + .MapType("StardewValley.AxeEnchantment", typeof(AxeEnchantment)) + .MapType("StardewValley.BaseEnchantment", typeof(BaseEnchantment)) + .MapType("StardewValley.BaseWeaponEnchantment", typeof(BaseWeaponEnchantment)) + .MapType("StardewValley.BottomlessEnchantment", typeof(BottomlessEnchantment)) + .MapType("StardewValley.BugKillerEnchantment", typeof(BugKillerEnchantment)) + .MapType("StardewValley.CrusaderEnchantment", typeof(CrusaderEnchantment)) + .MapType("StardewValley.DiamondEnchantment", typeof(DiamondEnchantment)) + .MapType("StardewValley.EfficientToolEnchantment", typeof(EfficientToolEnchantment)) + .MapType("StardewValley.EmeraldEnchantment", typeof(EmeraldEnchantment)) + .MapType("StardewValley.FishingRodEnchantment", typeof(FishingRodEnchantment)) + .MapType("StardewValley.GalaxySoulEnchantment", typeof(GalaxySoulEnchantment)) + .MapType("StardewValley.GenerousEnchantment", typeof(GenerousEnchantment)) + .MapType("StardewValley.HaymakerEnchantment", typeof(HaymakerEnchantment)) + .MapType("StardewValley.HoeEnchantment", typeof(HoeEnchantment)) + .MapType("StardewValley.JadeEnchantment", typeof(JadeEnchantment)) + .MapType("StardewValley.MagicEnchantment", typeof(MagicEnchantment)) + .MapType("StardewValley.MasterEnchantment", typeof(MasterEnchantment)) + .MapType("StardewValley.MilkPailEnchantment", typeof(MilkPailEnchantment)) + .MapType("StardewValley.PanEnchantment", typeof(PanEnchantment)) + .MapType("StardewValley.PickaxeEnchantment", typeof(PickaxeEnchantment)) + .MapType("StardewValley.PowerfulEnchantment", typeof(PowerfulEnchantment)) + .MapType("StardewValley.PreservingEnchantment", typeof(PreservingEnchantment)) + .MapType("StardewValley.ReachingToolEnchantment", typeof(ReachingToolEnchantment)) + .MapType("StardewValley.RubyEnchantment", typeof(RubyEnchantment)) + .MapType("StardewValley.ShavingEnchantment", typeof(ShavingEnchantment)) + .MapType("StardewValley.ShearsEnchantment", typeof(ShearsEnchantment)) + .MapType("StardewValley.SwiftToolEnchantment", typeof(SwiftToolEnchantment)) + .MapType("StardewValley.TopazEnchantment", typeof(TopazEnchantment)) + .MapType("StardewValley.VampiricEnchantment", typeof(VampiricEnchantment)) + .MapType("StardewValley.WateringCanEnchantment", typeof(WateringCanEnchantment)) + + .MapType("StardewValley.SpecialOrder", typeof(SpecialOrder)) + .MapType("StardewValley.SpecialOrder/QuestState", typeof(SpecialOrderStatus)) + + .MapType("StardewValley.CollectObjective", typeof(CollectObjective)) + .MapType("StardewValley.DeliverObjective", typeof(DeliverObjective)) + .MapType("StardewValley.DonateObjective", typeof(DonateObjective)) + .MapType("StardewValley.FishObjective", typeof(FishObjective)) + .MapType("StardewValley.GiftObjective", typeof(GiftObjective)) + .MapType("StardewValley.JKScoreObjective", typeof(JKScoreObjective)) + .MapType("StardewValley.OrderObjective", typeof(OrderObjective)) + .MapType("StardewValley.ReachMineFloorObjective", typeof(ReachMineFloorObjective)) + .MapType("StardewValley.ShipObjective", typeof(ShipObjective)) + .MapType("StardewValley.SlayObjective", typeof(SlayObjective)) + + .MapType("StardewValley.FriendshipReward", typeof(FriendshipReward)) + .MapType("StardewValley.GemsReward", typeof(GemsReward)) + .MapType("StardewValley.MailReward", typeof(MailReward)) + .MapType("StardewValley.MoneyReward", typeof(MoneyReward)) + .MapType("StardewValley.OrderReward", typeof(OrderReward)) + .MapType("StardewValley.ResetEventReward", typeof(ResetEventReward)) + + .MapType("StardewValley.IOAudioEngine", typeof(IAudioEngine)) + .MapType("StardewValley.ModDataDictionary", typeof(ModDataDictionary)) + .MapType("StardewValley.ModHooks", typeof(ModHooks)) + .MapType("StardewValley.Network.NetAudio/SoundContext", typeof(SoundContext)) + .MapType("StardewValley.PathFindController", typeof(PathFindController)) + + // general API changes + // note: types are mapped before members, regardless of the order listed here + .MapType("StardewValley.Buildings.Mill", typeof(Building)) + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade("Microsoft.Xna.Framework.Graphics.ViewportExtensions", typeof(ViewportExtensionsFacade)) + .MapFacade() + .MapMethod("System.Void StardewValley.BellsAndWhistles.SpriteText::drawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.String,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Boolean,System.Int32,System.String,System.Int32,StardewValley.BellsAndWhistles.SpriteText/ScrollTextAlignment)", typeof(SpriteTextFacade), nameof(SpriteTextFacade.drawString)) // may not get rewritten by the MapFacade above due to the ScrollTextAlignment enum also being de-nested in 1.6 too + + // BuildableGameLocation merged into GameLocation + .MapFacade("StardewValley.Locations.BuildableGameLocation", typeof(BuildableGameLocationFacade)) + .MapField("Netcode.NetCollection`1 StardewValley.Locations.BuildableGameLocation::buildings", typeof(GameLocation), nameof(GameLocation.buildings)) + + // OverlaidDictionary enumerators changed + // note: types are mapped before members, regardless of the order listed here + .MapType("StardewValley.Network.OverlaidDictionary/KeysCollection", typeof(OverlaidDictionaryFacade.KeysCollection)) + .MapType("StardewValley.Network.OverlaidDictionary/KeysCollection/Enumerator", typeof(OverlaidDictionaryFacade.KeysCollection.Enumerator)) + .MapType("StardewValley.Network.OverlaidDictionary/PairsCollection", typeof(OverlaidDictionaryFacade.PairsCollection)) + .MapType("StardewValley.Network.OverlaidDictionary/PairsCollection/Enumerator", typeof(OverlaidDictionaryFacade.PairsCollection.Enumerator)) + .MapType("StardewValley.Network.OverlaidDictionary/ValuesCollection", typeof(OverlaidDictionaryFacade.ValuesCollection)) + .MapType("StardewValley.Network.OverlaidDictionary/ValuesCollection/Enumerator", typeof(OverlaidDictionaryFacade.ValuesCollection.Enumerator)) + .MapMethod($"{typeof(OverlaidDictionaryFacade).FullName}/{nameof(OverlaidDictionaryFacade.KeysCollection)} StardewValley.Network.OverlaidDictionary::get_Keys()", typeof(OverlaidDictionaryFacade), $"get_{nameof(OverlaidDictionaryFacade.Keys)}") + .MapMethod($"{typeof(OverlaidDictionaryFacade).FullName}/{nameof(OverlaidDictionaryFacade.PairsCollection)} StardewValley.Network.OverlaidDictionary::get_Pairs()", typeof(OverlaidDictionaryFacade), $"get_{nameof(OverlaidDictionaryFacade.Pairs)}") + .MapMethod($"{typeof(OverlaidDictionaryFacade).FullName}/{nameof(OverlaidDictionaryFacade.ValuesCollection)} StardewValley.Network.OverlaidDictionary::get_Values()", typeof(OverlaidDictionaryFacade), $"get_{nameof(OverlaidDictionaryFacade.Values)}") + + // implicit NetField conversions removed + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + + .MapMethod("!0 StardewValley.Network.NetPausableField`3::op_Implicit(StardewValley.Network.NetPausableField`3)", typeof(NetPausableFieldFacade), nameof(NetPausableFieldFacade.op_Implicit)); // 32-bit to 64-bit in Stardew Valley 1.5.5 yield return new ArchitectureAssemblyRewriter(); From 5220bf209562b79d0f81bdebafb916447445f2c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 17 Dec 2023 12:53:12 -0500 Subject: [PATCH 43/84] add asset propagation for new Data/TriggerActions asset --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 4a830e54a..794893138 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -14,6 +14,7 @@ using StardewValley.Locations; using StardewValley.Pathfinding; using StardewValley.TerrainFeatures; +using StardewValley.Triggers; using StardewValley.WorldMaps; using xTile; @@ -393,6 +394,10 @@ static ISet GetWarpSet(GameLocation location) ItemRegistry.ResetCache(); return true; + case "data/triggeractions": + TriggerActionManager.ResetDataCache(); + return true; + case "data/weapons": // Game1.LoadContent Game1.weaponData = DataLoader.Weapons(content); ItemRegistry.ResetCache(); From 1128ec6d6f1cf60f6cc8a77cd3d39da6bb3dbe54 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Dec 2023 20:00:06 -0500 Subject: [PATCH 44/84] fix duplicate [JsonConstructor] in SDate --- src/SMAPI/Utilities/SDate.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index fc0d7505d..33512cc80 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -85,7 +85,6 @@ public SDate(int day, string season, int year) /// The season name. /// The year. /// One of the arguments has an invalid value (like day 35). - [JsonConstructor] public SDate(int day, Season season, int year) : this(day, season, year, allowDayZero: false) { } From 8dce49eeaf076e12c3fb8efbcf2eaa3cca016530 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Dec 2023 20:00:06 -0500 Subject: [PATCH 45/84] show new game version label in log --- src/SMAPI/Framework/Logging/LogManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index b9c32a597..cd17d7c30 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -12,6 +12,7 @@ using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; namespace StardewModdingAPI.Framework.Logging { @@ -220,7 +221,7 @@ public void LogFatalLaunchError(Exception exception) public void LogIntro(string modsPath, IDictionary customSettings) { // log platform - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.GetVersionString()} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); // log basic info this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); From d4b814521ba910a877682c8e3e5dac6e9ab90149 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 13:12:51 -0500 Subject: [PATCH 46/84] update for ID -> Id data field renames --- .../Commands/Player/SetFarmTypeCommand.cs | 14 +++--- .../Rewriters/ReplaceReferencesRewriter.cs | 48 +++++++++++++++---- src/SMAPI/Metadata/InstructionMetadata.cs | 12 +++++ 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs index 1483a577f..e270c2ddf 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs @@ -73,8 +73,8 @@ private void HandleList(IMonitor monitor) if (customTypes.Any()) { result.AppendLine("Or one of these custom farm types:"); - foreach (var type in customTypes.Values.OrderBy(p => p.ID)) - result.AppendLine($" - {type.ID} ({this.GetCustomName(type)})"); + foreach (var type in customTypes.Values.OrderBy(p => p.Id)) + result.AppendLine($" - {type.Id} ({this.GetCustomName(type)})"); } else result.AppendLine("Or a custom farm type (though none is loaded currently)."); @@ -104,7 +104,7 @@ private void HandleVanillaFarmType(int type, IMonitor monitor) /// Writes messages to the console and log file. private void HandleCustomFarmType(string id, IMonitor monitor) { - if (Game1.whichModFarm?.ID == id) + if (Game1.whichModFarm?.Id == id) { monitor.Log($"Your current farm is already set to {id} ({this.GetCustomName(Game1.whichModFarm)}).", LogLevel.Info); return; @@ -200,9 +200,9 @@ private IDictionary GetVanillaFarmTypes() private string? GetCustomName(ModFarmType? farmType) { if (string.IsNullOrWhiteSpace(farmType?.TooltipStringPath)) - return farmType?.ID; + return farmType?.Id; - return Game1.content.LoadString(farmType.TooltipStringPath)?.Split('_')[0] ?? farmType.ID; + return Game1.content.LoadString(farmType.TooltipStringPath)?.Split('_')[0] ?? farmType.Id; } /// Get the available custom farm types by ID. @@ -212,10 +212,10 @@ private IDictionary GetCustomFarmTypes() foreach (ModFarmType farmType in DataLoader.AdditionalFarms(Game1.content)) { - if (string.IsNullOrWhiteSpace(farmType.ID)) + if (string.IsNullOrWhiteSpace(farmType.Id)) continue; - farmTypes[farmType.ID] = farmType; + farmTypes[farmType.Id] = farmType; } return farmTypes; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index 807b84cde..b3ba6e6d6 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -87,7 +87,7 @@ public ReplaceReferencesRewriter MapType(string fromFullName, Type toType) return this; } - /// Rewrite field references to point to another field with the same field type. + /// Rewrite field references to point to another field with the same field type (not necessarily on the same parent). /// The full field name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::Tile. /// The new type which will have the field. /// The new field name to reference. @@ -99,22 +99,54 @@ public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, stri if (toType is null) throw new ArgumentException("Can't replace a field given a null target type.", nameof(toType)); if (string.IsNullOrWhiteSpace(toName)) - throw new ArgumentException("Can't replace a field given an empty target name.", nameof(toType)); + throw new ArgumentException("Can't replace a field given an empty target name.", nameof(toName)); // get field FieldInfo? toField; try { toField = toType.GetField(toName); - if (toField is null) - throw new InvalidOperationException($"Required field {toType.FullName}::{toName} could not be loaded."); } catch (Exception ex) { throw new InvalidOperationException($"Required field {toType.FullName}::{toName} could not be loaded.", ex); } + if (toField is null) + throw new InvalidOperationException($"Required field {toType.FullName}::{toName} could not be found."); + + // add mapping + return this.MapMember(fromFullName, toField, "field"); + } + + /// Rewrite field references to point to another field with the field and parent type. + /// The type which has the old and new fields. + /// The field name. + /// The new field name to reference. + public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string toName) + { + // validate parameters + if (type is null) + throw new ArgumentException("Can't replace a field given a null target type.", nameof(type)); + if (string.IsNullOrWhiteSpace(fromName)) + throw new ArgumentException("Can't replace a field given an empty name.", nameof(fromName)); + if (string.IsNullOrWhiteSpace(toName)) + throw new ArgumentException("Can't replace a field given an empty target name.", nameof(toName)); + + // get field + FieldInfo? toField; + try + { + toField = type.GetField(toName); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Required field {type.FullName}::{toName} could not be loaded.", ex); + } + if (toField is null) + throw new InvalidOperationException($"Required field {type.FullName}::{toName} could not be found."); // add mapping + string fromFullName = $"{this.FormatCecilType(toField.FieldType)} {this.FormatCecilType(type)}::{fromName}"; return this.MapMember(fromFullName, toField, "field"); } @@ -137,13 +169,13 @@ public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type to try { toProperty = toType.GetProperty(toName); - if (toProperty is null) - throw new InvalidOperationException($"Required property {toType.FullName}::{toName} could not be loaded."); } catch (Exception ex) { throw new InvalidOperationException($"Required property {toType.FullName}::{toName} could not be loaded.", ex); } + if (toProperty is null) + throw new InvalidOperationException($"Required property {toType.FullName}::{toName} could not be found."); // add mapping return this.MapMember(fromFullName, toProperty, "field-to-property"); @@ -171,13 +203,13 @@ public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, str method = parameterTypes != null ? toType.GetMethod(toName, parameterTypes) : toType.GetMethod(toName); - if (method is null) - throw new InvalidOperationException($"Required method {toType.FullName}::{toName} could not be loaded."); } catch (Exception ex) { throw new InvalidOperationException($"Required method {toType.FullName}::{toName} could not be loaded.", ex); } + if (method is null) + throw new InvalidOperationException($"Required method {toType.FullName}::{toName} could not be found."); // add mapping return this.MapMember(fromFullName, method, "method"); diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 2920161c3..c6213bb8d 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -13,6 +13,9 @@ using StardewValley.BellsAndWhistles; using StardewValley.Buildings; using StardewValley.Enchantments; +using StardewValley.GameData; +using StardewValley.GameData.FloorsAndPaths; +using StardewValley.GameData.Movies; using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Mods; @@ -139,6 +142,15 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr .MapType("StardewValley.Network.NetAudio/SoundContext", typeof(SoundContext)) .MapType("StardewValley.PathFindController", typeof(PathFindController)) + // field renames + .MapFieldName(typeof(FloorPathData), "ID", nameof(FloorPathData.Id)) + .MapFieldName(typeof(ModFarmType), "ID", nameof(ModFarmType.Id)) + .MapFieldName(typeof(ModLanguage), "ID", nameof(ModLanguage.Id)) + .MapFieldName(typeof(ModWallpaperOrFlooring), "ID", nameof(ModWallpaperOrFlooring.Id)) + .MapFieldName(typeof(MovieData), "ID", nameof(MovieData.Id)) + .MapFieldName(typeof(MovieReaction), "ID", nameof(MovieReaction.Id)) + .MapFieldName(typeof(MovieScene), "ID", nameof(MovieScene.Id)) + // general API changes // note: types are mapped before members, regardless of the order listed here .MapType("StardewValley.Buildings.Mill", typeof(Building)) From 9cbfa598c26eb1841fea26d5b42876f45c4ab611 Mon Sep 17 00:00:00 2001 From: atravita-mods <94934860+atravita-mods@users.noreply.github.com> Date: Sat, 7 Oct 2023 10:05:18 -0400 Subject: [PATCH 47/84] Implement the InterpolatedStringHandler pattern for verbose logging --- .../Logging/VerboseLogStringHandler.cs | 50 +++++++++++++++++++ src/SMAPI/Framework/Monitor.cs | 10 +++- src/SMAPI/IMonitor.cs | 8 +++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs diff --git a/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs b/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs new file mode 100644 index 000000000..9a23c0648 --- /dev/null +++ b/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs @@ -0,0 +1,50 @@ +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.Logging; + +/// +/// An interpolated string handler to handle verbose logging. +/// +[InterpolatedStringHandler] +public ref struct VerboseLogStringHandler +{ + private readonly DefaultInterpolatedStringHandler _handler; + + /// + /// Construct an instance. + /// + /// The total length of literals used in interpolation. + /// The number of interpolation holes to fill. + /// The monitor instance. + /// Whether or not + public VerboseLogStringHandler(int literalLength, int formattedCount, IMonitor monitor, out bool isValid) + { + if (monitor.IsVerbose) + { + this._handler = new(literalLength, formattedCount); + isValid = true; + return; + } + + isValid = false; + this._handler = default; + } + + /// + public void AppendLiteral(string literal) + { + this._handler.AppendLiteral(literal); + } + + /// + public void AppendFormatted(T value) + { + this._handler.AppendFormatted(value); + } + + /// + public override string ToString() + { + return this._handler.ToStringAndClear(); + } +} diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index aaaafcbc9..32533cc3e 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; + using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Internal.ConsoleWriting; @@ -25,7 +27,7 @@ internal class Monitor : IMonitor private static readonly int MaxLevelLength = Enum.GetValues().Max(level => level.ToString().Length); /// The cached representation for each level when added to a log header. - private static readonly Dictionary LogStrings = Enum.GetValues().ToDictionary(level => level, level => level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength)); + private static readonly Dictionary LogStrings = Enum.GetValues().ToDictionary(level => level, level => level.ToString().ToUpperInvariant().PadRight(Monitor.MaxLevelLength)); /// A cache of messages that should only be logged once. private readonly HashSet LogOnceCache = new(); @@ -93,6 +95,12 @@ public void VerboseLog(string message) this.Log(message); } + public void VerboseLog([InterpolatedStringHandlerArgument("")] ref VerboseLogStringHandler message) + { + if (this.IsVerbose) + this.Log(message.ToString()); + } + /// Write a newline to the console and log file. internal void Newline() { diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs index c400a211b..2305f2213 100644 --- a/src/SMAPI/IMonitor.cs +++ b/src/SMAPI/IMonitor.cs @@ -1,3 +1,7 @@ +using System.Runtime.CompilerServices; + +using StardewModdingAPI.Framework.Logging; + namespace StardewModdingAPI { /// Encapsulates monitoring and logging for a given module. @@ -26,5 +30,9 @@ public interface IMonitor /// Log a message that only appears when is enabled. /// The message to log. void VerboseLog(string message); + + /// Log a message that only appears when is enabled. + /// The message to log. + void VerboseLog([InterpolatedStringHandlerArgument("")] ref VerboseLogStringHandler message); } } From 143333d0398de8b6d057a171ce87a9ef4bc1465e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 13:58:15 -0500 Subject: [PATCH 48/84] fix verbose log string handler not logging anything --- src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs b/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs index 9a23c0648..3608da311 100644 --- a/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs +++ b/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs @@ -8,7 +8,8 @@ namespace StardewModdingAPI.Framework.Logging; [InterpolatedStringHandler] public ref struct VerboseLogStringHandler { - private readonly DefaultInterpolatedStringHandler _handler; + /// The underlying interpolated string handler. + private DefaultInterpolatedStringHandler _handler; /// /// Construct an instance. From 48c1c211da87922b60f11d51b8192117ef52f548 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 13:59:44 -0500 Subject: [PATCH 49/84] minor code formatting & code docs --- .../Logging/VerboseLogStringHandler.cs | 79 +++++++++---------- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/IMonitor.cs | 1 - 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs b/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs index 3608da311..b69df6307 100644 --- a/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs +++ b/src/SMAPI/Framework/Logging/VerboseLogStringHandler.cs @@ -1,51 +1,50 @@ using System.Runtime.CompilerServices; -namespace StardewModdingAPI.Framework.Logging; - -/// -/// An interpolated string handler to handle verbose logging. -/// -[InterpolatedStringHandler] -public ref struct VerboseLogStringHandler +namespace StardewModdingAPI.Framework.Logging { - /// The underlying interpolated string handler. - private DefaultInterpolatedStringHandler _handler; - - /// - /// Construct an instance. - /// - /// The total length of literals used in interpolation. - /// The number of interpolation holes to fill. - /// The monitor instance. - /// Whether or not - public VerboseLogStringHandler(int literalLength, int formattedCount, IMonitor monitor, out bool isValid) + /// An interpolated string handler to handle verbose logging. + [InterpolatedStringHandler] + public ref struct VerboseLogStringHandler { - if (monitor.IsVerbose) + /********* + ** Fields + *********/ + /// The underlying interpolated string handler. + private DefaultInterpolatedStringHandler Handler; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The total length of literals used in interpolation. + /// The number of interpolation holes to fill. + /// The monitor instance. + /// Whether the handler can receive and output data. + public VerboseLogStringHandler(int literalLength, int formattedCount, IMonitor monitor, out bool isValid) { - this._handler = new(literalLength, formattedCount); - isValid = true; - return; - } + isValid = monitor.IsVerbose; - isValid = false; - this._handler = default; - } + if (isValid) + this.Handler = new(literalLength, formattedCount); + } - /// - public void AppendLiteral(string literal) - { - this._handler.AppendLiteral(literal); - } + /// + public void AppendLiteral(string literal) + { + this.Handler.AppendLiteral(literal); + } - /// - public void AppendFormatted(T value) - { - this._handler.AppendFormatted(value); - } + /// + public void AppendFormatted(T value) + { + this.Handler.AppendFormatted(value); + } - /// - public override string ToString() - { - return this._handler.ToStringAndClear(); + /// + public override string ToString() + { + return this.Handler.ToStringAndClear(); + } } } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 32533cc3e..543ac2d5a 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; - using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Internal.ConsoleWriting; @@ -95,6 +94,7 @@ public void VerboseLog(string message) this.Log(message); } + /// public void VerboseLog([InterpolatedStringHandlerArgument("")] ref VerboseLogStringHandler message) { if (this.IsVerbose) diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs index 2305f2213..e4a71436c 100644 --- a/src/SMAPI/IMonitor.cs +++ b/src/SMAPI/IMonitor.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; - using StardewModdingAPI.Framework.Logging; namespace StardewModdingAPI From a563fc7317c1a8ccc31b49e1934dd9ce4c128c29 Mon Sep 17 00:00:00 2001 From: SinZ Date: Sat, 28 Oct 2023 22:10:39 +1100 Subject: [PATCH 50/84] Uplift rewriters to handle inherited fields --- .../Finders/ReferenceToMissingMemberFinder.cs | 2 +- .../Rewriters/HeuristicFieldRewriter.cs | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index fae7fb125..f140c4fed 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -36,7 +36,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { FieldDefinition? target = fieldRef.Resolve(); - if (target == null || target.HasConstant) + if (target == null || target.HasConstant || target.DeclaringType.FullName != fieldRef.DeclaringType.FullName) { this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); return false; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index 9c6a39804..f1708c1d3 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -38,7 +38,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // skip if not broken FieldDefinition? fieldDefinition = fieldRef.Resolve(); - if (fieldDefinition?.HasConstant == false) + if (fieldDefinition?.HasConstant == false && fieldDefinition?.DeclaringType.FullName == fieldRef.DeclaringType.FullName) return false; // rewrite if possible @@ -46,7 +46,8 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio bool isRead = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld; return this.TryRewriteToProperty(module, instruction, fieldRef, declaringType, isRead) - || this.TryRewriteToConstField(instruction, fieldDefinition); + || this.TryRewriteToConstField(instruction, fieldDefinition) + || this.TryRewriteToInheritedField(module, instruction, fieldRef, fieldDefinition); } @@ -103,5 +104,32 @@ private bool TryRewriteToConstField(Instruction instruction, FieldDefinition? fi this.Phrases.Add($"{field.DeclaringType.Name}.{field.Name} (field => const)"); return this.MarkRewritten(); } + + /// Try rewriting the field into an inherited field. + /// + /// + /// + /// + /// + + private bool TryRewriteToInheritedField(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, FieldDefinition? fieldDefinition) + { + // If its not resolvable, don't rewrite + if (fieldDefinition == null) + return false; + // If same they don't need rewriting + if (fieldRef.DeclaringType.FullName == fieldDefinition.DeclaringType.FullName) + return false; + // If static, it is less intuitive that rewriting should happen + if (instruction.OpCode != OpCodes.Ldfld) + return false; + + instruction.Operand = module.ImportReference(fieldDefinition); + + fieldRef.FieldType = fieldDefinition.FieldType; + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} -> {fieldDefinition.DeclaringType.Name}.{fieldRef.Name} (field => inherited field)"); + + return this.MarkRewritten(); + } } } From f58068a6d5596d90d5a7d0049ccef494e2aa2b36 Mon Sep 17 00:00:00 2001 From: SinZ Date: Sat, 28 Oct 2023 22:35:13 +1100 Subject: [PATCH 51/84] FullName can't be fairly compared due to generics --- .../ModLoading/Finders/ReferenceToMissingMemberFinder.cs | 2 +- .../Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index f140c4fed..ba4ec83f6 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -36,7 +36,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { FieldDefinition? target = fieldRef.Resolve(); - if (target == null || target.HasConstant || target.DeclaringType.FullName != fieldRef.DeclaringType.FullName) + if (target == null || target.HasConstant || target.DeclaringType.Namespace != fieldRef.DeclaringType.Namespace || target.DeclaringType.Name != fieldRef.DeclaringType.Name) { this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); return false; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index f1708c1d3..0bb9a23e5 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -117,8 +117,9 @@ private bool TryRewriteToInheritedField(ModuleDefinition module, Instruction ins // If its not resolvable, don't rewrite if (fieldDefinition == null) return false; - // If same they don't need rewriting - if (fieldRef.DeclaringType.FullName == fieldDefinition.DeclaringType.FullName) + // If same they don't need rewriting, avoidingFullName as fieldRef and fieldDefinition resolve generics differently + if (fieldRef.DeclaringType.Namespace == fieldDefinition.DeclaringType.Namespace && + fieldRef.DeclaringType.Name == fieldDefinition.DeclaringType.Name) return false; // If static, it is less intuitive that rewriting should happen if (instruction.OpCode != OpCodes.Ldfld) From 87174ed7e6d9a2ca032f3e04f5abe12936c7781a Mon Sep 17 00:00:00 2001 From: SinZ Date: Sun, 29 Oct 2023 16:44:24 +1100 Subject: [PATCH 52/84] Allow ReplaceReferencesRewriter to rewrite methods on Generic classes --- .../ModLoading/Framework/RewriteHelper.cs | 15 +++++++++ .../Rewriters/ReplaceReferencesRewriter.cs | 26 +++++++++++++++- .../StardewValley_1_6/NetLongFacade.cs | 31 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 5f93510a4..d903330fb 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -97,6 +97,21 @@ public static string GetFullCecilName(Type type) return $"{type.Namespace}.{type.Name}"; } + public static Type? GetCSharpType(TypeReference type) + { + string typeName = GetReflectionName(type); + return Type.GetType(typeName, false); + } + + public static string GetReflectionName(TypeReference type) + { + if (type.IsGenericInstance) + { + var genericInstance = (GenericInstanceType)type; + return string.Format("{0}.{1}[{2}],{3}", genericInstance.Namespace, type.Name, string.Join(",", genericInstance.GenericArguments.Select(row => "[" + GetReflectionName(row) + "]").ToArray()), type.Scope.Name); + } + return type.FullName + "," + type.Scope.Name; + } /// Get whether a type matches a type reference. /// The defined type. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index b3ba6e6d6..b207d6d2e 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -306,8 +306,22 @@ public override bool Handle(ModuleDefinition module, TypeReference type, Action< /// public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { - if (instruction.Operand is not MemberReference fromMember || !this.MemberMap.TryGetValue(fromMember.FullName, out MemberInfo? toMember)) + if (instruction.Operand is not MemberReference fromMember) return false; + if (!this.MemberMap.TryGetValue(fromMember.FullName, out MemberInfo? toMember)) + { + if (fromMember.DeclaringType is GenericInstanceType genericType) + { + if (!this.MemberMap.TryGetValue(fromMember.DeclaringType.GetElementType().FullName + "::" + fromMember.Name, out toMember)) { + + return false; + } + } + else + { + return false; + } + } switch (toMember) { @@ -318,6 +332,16 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // method case MethodInfo toMethod: + if (toMethod.DeclaringType!.IsGenericTypeDefinition && fromMember.DeclaringType is GenericInstanceType generic) + { + Type[] arguments = generic.GenericArguments.Select(RewriteHelper.GetCSharpType).ToArray()!; + foreach (var argument in arguments) + { + if (argument == null) return false; + } + toMethod = toMethod.DeclaringType?.MakeGenericType(arguments)?.GetMethod(toMethod.Name); + if (toMethod == null) return false; + } instruction.Operand = module.ImportReference(toMethod); if (instruction.OpCode == OpCodes.Newobj) // rewriting constructor to static method diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs new file mode 100644 index 000000000..3057e2db4 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class NetLongFacade : NetFieldBase + { + /********* + ** Public methods + *********/ + public static long op_Implicit(NetLong netField) + { + return netField.Value; + } + + + /********* + ** Private methods + *********/ + private NetLongFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} From eabe8f0a81cbce4af892d87241fd7d91eeb00d03 Mon Sep 17 00:00:00 2001 From: SinZ Date: Sat, 11 Nov 2023 13:07:56 +1100 Subject: [PATCH 53/84] Include the MapMethod changes --- src/SMAPI/Metadata/InstructionMetadata.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index c6213bb8d..284c8a444 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -222,22 +222,8 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr .MapMethod($"{typeof(OverlaidDictionaryFacade).FullName}/{nameof(OverlaidDictionaryFacade.ValuesCollection)} StardewValley.Network.OverlaidDictionary::get_Values()", typeof(OverlaidDictionaryFacade), $"get_{nameof(OverlaidDictionaryFacade.Values)}") // implicit NetField conversions removed - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2>::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade>), nameof(NetFieldBaseFacade>.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) - .MapMethod("!0 Netcode.NetFieldBase`2::op_Implicit(Netcode.NetFieldBase`2)", typeof(NetFieldBaseFacade), nameof(NetFieldBaseFacade.op_Implicit)) + .MapMethod("Netcode.NetFieldBase`2::op_Implicit", typeof(NetFieldBaseFacade<,>), "op_Implicit") + .MapMethod("System.Int64 Netcode.NetLong::op_Implicit(Netcode.NetLong)", typeof(NetLongFacade), nameof(NetLongFacade.op_Implicit)) .MapMethod("!0 StardewValley.Network.NetPausableField`3::op_Implicit(StardewValley.Network.NetPausableField`3)", typeof(NetPausableFieldFacade), nameof(NetPausableFieldFacade.op_Implicit)); From b5867dba0d3114ee51bb1a6f33e2440d4ec345c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 15:06:16 -0500 Subject: [PATCH 54/84] encapsulate type name/namespace check, minor code/doc tweaks --- .../Finders/ReferenceToMissingMemberFinder.cs | 2 +- .../ModLoading/Framework/RewriteHelper.cs | 11 ++++++++ .../Rewriters/HeuristicFieldRewriter.cs | 27 +++++++++---------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index ba4ec83f6..b54d57c4b 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -36,7 +36,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { FieldDefinition? target = fieldRef.Resolve(); - if (target == null || target.HasConstant || target.DeclaringType.Namespace != fieldRef.DeclaringType.Namespace || target.DeclaringType.Name != fieldRef.DeclaringType.Name) + if (target == null || target.HasConstant || !RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, target.DeclaringType)) { this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); return false; diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 5f93510a4..550e26fed 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -173,6 +173,17 @@ public static bool LooksLikeSameType(TypeReference? typeA, TypeReference? typeB) return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB); } + /// Get whether a type reference and definition have the same namespace and name. This does not guarantee they point to the same type due to generics. + /// The type reference. + /// The type definition. + /// This avoids an issue where we can't compare to because of the different ways they handle generics (e.g. List`1<System.String> vs List`1). + public static bool HasSameNamespaceAndName(TypeReference? typeReference, TypeDefinition? typeDefinition) + { + return + typeReference?.Namespace == typeDefinition?.Namespace + && typeReference?.Name == typeDefinition?.Name; + } + /// Get whether a method definition matches the signature expected by a method reference. /// The method definition. /// The method reference. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index 0bb9a23e5..92db7a550 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -105,31 +105,30 @@ private bool TryRewriteToConstField(Instruction instruction, FieldDefinition? fi return this.MarkRewritten(); } - /// Try rewriting the field into an inherited field. - /// - /// - /// - /// - /// - + /// Try rewriting the field into a matching inherited field. + /// The assembly module containing the instruction. + /// The CIL instruction to rewrite. + /// The field reference. + /// The actual field resolved by Cecil. private bool TryRewriteToInheritedField(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, FieldDefinition? fieldDefinition) { - // If its not resolvable, don't rewrite + // skip if not resolvable if (fieldDefinition == null) return false; - // If same they don't need rewriting, avoidingFullName as fieldRef and fieldDefinition resolve generics differently - if (fieldRef.DeclaringType.Namespace == fieldDefinition.DeclaringType.Namespace && - fieldRef.DeclaringType.Name == fieldDefinition.DeclaringType.Name) + + // skip if no rewrite needed + if (RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, fieldDefinition.DeclaringType)) return false; - // If static, it is less intuitive that rewriting should happen + + // skip if static (it's less intuitive that rewriting should happen) if (instruction.OpCode != OpCodes.Ldfld) return false; + // rewrite reference instruction.Operand = module.ImportReference(fieldDefinition); - fieldRef.FieldType = fieldDefinition.FieldType; - this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} -> {fieldDefinition.DeclaringType.Name}.{fieldRef.Name} (field => inherited field)"); + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} -> {fieldDefinition.DeclaringType.Name}.{fieldRef.Name} (field => inherited field)"); return this.MarkRewritten(); } } From 5e0ea7c604b5a11b4efdf9323e09af10193dec8f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 15:06:16 -0500 Subject: [PATCH 55/84] fix a FullName compare in HeuristicFieldRewriter --- .../Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index 92db7a550..015544193 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -38,7 +38,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // skip if not broken FieldDefinition? fieldDefinition = fieldRef.Resolve(); - if (fieldDefinition?.HasConstant == false && fieldDefinition?.DeclaringType.FullName == fieldRef.DeclaringType.FullName) + if (fieldDefinition?.HasConstant == false && RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, fieldDefinition.DeclaringType)) return false; // rewrite if possible From 45c3b19fa9a710561247a259fbb309972b06ee4e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 15:06:16 -0500 Subject: [PATCH 56/84] simplify log for rewritten inherited field --- .../Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index 015544193..26d942df4 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -128,7 +128,7 @@ private bool TryRewriteToInheritedField(ModuleDefinition module, Instruction ins instruction.Operand = module.ImportReference(fieldDefinition); fieldRef.FieldType = fieldDefinition.FieldType; - this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} -> {fieldDefinition.DeclaringType.Name}.{fieldRef.Name} (field => inherited field)"); + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} -> {fieldDefinition.DeclaringType.Name}.{fieldRef.Name} (field now inherited)"); return this.MarkRewritten(); } } From c1e41aabdc7823bb7ce42c13c108f9aa53033433 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 16:40:35 -0500 Subject: [PATCH 57/84] refactor new rewrite logic a bit, add code docs --- .../ModLoading/Framework/RewriteHelper.cs | 19 +++++--- .../Rewriters/ReplaceReferencesRewriter.cs | 44 ++++++++++++------- .../StardewValley_1_6/NetFieldBaseFacade.cs | 2 +- .../StardewValley_1_6/NetLongFacade.cs | 2 +- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index d903330fb..7a44e02cd 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -97,20 +97,25 @@ public static string GetFullCecilName(Type type) return $"{type.Namespace}.{type.Name}"; } + + /// Get the resolved type for a Cecil type reference. + /// The type reference. public static Type? GetCSharpType(TypeReference type) { - string typeName = GetReflectionName(type); + string typeName = RewriteHelper.GetReflectionName(type); return Type.GetType(typeName, false); } + /// Get the .NET reflection full name for a Cecil type reference. + /// The type reference. public static string GetReflectionName(TypeReference type) { - if (type.IsGenericInstance) - { - var genericInstance = (GenericInstanceType)type; - return string.Format("{0}.{1}[{2}],{3}", genericInstance.Namespace, type.Name, string.Join(",", genericInstance.GenericArguments.Select(row => "[" + GetReflectionName(row) + "]").ToArray()), type.Scope.Name); - } - return type.FullName + "," + type.Scope.Name; + if (!type.IsGenericInstance) + return $"{type.FullName},{type.Scope.Name}"; + + var genericInstance = (GenericInstanceType) type; + var genericArgs = genericInstance.GenericArguments.Select(row => "[" + RewriteHelper.GetReflectionName(row) + "]"); + return $"{genericInstance.Namespace}.{type.Name}[{string.Join(",", genericArgs)}],{type.Scope.Name}"; } /// Get whether a type matches a type reference. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index b207d6d2e..30ebc9919 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -308,21 +308,25 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio { if (instruction.Operand is not MemberReference fromMember) return false; + + // get target member if (!this.MemberMap.TryGetValue(fromMember.FullName, out MemberInfo? toMember)) { - if (fromMember.DeclaringType is GenericInstanceType genericType) - { - if (!this.MemberMap.TryGetValue(fromMember.DeclaringType.GetElementType().FullName + "::" + fromMember.Name, out toMember)) { - - return false; - } - } - else - { + // If this is a generic type, there's two cases where the above might not match: + // 1. we mapped an open generic type like "Netcode.NetFieldBase`2::op_Implicit" without specific + // generic types; + // 2. or due to Cecil's odd generic type handling, which can result in type names like + // "Netcode.NetFieldBase`2". + // + // In either case, we can check for a mapping registered using the simple generic name like + // "Netcode.NetFieldBase`2" (without type args) by using `GetElementType().FullName` instead. + if (fromMember.DeclaringType is not GenericInstanceType) + return false; + if (!this.MemberMap.TryGetValue($"{fromMember.DeclaringType.GetElementType().FullName}::{fromMember.Name}", out toMember)) return false; - } } + // apply switch (toMember) { // constructor @@ -332,16 +336,24 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // method case MethodInfo toMethod: - if (toMethod.DeclaringType!.IsGenericTypeDefinition && fromMember.DeclaringType is GenericInstanceType generic) + // resolve generic method to a specific implementation + if (toMethod.DeclaringType?.IsGenericTypeDefinition is true && fromMember.DeclaringType is GenericInstanceType generic) { - Type[] arguments = generic.GenericArguments.Select(RewriteHelper.GetCSharpType).ToArray()!; - foreach (var argument in arguments) + Type?[] arguments = generic.GenericArguments.Select(RewriteHelper.GetCSharpType).ToArray(); + foreach (Type? argument in arguments) { - if (argument == null) return false; + if (argument is null) + return false; } - toMethod = toMethod.DeclaringType?.MakeGenericType(arguments)?.GetMethod(toMethod.Name); - if (toMethod == null) return false; + + MethodInfo? newMethod = toMethod.DeclaringType.MakeGenericType(arguments!)?.GetMethod(toMethod.Name); + if (newMethod is null) + return false; + + toMethod = newMethod; } + + // rewrite instruction.Operand = module.ImportReference(toMethod); if (instruction.OpCode == OpCodes.Newobj) // rewriting constructor to static method diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs index 7f2010a4d..afb67157f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldBaseFacade.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public abstract class NetFieldBaseFacade : NetFieldBase, IRewriteFacade + public abstract class NetFieldBaseFacade : NetFieldBase // don't mark IRewriteFacade; the op_Implicit method is mapped manually where TSelf : NetFieldBase { /********* diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs index 3057e2db4..60298e875 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetLongFacade.cs @@ -9,7 +9,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public abstract class NetLongFacade : NetFieldBase + public abstract class NetLongFacade : NetFieldBase // don't mark IRewriteFacade; the op_Implicit method is mapped manually { /********* ** Public methods From 1a6e25ecf8c5c4e32a7647a71eb4b8dda946cf20 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Dec 2023 16:44:27 -0500 Subject: [PATCH 58/84] update release notes --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index af573a2b9..5bd81a0ba 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * Updated for Stardew Valley 1.6. * Improved performance. + * Improved compatibility rewriting to handle more cases (thanks to SinZ!). * Removed the bundled `ErrorHandler` mod (now integrated into Stardew Valley 1.6). * Removed obsolete console commands: `list_item_types` (no longer needed) and `player_setimmunity` (broke in 1.6 and rarely used). * Removed support for seamlessly updating from SMAPI 2.11.3 and earlier (released in 2019). @@ -15,6 +16,7 @@ * Added `RenderingStep` and `RenderedStep` events, which let you handle a specific step in the game's render cycle. * Removed all deprecated APIs. * SMAPI no longer intercepts output written to the console. Mods which directly access `Console` will be listed under mod warnings. + * Calling `Monitor.VerboseLog` with an interpolated string no longer evaluates the string if verbose mode is disabled (thanks to atravita!). This only applies to mods compiled in SMAPI 4.0.0 or later. ## Upcoming release * For players: From 6101146cecf4057a7567c6b2ed3aff554e60787e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Dec 2023 13:02:09 -0500 Subject: [PATCH 59/84] fix in-place texture asset propagation for non-English players --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 59 +++++++++++++++-------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 794893138..3ae023d99 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -142,40 +142,59 @@ public void Propagate(IList contentManagers, IDictionary contentManagers, bool ignoreWorld) { - /**** - ** Update textures in-place - ****/ - assetName = assetName.GetBaseAssetName(); bool changed = false; + + // get asset names to replace + // We propagate non-textures by comparing base asset names, to update any localized version like + // `asset.fr-FR` too. We need to check every content manager for in-place texture edits though, so we + // should avoid iterating their assets if possible. So here we just check for the current localized name + // and base name, which should cover normal cases. + IAssetName[] assetNames = assetName.LocaleCode != null + ? new[] { assetName, assetName.GetBaseAssetName() } + : new[] { assetName }; + + // update textures in-place { - Lazy newTexture = new(() => this.DisposableContentManager.LoadLocalized(assetName, language, useCache: false)); + // get new textures to copy + Lazy[] newTextures = new Lazy[assetNames.Length]; + newTextures[0] = new Lazy(() => this.DisposableContentManager.LoadLocalized(assetName, language, useCache: false)); + if (assetNames.Length > 1) + newTextures[1] = new Lazy(() => this.DisposableContentManager.LoadLocalized(assetNames[1], language, useCache: false)); + // apply to content managers foreach (IContentManager contentManager in contentManagers) { - if (contentManager.IsLoaded(assetName)) + for (int i = 0; i < assetNames.Length; i++) { - if (this.DisposableContentManager.DoesAssetExist(assetName)) - { - changed = true; + IAssetName name = assetNames[i]; - Texture2D texture = contentManager.LoadLocalized(assetName, language, useCache: true); - texture.CopyFromTexture(newTexture.Value); - } - else + if (contentManager.IsLoaded(name)) { - this.Monitor.Log($"Skipped reload for '{assetName.Name}' because the underlying asset no longer exists.", LogLevel.Warn); - break; + if (this.DisposableContentManager.DoesAssetExist(name)) + { + changed = true; + + Texture2D texture = contentManager.LoadLocalized(name, language, useCache: true); + texture.CopyFromTexture(newTextures[i].Value); + } + else + { + this.Monitor.Log($"Skipped reload for '{name.Name}' because the underlying asset no longer exists.", LogLevel.Warn); + break; + } } } } - if (newTexture.IsValueCreated) - newTexture.Value.Dispose(); + // drop temporary textures + foreach (Lazy newTexture in newTextures) + { + if (newTexture.IsValueCreated) + newTexture.Value.Dispose(); + } } - /**** - ** Update game state if needed - ****/ + // update game state if needed if (changed) { switch (assetName.Name.ToLower().Replace("\\", "/")) // normalized key so we can compare statically From 162f14dfe2facdfe27492a2a8fb0e124d0371f28 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jan 2024 18:49:42 -0500 Subject: [PATCH 60/84] add basic update-check stats tracking & API --- docs/technical/web.md | 10 ++- .../Controllers/ModsApiController.cs | 43 +++++---- .../Framework/Metrics/ApiMetricsModel.cs | 65 ++++++++++++++ .../Framework/Metrics/MetricsManager.cs | 88 +++++++++++++++++++ .../Framework/Metrics/MetricsModel.cs | 68 ++++++++++++++ .../Framework/Metrics/MetricsSummary.cs | 30 +++++++ 6 files changed, 288 insertions(+), 16 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Metrics/ApiMetricsModel.cs create mode 100644 src/SMAPI.Web/Framework/Metrics/MetricsManager.cs create mode 100644 src/SMAPI.Web/Framework/Metrics/MetricsModel.cs create mode 100644 src/SMAPI.Web/Framework/Metrics/MetricsSummary.cs diff --git a/docs/technical/web.md b/docs/technical/web.md index f0d43fb14..fefe15353 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -275,7 +275,6 @@ field | summary `brokeIn` | The SMAPI or Stardew Valley version that broke this mod, if any. `betaCompatibilityStatus`
    `betaCompatibilitySummary`
    `betaBrokeIn` | Equivalent to the preceding fields, but for beta versions of SMAPI or Stardew Valley. - @@ -324,6 +323,15 @@ Example response with `includeExtendedMetadata: true`: ] ``` +### `/mods/metrics` +The `/mods/metrics` endpoint returns a summary of update-check metrics since the server was last +deployed or restarted. + +Example request: +```js +GET https://smapi.io/api/v3.0/mods/metrics +``` + ## Short URLs The SMAPI web services provides a few short URLs for convenience: diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index f687c7dd2..f56c1102f 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -24,6 +24,7 @@ using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.UpdateManifest; using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.Metrics; namespace StardewModdingAPI.Web.Controllers { @@ -81,6 +82,9 @@ public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository w [HttpPost] public async Task> PostAsync([FromBody] ModSearchModel? model, [FromRoute] string version) { + ApiMetricsModel metrics = MetricsManager.GetMetricsForNow(); + metrics.TrackRequest(); + if (model?.Mods == null) return Array.Empty(); @@ -99,7 +103,7 @@ public async Task> PostAsync([FromBody] ModSearchMode mod.AddUpdateKeys(config.SmapiInfo.AddBetaUpdateKeys); // fetch result - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion); + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion, metrics); if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) { result.Errors = result.Errors @@ -114,6 +118,13 @@ public async Task> PostAsync([FromBody] ModSearchMode return mods.Values; } + /// Fetch a summary of update-check metrics since the server was last deployed or restarted. + [HttpGet("metrics")] + public MetricsSummary GetMetrics() + { + return MetricsManager.GetSummary(this.Config.Value); + } + /********* ** Private methods @@ -123,8 +134,9 @@ public async Task> PostAsync([FromBody] ModSearchMode /// The wiki data. /// Whether to include extended metadata for each mod. /// The SMAPI version installed by the player. + /// The metrics to update with update-check results. /// Returns the mod data if found, else null. - private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion? apiVersion) + private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion? apiVersion, ApiMetricsModel metrics) { // cross-reference data ModDataRecord? record = this.ModDatabase.Get(search.ID); @@ -159,7 +171,7 @@ private async Task GetModData(ModSearchEntryModel search, WikiMod } // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.Overrides?.ChangeRemoteVersions); + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.Overrides?.ChangeRemoteVersions, metrics); if (data.Status != RemoteModStatus.Ok) { errors.Add(data.Error ?? data.Status.ToString()); @@ -284,27 +296,28 @@ private bool IsNewer([NotNullWhen(true)] ISemanticVersion? current, ISemanticVer /// The namespaced update key. /// Whether to allow non-standard versions. /// The changes to apply to remote versions for update checks. - private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) + /// The metrics to update with update-check results. + private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, ApiMetricsModel metrics) { if (!updateKey.LooksValid) return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"Invalid update key '{updateKey}'."); // get mod page + bool wasCached = + this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached? cachedMod) + && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes); IModPage page; + if (wasCached) + page = cachedMod!.Data; + else { - bool isCached = - this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached? cachedMod) - && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes); - - if (isCached) - page = cachedMod!.Data; - else - { - page = await this.ModSites.GetModPageAsync(updateKey); - this.ModCache.SaveMod(updateKey.Site, updateKey.ID, page); - } + page = await this.ModSites.GetModPageAsync(updateKey); + this.ModCache.SaveMod(updateKey.Site, updateKey.ID, page); } + // update metrics + metrics.TrackUpdateKey(updateKey, wasCached, page.IsValid); + // get version info return this.ModSites.GetPageVersions(page, updateKey, allowNonStandardVersions, mapRemoteVersions); } diff --git a/src/SMAPI.Web/Framework/Metrics/ApiMetricsModel.cs b/src/SMAPI.Web/Framework/Metrics/ApiMetricsModel.cs new file mode 100644 index 000000000..e3d68c806 --- /dev/null +++ b/src/SMAPI.Web/Framework/Metrics/ApiMetricsModel.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Metrics +{ + /// The metrics tracked for a specific block of time. + internal class ApiMetricsModel + { + /********* + ** Accessors + *********/ + /// The total number of update-check requests received by the API (each of which may include multiple update keys). + public int ApiRequests { get; private set; } + + /// The metrics by mod site. + public Dictionary Sites { get; } = new(); + + + /********* + ** Public methods + *********/ + /// Track an update-check request received by the API. + public void TrackRequest() + { + this.ApiRequests++; + } + + /// Track the update-check result for a specific update key. + /// The update key that was requested. + /// Whether the data was returned from the cache; else it was fetched from the remote modding site. + /// Whether the data was fetched successfully from the remote modding site. + public void TrackUpdateKey(UpdateKey updateKey, bool wasCached, bool wasSuccessful) + { + MetricsModel siteMetrics = this.GetSiteMetrics(updateKey.Site); + siteMetrics.TrackUpdateKey(updateKey, wasCached, wasSuccessful); + } + + /// Merge the values from another metrics model into this one. + /// The metrics to merge into this model. + public void AggregateFrom(ApiMetricsModel other) + { + this.ApiRequests += other.ApiRequests; + + foreach ((ModSiteKey site, var otherSiteMetrics) in other.Sites) + { + var siteMetrics = this.GetSiteMetrics(site); + siteMetrics.AggregateFrom(otherSiteMetrics); + } + } + + + /********* + ** Private methods + *********/ + /// Get the metrics for a site key, adding it if needed. + /// The mod site key. + private MetricsModel GetSiteMetrics(ModSiteKey site) + { + if (!this.Sites.TryGetValue(site, out MetricsModel? siteMetrics)) + this.Sites[site] = siteMetrics = new MetricsModel(); + + return siteMetrics; + } + } +} diff --git a/src/SMAPI.Web/Framework/Metrics/MetricsManager.cs b/src/SMAPI.Web/Framework/Metrics/MetricsManager.cs new file mode 100644 index 000000000..0189e12e1 --- /dev/null +++ b/src/SMAPI.Web/Framework/Metrics/MetricsManager.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.ConfigModels; + +namespace StardewModdingAPI.Web.Framework.Metrics +{ + /// Manages in-memory update check metrics since the server was last deployed or restarted. + internal static class MetricsManager + { + /********* + ** Fields + *********/ + /// The date/time format used to generate hourly metrics keys. + private const string HourlyKeyFormat = "yyyy-MM-dd HH:*"; + + /// The length of the date-only prefix in . + private const int DateOnlyKeyLength = 10; + + /// The tracked metrics. + private static readonly IDictionary Metrics = new Dictionary(); + + /// When the server began tracking metrics. + private static readonly DateTimeOffset MetricsTrackedSince = DateTimeOffset.UtcNow; + + + /********* + ** Public methods + *********/ + /// Get the metrics model for the current hour. + public static ApiMetricsModel GetMetricsForNow() + { + string key = $"{DateTimeOffset.UtcNow.ToString(HourlyKeyFormat)}"; + + if (!MetricsManager.Metrics.TryGetValue(key, out ApiMetricsModel? metrics)) + MetricsManager.Metrics[key] = metrics = new ApiMetricsModel(); + + return metrics; + } + + /// Get a summary of the metrics collected since the server started. + /// The update-check settings. + public static MetricsSummary GetSummary(ModUpdateCheckConfig config) + { + // get aggregate stats + int totalRequests = 0; + var totals = new MetricsModel(); + var bySite = new Dictionary(); + var byDate = new Dictionary(); + foreach ((string hourlyKey, ApiMetricsModel hourly) in MetricsManager.Metrics) + { + // totals + totalRequests += hourly.ApiRequests; + foreach (MetricsModel site in hourly.Sites.Values) + totals.AggregateFrom(site); + + // by site + foreach ((ModSiteKey site, MetricsModel fromMetrics) in hourly.Sites) + { + if (!bySite.TryGetValue(site, out MetricsModel? metrics)) + bySite[site] = metrics = new MetricsModel(); + + metrics.AggregateFrom(fromMetrics); + } + + // by date + string dailyKey = hourlyKey[..MetricsManager.DateOnlyKeyLength]; + if (!byDate.TryGetValue(dailyKey, out ApiMetricsModel? daily)) + byDate[dailyKey] = daily = new ApiMetricsModel(); + + daily.AggregateFrom(hourly); + } + + return new MetricsSummary( + Uptime: DateTimeOffset.UtcNow - MetricsTrackedSince, + SuccessCacheMinutes: config.SuccessCacheMinutes, + ErrorCacheMinutes: config.ErrorCacheMinutes, + TotalApiRequests: totalRequests, + UniqueModsChecked: totals.UniqueModsChecked, + TotalCacheHits: totals.CacheHits, + TotalSuccessCacheMisses: totals.SuccessCacheMisses, + TotalErrorCacheMisses: totals.ErrorCacheMisses, + BySite: bySite, + ByDate: byDate + ); + } + } +} diff --git a/src/SMAPI.Web/Framework/Metrics/MetricsModel.cs b/src/SMAPI.Web/Framework/Metrics/MetricsModel.cs new file mode 100644 index 000000000..4b94e1745 --- /dev/null +++ b/src/SMAPI.Web/Framework/Metrics/MetricsModel.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Metrics +{ + /// The metrics for a specific site. + internal class MetricsModel + { + /********* + ** Accessors + *********/ + /// The number of times an update key returned data from the cache. + public int CacheHits { get; private set; } + + /// The number of times an update key successfully fetched data from the remote mod site. + public int SuccessCacheMisses { get; private set; } + + /// The number of times an update key could not fetch data from the remote mod site (e.g. mod page didn't exist or mod site returned an API error). + public int ErrorCacheMisses { get; private set; } + + /// The unique mod IDs requested from each site. + [JsonIgnore] + public HashSet UniqueKeys { get; } = new(); + + /// The number of unique mod IDs requested from each site. + public int UniqueModsChecked => this.UniqueKeys.Count; + + + /********* + ** Public methods + *********/ + /// Track the update-check result for a specific update key. + /// The update key that was requested. + /// Whether the data was returned from the cache; else it was fetched from the remote modding site. + /// Whether the data was fetched successfully from the remote modding site. + public void TrackUpdateKey(UpdateKey updateKey, bool wasCached, bool wasSuccessful) + { + // normalize site key + ModSiteKey site = updateKey.Site; + if (!Enum.IsDefined(site)) + site = ModSiteKey.Unknown; + + // update metrics + if (wasCached) + this.CacheHits++; + else if (wasSuccessful) + this.SuccessCacheMisses++; + else + this.ErrorCacheMisses++; + + this.UniqueKeys.Add(updateKey.ID?.Trim()); + } + + /// Merge the values from another metrics model into this one. + /// The metrics to merge into this model. + public void AggregateFrom(MetricsModel other) + { + this.CacheHits += other.CacheHits; + this.SuccessCacheMisses += other.SuccessCacheMisses; + this.ErrorCacheMisses += other.ErrorCacheMisses; + + foreach (string? id in other.UniqueKeys) + this.UniqueKeys.Add(id); + } + } +} diff --git a/src/SMAPI.Web/Framework/Metrics/MetricsSummary.cs b/src/SMAPI.Web/Framework/Metrics/MetricsSummary.cs new file mode 100644 index 000000000..847b88049 --- /dev/null +++ b/src/SMAPI.Web/Framework/Metrics/MetricsSummary.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Metrics +{ + /// An aggregate summary of tracked metrics. + /// The total time since the server began tracking metrics. + /// The number of minutes for which a successful data fetch from a remote mod site is cached. + /// The number of minutes for which a failed data fetch from a remote mod site is cached. + /// The total number of update-check requests received by the API (each of which may include multiple update keys). + /// The number of unique mod IDs requested. + /// The number of times an update key returned data from the cache. + /// The number of times an update key successfully fetched data from a remote mod site. + /// The number of times an update key could not fetch data from a remote mod site (e.g. mod page didn't exist or mod site returned an API error). + /// The metrics grouped by site. + /// The metrics grouped by UTC date. + internal record MetricsSummary( + TimeSpan Uptime, + int SuccessCacheMinutes, + int ErrorCacheMinutes, + int TotalApiRequests, + int UniqueModsChecked, + int TotalCacheHits, + int TotalSuccessCacheMisses, + int TotalErrorCacheMisses, + IDictionary BySite, + IDictionary ByDate + ); +} From f58bcb8fb4eb4a0cbfd1085baf400d922895ca9c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Jan 2024 23:57:07 -0500 Subject: [PATCH 61/84] remove last Harmony patches --- src/SMAPI.Internal.Patching/BasePatcher.cs | 54 ------------- src/SMAPI.Internal.Patching/HarmonyPatcher.cs | 36 --------- src/SMAPI.Internal.Patching/IPatcher.cs | 16 ---- src/SMAPI.Internal.Patching/PatchHelper.cs | 77 ------------------ .../SMAPI.Internal.Patching.projitems | 17 ---- .../SMAPI.Internal.Patching.shproj | 13 --- src/SMAPI.sln | 5 -- src/SMAPI/Framework/SCore.cs | 12 +-- src/SMAPI/Framework/SGame.cs | 32 +++++++- src/SMAPI/Framework/SGameRunner.cs | 9 ++- src/SMAPI/Patches/Game1Patcher.cs | 80 ------------------- src/SMAPI/Patches/TitleMenuPatcher.cs | 55 ------------- src/SMAPI/SMAPI.csproj | 1 - 13 files changed, 43 insertions(+), 364 deletions(-) delete mode 100644 src/SMAPI.Internal.Patching/BasePatcher.cs delete mode 100644 src/SMAPI.Internal.Patching/HarmonyPatcher.cs delete mode 100644 src/SMAPI.Internal.Patching/IPatcher.cs delete mode 100644 src/SMAPI.Internal.Patching/PatchHelper.cs delete mode 100644 src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems delete mode 100644 src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj delete mode 100644 src/SMAPI/Patches/Game1Patcher.cs delete mode 100644 src/SMAPI/Patches/TitleMenuPatcher.cs diff --git a/src/SMAPI.Internal.Patching/BasePatcher.cs b/src/SMAPI.Internal.Patching/BasePatcher.cs deleted file mode 100644 index c1936ccc1..000000000 --- a/src/SMAPI.Internal.Patching/BasePatcher.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Reflection; -using HarmonyLib; - -namespace StardewModdingAPI.Internal.Patching -{ - /// Provides base implementation logic for instances. - internal abstract class BasePatcher : IPatcher - { - /********* - ** Public methods - *********/ - /// - public abstract void Apply(Harmony harmony, IMonitor monitor); - - - /********* - ** Protected methods - *********/ - /// Get a method and assert that it was found. - /// The type containing the method. - /// The method parameter types, or null if it's not overloaded. - protected ConstructorInfo RequireConstructor(params Type[] parameters) - { - return PatchHelper.RequireConstructor(parameters); - } - - /// Get a method and assert that it was found. - /// The type containing the method. - /// The method name. - /// The method parameter types, or null if it's not overloaded. - /// The method generic types, or null if it's not generic. - protected MethodInfo RequireMethod(string name, Type[]? parameters = null, Type[]? generics = null) - { - return PatchHelper.RequireMethod(name, parameters, generics); - } - - /// Get a Harmony patch method on the current patcher instance. - /// The method name. - /// The patch priority to apply, usually specified using Harmony's enum, or null to keep the default value. - protected HarmonyMethod GetHarmonyMethod(string name, int? priority = null) - { - HarmonyMethod method = new( - AccessTools.Method(this.GetType(), name) - ?? throw new InvalidOperationException($"Can't find patcher method {PatchHelper.GetMethodString(this.GetType(), name)}.") - ); - - if (priority.HasValue) - method.priority = priority.Value; - - return method; - } - } -} diff --git a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs deleted file mode 100644 index 6f30c241a..000000000 --- a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using HarmonyLib; - -namespace StardewModdingAPI.Internal.Patching -{ - /// Simplifies applying instances to the game. - internal static class HarmonyPatcher - { - /********* - ** Public methods - *********/ - /// Apply the given Harmony patchers. - /// The mod ID applying the patchers. - /// The monitor with which to log any errors. - /// The patchers to apply. - public static Harmony Apply(string id, IMonitor monitor, params IPatcher[] patchers) - { - Harmony harmony = new(id); - - foreach (IPatcher patcher in patchers) - { - try - { - patcher.Apply(harmony, monitor); - } - catch (Exception ex) - { - monitor.Log($"Couldn't apply runtime patch '{patcher.GetType().Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); - monitor.Log($"Technical details:\n{ex.GetLogSummary()}"); - } - } - - return harmony; - } - } -} diff --git a/src/SMAPI.Internal.Patching/IPatcher.cs b/src/SMAPI.Internal.Patching/IPatcher.cs deleted file mode 100644 index a732d64ff..000000000 --- a/src/SMAPI.Internal.Patching/IPatcher.cs +++ /dev/null @@ -1,16 +0,0 @@ -using HarmonyLib; - -namespace StardewModdingAPI.Internal.Patching -{ - /// A set of Harmony patches to apply. - internal interface IPatcher - { - /********* - ** Public methods - *********/ - /// Apply the Harmony patches for this instance. - /// The Harmony instance. - /// The monitor with which to log any errors. - public void Apply(Harmony harmony, IMonitor monitor); - } -} diff --git a/src/SMAPI.Internal.Patching/PatchHelper.cs b/src/SMAPI.Internal.Patching/PatchHelper.cs deleted file mode 100644 index edd8ef57f..000000000 --- a/src/SMAPI.Internal.Patching/PatchHelper.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Text; -using HarmonyLib; - -namespace StardewModdingAPI.Internal.Patching -{ - /// Provides utility methods for patching game code with Harmony. - internal static class PatchHelper - { - /********* - ** Public methods - *********/ - /// Get a constructor and assert that it was found. - /// The type containing the method. - /// The method parameter types, or null if it's not overloaded. - /// The type has no matching constructor. - public static ConstructorInfo RequireConstructor(Type[]? parameters = null) - { - return - AccessTools.Constructor(typeof(TTarget), parameters) - ?? throw new InvalidOperationException($"Can't find constructor {PatchHelper.GetMethodString(typeof(TTarget), null, parameters)} to patch."); - } - - /// Get a method and assert that it was found. - /// The type containing the method. - /// The method name. - /// The method parameter types, or null if it's not overloaded. - /// The method generic types, or null if it's not generic. - /// The type has no matching method. - public static MethodInfo RequireMethod(string name, Type[]? parameters = null, Type[]? generics = null) - { - return - AccessTools.Method(typeof(TTarget), name, parameters, generics) - ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(typeof(TTarget), name, parameters, generics)} to patch."); - } - - /// Get a human-readable representation of a method target. - /// The type containing the method. - /// The method name, or null for a constructor. - /// The method parameter types, or null if it's not overloaded. - /// The method generic types, or null if it's not generic. - public static string GetMethodString(Type type, string? name, Type[]? parameters = null, Type[]? generics = null) - { - StringBuilder str = new(); - - // type - str.Append(type.FullName); - - // method name (if not constructor) - if (name != null) - { - str.Append('.'); - str.Append(name); - } - - // generics - if (generics?.Any() == true) - { - str.Append('<'); - str.Append(string.Join(", ", generics.Select(p => p.FullName))); - str.Append('>'); - } - - // parameters - if (parameters?.Any() == true) - { - str.Append('('); - str.Append(string.Join(", ", parameters.Select(p => p.FullName))); - str.Append(')'); - } - - return str.ToString(); - } - } -} diff --git a/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems b/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems deleted file mode 100644 index 4fa2a0628..000000000 --- a/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.projitems +++ /dev/null @@ -1,17 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 6c16e948-3e5c-47a7-bf4b-07a7469a87a5 - - - SMAPI.Internal.Patching - - - - - - - - \ No newline at end of file diff --git a/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj b/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj deleted file mode 100644 index 1a102c826..000000000 --- a/src/SMAPI.Internal.Patching/SMAPI.Internal.Patching.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 6c16e948-3e5c-47a7-bf4b-07a7469a87a5 - 14.0 - - - - - - - - diff --git a/src/SMAPI.sln b/src/SMAPI.sln index c7eac4c3e..4ed075106 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -51,8 +51,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mods", "Mods", "{AE9A4D46-E EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMAPI.Internal", "SMAPI.Internal\SMAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMAPI.Internal.Patching", "SMAPI.Internal.Patching\SMAPI.Internal.Patching.shproj", "{6C16E948-3E5C-47A7-BF4B-07A7469A87A5}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{680B2641-81EA-467C-86A5-0E81CDC57ED0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests", "SMAPI.Tests\SMAPI.Tests.csproj", "{AA95884B-7097-476E-92C8-D0500DE9D6D1}" @@ -106,11 +104,9 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 - SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{6c16e948-3e5c-47a7-bf4b-07a7469a87a5}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5 - SMAPI.Internal.Patching\SMAPI.Internal.Patching.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -181,7 +177,6 @@ Global {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {5947303D-3512-413A-9009-7AC43F5D3513} = {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} {85208F8D-6FD1-4531-BE05-7142490F59FE} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {6C16E948-3E5C-47A7-BF4B-07A7469A87A5} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 014f119fd..c309baaf9 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -34,8 +34,6 @@ using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; -using StardewModdingAPI.Internal.Patching; -using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; @@ -273,19 +271,17 @@ public void RunInteractively() exitGameImmediately: this.ExitGameImmediately, onGameContentLoaded: this.OnInstanceContentLoaded, + onLoadStageChanged: this.OnLoadStageChanged, onGameUpdating: this.OnGameUpdating, onPlayerInstanceUpdating: this.OnPlayerInstanceUpdating, onPlayerInstanceRendered: this.OnRendered, onGameExiting: this.OnGameExiting ); - StardewValley.GameRunner.instance = this.Game; + GameRunner.instance = this.Game; + TitleMenu.OnCreatedNewCharacter += () => this.OnLoadStageChanged(LoadStage.CreatedBasicInfo); - // apply game patches + // fix Harmony for mods MiniMonoModHotfix.Apply(); - HarmonyPatcher.Apply("SMAPI", this.Monitor, - new Game1Patcher(this.OnLoadStageChanged), - new TitleMenuPatcher(this.OnLoadStageChanged) - ); // set window titles this.UpdateWindowTitles(); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index a6f2ab319..4c940f447 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; @@ -12,6 +13,8 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Logging; +using StardewValley.Menus; +using StardewValley.Minigames; namespace StardewModdingAPI.Framework { @@ -45,6 +48,9 @@ internal class SGame : Game1 /// Raised after the instance finishes loading its initial content. private readonly Action OnContentLoaded; + /// Raised invoke when the load stage changes through a method like . + private readonly Action OnLoadStageChanged; + /// Raised after the instance finishes a draw loop. private readonly Action OnRendered; @@ -98,8 +104,9 @@ internal class SGame : Game1 /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised when the instance is updating its state (roughly 60 times per second). /// Raised after the game finishes loading its initial content. + /// Raised invoke when the load stage changes through a method like . /// Raised after the instance finishes a draw loop. - public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflector reflection, SInputState input, SModHooks modHooks, IGameLogger gameLogger, SMultiplayer multiplayer, Action exitGameImmediately, Action onUpdating, Action onContentLoaded, Action onRendered) + public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflector reflection, SInputState input, SModHooks modHooks, IGameLogger gameLogger, SMultiplayer multiplayer, Action exitGameImmediately, Action onUpdating, Action onContentLoaded, Action onLoadStageChanged, Action onRendered) : base(playerIndex, instanceIndex) { // init XNA @@ -118,6 +125,7 @@ public SGame(PlayerIndex playerIndex, int instanceIndex, Monitor monitor, Reflec this.ExitGameImmediately = exitGameImmediately; this.OnUpdating = onUpdating; this.OnContentLoaded = onContentLoaded; + this.OnLoadStageChanged = onLoadStageChanged; this.OnRendered = onRendered; } @@ -168,6 +176,20 @@ protected override void Initialize() this.InitialMultiplayer = null; } + /// The method called when loading or creating a save. + /// Whether this is being called from the game's load enumerator. + public override void loadForNewGame(bool loadedGame = false) + { + base.loadForNewGame(loadedGame); + + bool isCreating = + (Game1.currentMinigame is Intro) // creating save with intro + || (Game1.activeClickableMenu is TitleMenu menu && menu.transitioningCharacterCreationMenu); // creating save, skipped intro + + if (isCreating) + this.OnLoadStageChanged(LoadStage.CreatedLocations); + } + /// The method called when the instance is updating its state (roughly 60 times per second). /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) @@ -236,5 +258,13 @@ protected override void _draw(GameTime gameTime, RenderTarget2D target_screen) } Context.IsInDrawLoop = false; } + + /// The method called when the game is returning to the title screen from a loaded save. + public override void CleanupReturningToTitle() + { + this.OnLoadStageChanged(LoadStage.ReturningToTitle); + + base.CleanupReturningToTitle(); + } } } diff --git a/src/SMAPI/Framework/SGameRunner.cs b/src/SMAPI/Framework/SGameRunner.cs index d9c79088d..28946f6b0 100644 --- a/src/SMAPI/Framework/SGameRunner.cs +++ b/src/SMAPI/Framework/SGameRunner.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -37,6 +38,9 @@ internal class SGameRunner : GameRunner /// Raised after the game finishes loading its initial content. private readonly Action OnGameContentLoaded; + /// Raised invoke when the load stage changes through a method like . + private readonly Action OnLoadStageChanged; + /// Raised when XNA is updating (roughly 60 times per second). private readonly Action OnGameUpdating; @@ -68,11 +72,12 @@ internal class SGameRunner : GameRunner /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised after the game finishes loading its initial content. + /// Raised invoke when the load stage changes through a method like . /// Raised when XNA is updating its state (roughly 60 times per second). /// Raised when the game instance for a local split-screen player is updating (once per per player). /// Raised after an instance finishes a draw loop. /// Raised before the game exits. - public SGameRunner(Monitor monitor, Reflector reflection, SModHooks modHooks, IGameLogger gameLogger, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting, Action onPlayerInstanceRendered) + public SGameRunner(Monitor monitor, Reflector reflection, SModHooks modHooks, IGameLogger gameLogger, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onLoadStageChanged, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting, Action onPlayerInstanceRendered) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -87,6 +92,7 @@ public SGameRunner(Monitor monitor, Reflector reflection, SModHooks modHooks, IG this.Multiplayer = multiplayer; this.ExitGameImmediately = exitGameImmediately; this.OnGameContentLoaded = onGameContentLoaded; + this.OnLoadStageChanged = onLoadStageChanged; this.OnGameUpdating = onGameUpdating; this.OnPlayerInstanceUpdating = onPlayerInstanceUpdating; this.OnPlayerInstanceRendered = onPlayerInstanceRendered; @@ -111,6 +117,7 @@ public override Game1 CreateGameInstance(PlayerIndex playerIndex = PlayerIndex.O exitGameImmediately: this.ExitGameImmediately, onUpdating: this.OnPlayerInstanceUpdating, onContentLoaded: this.OnGameContentLoaded, + onLoadStageChanged: this.OnLoadStageChanged, onRendered: this.OnPlayerInstanceRendered ); } diff --git a/src/SMAPI/Patches/Game1Patcher.cs b/src/SMAPI/Patches/Game1Patcher.cs deleted file mode 100644 index fab5f9924..000000000 --- a/src/SMAPI/Patches/Game1Patcher.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Enums; -using StardewModdingAPI.Internal.Patching; -using StardewValley; -using StardewValley.Menus; -using StardewValley.Minigames; - -namespace StardewModdingAPI.Patches -{ - /// Harmony patches for which notify SMAPI for save load stages. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class Game1Patcher : BasePatcher - { - /********* - ** Fields - *********/ - /// A callback to invoke when the load stage changes. - private static Action OnStageChanged = null!; // initialized in constructor - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A callback to invoke when the load stage changes. - public Game1Patcher(Action onStageChanged) - { - Game1Patcher.OnStageChanged = onStageChanged; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - // detect CreatedLocations - harmony.Patch( - original: this.RequireMethod(nameof(Game1.loadForNewGame)), - postfix: this.GetHarmonyMethod(nameof(Game1Patcher.After_LoadForNewGame)) - ); - - // detect ReturningToTitle - harmony.Patch( - original: this.RequireMethod(nameof(Game1.CleanupReturningToTitle)), - prefix: this.GetHarmonyMethod(nameof(Game1Patcher.Before_CleanupReturningToTitle)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call before . - /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_CleanupReturningToTitle() - { - Game1Patcher.OnStageChanged(LoadStage.ReturningToTitle); - return true; - } - - /// The method to call after . - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static void After_LoadForNewGame() - { - if (Game1Patcher.IsCreating()) - Game1Patcher.OnStageChanged(LoadStage.CreatedLocations); - } - - /// Get whether the save file is currently being created. - private static bool IsCreating() - { - return - (Game1.currentMinigame is Intro) // creating save with intro - || (Game1.activeClickableMenu is TitleMenu menu && menu.transitioningCharacterCreationMenu); // creating save, skipped intro - } - } -} diff --git a/src/SMAPI/Patches/TitleMenuPatcher.cs b/src/SMAPI/Patches/TitleMenuPatcher.cs deleted file mode 100644 index 18f1a8306..000000000 --- a/src/SMAPI/Patches/TitleMenuPatcher.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Enums; -using StardewModdingAPI.Internal.Patching; -using StardewValley.Menus; - -namespace StardewModdingAPI.Patches -{ - /// Harmony patches for which notify SMAPI when a new character was created. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class TitleMenuPatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// A callback to invoke when the load stage changes. - private static Action OnStageChanged = null!; // initialized in constructor - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A callback to invoke when the load stage changes. - public TitleMenuPatcher(Action onStageChanged) - { - TitleMenuPatcher.OnStageChanged = onStageChanged; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - harmony.Patch( - original: this.RequireMethod(nameof(TitleMenu.createdNewCharacter)), - prefix: this.GetHarmonyMethod(nameof(TitleMenuPatcher.Before_CreatedNewCharacter)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call before . - /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Before_CreatedNewCharacter() - { - TitleMenuPatcher.OnStageChanged(LoadStage.CreatedBasicInfo); - return true; - } - } -} diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index f86509be8..b77d332b8 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -64,5 +64,4 @@ - From 507650353859876d47ecc46402bd6f169a0c0203 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 13 Feb 2024 17:40:03 -0500 Subject: [PATCH 62/84] fix load stage no longer updated on initial save creation This broke in f58bcb8fb4eb4a0cbfd1085baf400d922895ca9c because Game1.Initialize() resets static fields, so we need to register `TitleMenu.OnCreatedNewCharacter` after Initialize() runs. --- src/SMAPI/Framework/SCore.cs | 1 - src/SMAPI/Framework/SGame.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c309baaf9..9548781fe 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -278,7 +278,6 @@ public void RunInteractively() onGameExiting: this.OnGameExiting ); GameRunner.instance = this.Game; - TitleMenu.OnCreatedNewCharacter += () => this.OnLoadStageChanged(LoadStage.CreatedBasicInfo); // fix Harmony for mods MiniMonoModHotfix.Apply(); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4c940f447..a67b8d05b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -170,6 +170,8 @@ protected override void Initialize() // The game resets public static fields after the class is constructed (see GameRunner.SetInstanceDefaults), so SMAPI needs to re-override them here. Game1.input = this.InitialInput; Game1.multiplayer = this.InitialMultiplayer; + if (this.IsMainInstance) + TitleMenu.OnCreatedNewCharacter += () => this.OnLoadStageChanged(LoadStage.CreatedBasicInfo); // event is static and shared between screens // The Initial* fields should no longer be used after this point, since mods may further override them after initialization. this.InitialInput = null; From 5af2b0804c4a601ead19c4f5510da1add0ed481c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 13 Feb 2024 20:42:06 -0500 Subject: [PATCH 63/84] fix package script after recent .NET updates --- build/windows/prepare-install-package.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 2e6dc9912..48c013ff0 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -21,6 +21,7 @@ $bundleModNames = "ConsoleCommands", "SaveBackup" # build configuration $buildConfig = "Release" +$framework = "net6.0" $folders = "linux", "macOS", "windows" $runtimes = @{ linux = "linux-x64"; macOS = "osx-x64"; windows = "win-x64" } $msBuildPlatformNames = @{ linux = "Unix"; macOS = "OSX"; windows = "Windows_NT" } @@ -72,20 +73,20 @@ foreach ($folder in $folders) { echo "Compiling SMAPI for $folder..." echo "-------------------------------------------------" - dotnet publish src/SMAPI --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained true + dotnet publish src/SMAPI --configuration $buildConfig -v minimal --runtime "$runtime" --framework "$framework" -p:OS="$msbuildPlatformName" -p:TargetFrameworks="$framework" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained true echo "" echo "" echo "Compiling installer for $folder..." echo "-------------------------------------------------" - dotnet publish src/SMAPI.Installer --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" -p:PublishTrimmed=True -p:TrimMode=Link --self-contained true + dotnet publish src/SMAPI.Installer --configuration $buildConfig -v minimal --runtime "$runtime" --framework "$framework" -p:OS="$msbuildPlatformName" -p:TargetFrameworks="$framework" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" -p:PublishTrimmed=True -p:TrimMode=Link --self-contained true echo "" echo "" foreach ($modName in $bundleModNames) { echo "Compiling $modName for $folder..." echo "-------------------------------------------------" - dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" -p:OS="$msbuildPlatformName" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained false + dotnet publish src/SMAPI.Mods.$modName --configuration $buildConfig -v minimal --runtime "$runtime" --framework "$framework" -p:OS="$msbuildPlatformName" -p:TargetFrameworks="$framework" -p:GamePath="$gamePath" -p:CopyToGameFolder="false" --self-contained false echo "" echo "" } From 91203d5d683cd1f781674ffc89709193af42db13 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Feb 2024 17:20:23 -0500 Subject: [PATCH 64/84] add shortcut link for Content Patcher migration guide --- src/SMAPI.Web/Startup.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index a068a9984..4e7c9f877 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -226,8 +226,11 @@ private RewriteOptions GetRedirectRules() [@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods", // GitHub docs - [@"^/package(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1", - [@"^/release(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#$1", + ["^/package(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1", + ["^/release(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#$1", + + // Content Patcher docs + ["/cp-migrate(?:/?(.*))$"] = "https://github.com/Pathoschild/StardewMods/blob/develop/ContentPatcher/docs/author-migration-guide.md#$1", // legacy redirects [@"^/compat\.?$"] = "https://smapi.io/mods" From 3c7b8fbd0de8a29f1d234e16a749a2f14ceaf6d2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 21 Feb 2024 20:26:51 -0500 Subject: [PATCH 65/84] remove unneeded property --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 17432085d..67467d58f 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -23,9 +23,6 @@ internal class AssemblyLoader : IDisposable /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// Whether to detect paranoid mode issues. - private readonly bool ParanoidMode; - /// Metadata for mapping assemblies to the current platform. private readonly PlatformAssemblyMap AssemblyMap; @@ -62,7 +59,6 @@ internal class AssemblyLoader : IDisposable public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods) { this.Monitor = monitor; - this.ParanoidMode = paranoidMode; this.RewriteMods = rewriteMods; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); @@ -86,7 +82,7 @@ public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMo } // init rewriters - this.InstructionHandlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, this.RewriteMods).ToArray(); + this.InstructionHandlers = new InstructionMetadata().GetHandlers(paranoidMode, this.RewriteMods).ToArray(); } From 720ebf28808fd375415072657e0be7a58349250d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 22 Feb 2024 01:05:12 -0500 Subject: [PATCH 66/84] merge invalid-member rewrite finders This fixes redundant messages for members with mismatched types. For example: ``` reference to StardewValley.Event.id (no such field) reference to StardewValley.Event.id (field returns string, not int) ``` --- docs/release-notes.md | 10 +- ...r.cs => ReferenceToInvalidMemberFinder.cs} | 63 +++++++----- .../Finders/ReferenceToMissingMemberFinder.cs | 95 ------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 +- 4 files changed, 43 insertions(+), 128 deletions(-) rename src/SMAPI/Framework/ModLoading/Finders/{ReferenceToMemberWithUnexpectedTypeFinder.cs => ReferenceToInvalidMemberFinder.cs} (58%) delete mode 100644 src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bd81a0ba..8cbc4cd16 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming release for Stardew Valley 1.6 * For players: * Updated for Stardew Valley 1.6. + * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). * Improved performance. * Improved compatibility rewriting to handle more cases (thanks to SinZ!). * Removed the bundled `ErrorHandler` mod (now integrated into Stardew Valley 1.6). @@ -14,16 +15,11 @@ * For mod authors: * Updated to .NET 6. * Added `RenderingStep` and `RenderedStep` events, which let you handle a specific step in the game's render cycle. + * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!). * Removed all deprecated APIs. * SMAPI no longer intercepts output written to the console. Mods which directly access `Console` will be listed under mod warnings. * Calling `Monitor.VerboseLog` with an interpolated string no longer evaluates the string if verbose mode is disabled (thanks to atravita!). This only applies to mods compiled in SMAPI 4.0.0 or later. - -## Upcoming release -* For players: - * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). - -* For mod authors: - * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!). + * Fixed redundant `TRACE` logs for a broken mod which references members with the wrong types. * For the web UI: * Fixed uploaded log/JSON file expiry alway shown as renewed. diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs similarity index 58% rename from src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs index f34542c30..e5625c637 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs @@ -7,9 +7,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds references to a field, property, or method which returns a different type than the code expects. + /// Finds references to a field, property, or method which either doesn't exist or returns a different type than the code expects. /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. - internal class ReferenceToMemberWithUnexpectedTypeFinder : BaseInstructionHandler + internal class ReferenceToInvalidMemberFinder : BaseInstructionHandler { /********* ** Fields @@ -23,7 +23,7 @@ internal class ReferenceToMemberWithUnexpectedTypeFinder : BaseInstructionHandle *********/ /// Construct an instance. /// The assembly names to which to heuristically detect broken references. - public ReferenceToMemberWithUnexpectedTypeFinder(ISet validateReferencesToAssemblies) + public ReferenceToInvalidMemberFinder(ISet validateReferencesToAssemblies) : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; @@ -36,37 +36,45 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - // get target field FieldDefinition? targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); - if (targetField == null) - return false; - // validate return type - if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) - { + // wrong return type + if (targetField != null && !RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"); - return false; - } + + // missing + else if (targetField == null || targetField.HasConstant || !RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, targetField.DeclaringType)) + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); + + return false; } // method reference - MethodReference? methodReference = RewriteHelper.AsMethodReference(instruction); - if (methodReference != null && !this.IsUnsupported(methodReference) && this.ShouldValidate(methodReference.DeclaringType)) + MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null && !this.IsUnsupported(methodRef)) { - // get potential targets - MethodDefinition[]? candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray(); - if (candidateMethods == null || !candidateMethods.Any()) - return false; + MethodDefinition? methodDef = methodRef.Resolve(); - // compare return types - MethodDefinition? methodDef = methodReference.Resolve(); - if (methodDef == null) - return false; // validated by ReferenceToMissingMemberFinder + // wrong return type + if (methodDef != null && this.ShouldValidate(methodRef.DeclaringType)) + { + MethodDefinition[]? candidateMethods = methodRef.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodRef.Name).ToArray(); + if (candidateMethods?.Any() is true && candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); + } - if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) + // missing + else if (methodDef is null) { - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); - return false; + string phrase; + if (this.IsProperty(methodRef)) + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + else if (methodRef.Name == ".ctor") + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + else + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + + this.MarkFlag(InstructionHandleResult.NotCompatible, phrase); } } @@ -116,5 +124,12 @@ private string GetFriendlyTypeName(TypeReference type) return type.FullName; } + + /// Get whether a method reference is a property getter or setter. + /// The method reference. + private bool IsProperty(MethodReference method) + { + return method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); + } } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs deleted file mode 100644 index b54d57c4b..000000000 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds references to a field, property, or method which no longer exists. - /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. - internal class ReferenceToMissingMemberFinder : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The assembly names to which to heuristically detect broken references. - private readonly ISet ValidateReferencesToAssemblies; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The assembly names to which to heuristically detect broken references. - public ReferenceToMissingMemberFinder(ISet validateReferencesToAssemblies) - : base(defaultPhrase: "") - { - this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; - } - - /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) - { - // field reference - FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) - { - FieldDefinition? target = fieldRef.Resolve(); - if (target == null || target.HasConstant || !RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, target.DeclaringType)) - { - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); - return false; - } - } - - // method reference - MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef)) - { - MethodDefinition? target = methodRef.Resolve(); - if (target == null) - { - string phrase; - if (this.IsProperty(methodRef)) - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; - else if (methodRef.Name == ".ctor") - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; - else - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; - - this.MarkFlag(InstructionHandleResult.NotCompatible, phrase); - return false; - } - } - - return false; - } - - - /********* - ** Private methods - *********/ - /// Whether references to the given type should be validated. - /// The type reference. - private bool ShouldValidate([NotNullWhen(true)] TypeReference? type) - { - return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); - } - - /// Get whether a method reference is a special case that's not currently supported (e.g. array methods). - /// The method reference. - private bool IsUnsupported(MethodReference method) - { - return - method.DeclaringType.Name.Contains("["); // array methods - } - - /// Get whether a method reference is a property getter or setter. - /// The method reference. - private bool IsProperty(MethodReference method) - { - return method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 284c8a444..cea30f7a7 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -240,8 +240,7 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr ** detect mod issues ****/ // broken code - yield return new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies); - yield return new ReferenceToMemberWithUnexpectedTypeFinder(this.ValidateReferencesToAssemblies); + yield return new ReferenceToInvalidMemberFinder(this.ValidateReferencesToAssemblies); // code which may impact game stability yield return new FieldFinder(typeof(SaveGame).FullName!, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer); From 21abd0f552930215c55e0993bb6a06c90825cdaf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 22 Feb 2024 01:05:12 -0500 Subject: [PATCH 67/84] prefer `base` in rewriters for clarity --- .../ModLoading/Framework/SuppressReasons.cs | 3 ++ .../StardewValley_1_6/BedFurnitureFacade.cs | 4 +-- .../StardewValley_1_6/BreakableContainer.cs | 2 +- .../Rewriters/StardewValley_1_6/BuffFacade.cs | 3 +- .../BuildableGameLocationFacade.cs | 3 +- .../StardewValley_1_6/BuildingFacade.cs | 5 ++-- .../Rewriters/StardewValley_1_6/BushFacade.cs | 9 +++--- .../Rewriters/StardewValley_1_6/CaskFacade.cs | 2 +- .../StardewValley_1_6/CharacterFacade.cs | 15 +++++----- .../StardewValley_1_6/ChestFacade.cs | 4 +-- .../StardewValley_1_6/FarmAnimalFacade.cs | 3 +- .../StardewValley_1_6/FarmerFacade.cs | 29 ++++++++++--------- .../StardewValley_1_6/FarmerTeamFacade.cs | 3 +- .../StardewValley_1_6/FenceFacade.cs | 2 +- .../StardewValley_1_6/FruitTreeFacade.cs | 3 +- .../StardewValley_1_6/FurnitureFacade.cs | 10 +++---- .../StardewValley_1_6/GameLocationFacade.cs | 15 +++++----- .../StardewValley_1_6/JunimoHutFacade.cs | 3 +- .../Rewriters/StardewValley_1_6/NpcFacade.cs | 11 +++---- .../StardewValley_1_6/ObjectFacade.cs | 18 ++++++------ .../StardewValley_1_6/ResourceClumpFacade.cs | 3 +- .../StardewValley_1_6/WorldDateFacade.cs | 5 ++-- 22 files changed, 86 insertions(+), 69 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/Framework/SuppressReasons.cs b/src/SMAPI/Framework/ModLoading/Framework/SuppressReasons.cs index f2e723769..c960b5196 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/SuppressReasons.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/SuppressReasons.cs @@ -8,5 +8,8 @@ internal static class SuppressReasons /// A message indicating the code is used via assembly rewriting. public const string UsedViaRewriting = "This code is used via assembly rewriting."; + + /// A message indicating the code is used via assembly rewriting. + public const string BaseForClarity = "This code deliberately uses 'base' to ensure we're calling the real method instead of a rewritten one."; } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs index 2625c9b7f..5156c8e8c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BedFurnitureFacade.cs @@ -28,12 +28,12 @@ public static BedFurniture Constructor(int which, Vector2 tile) public bool CanModifyBed(GameLocation location, Farmer who) { - return this.CanModifyBed(who); + return base.CanModifyBed(who); } public bool IsBeingSleptIn(GameLocation location) { - return this.IsBeingSleptIn(); + return base.IsBeingSleptIn(); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs index b04c726f9..e9eaea59b 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs @@ -18,7 +18,7 @@ public class BreakableContainerFacade : BreakableContainer, IRewriteFacade *********/ public void releaseContents(GameLocation location, Farmer who) { - this.releaseContents(who); + base.releaseContents(who); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs index 8d98fb46a..d479334af 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffFacade.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] public class BuffFacade : Buff, IRewriteFacade @@ -46,7 +47,7 @@ public void addBuff() public void removeBuff() { - Game1.player.buffs.Remove(this.id); + Game1.player.buffs.Remove(base.id); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs index bc8cf1150..a66b096c3 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildableGameLocationFacade.cs @@ -12,6 +12,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class BuildableGameLocationFacade : GameLocation, IRewriteFacade { @@ -45,7 +46,7 @@ public class BuildableGameLocationFacade : GameLocation, IRewriteFacade public Building? getBuildingUnderConstruction() { - foreach (Building b in this.buildings) + foreach (Building b in base.buildings) { if (b.daysOfConstructionLeft > 0 || b.daysUntilUpgrade > 0) return b; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs index 5b8cf14e3..6c21ee719 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs @@ -12,14 +12,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class BuildingFacade : Building, IRewriteFacade { /********* ** Accessors *********/ - public NetRef input => NetRefWrapperCache.GetCachedWrapperFor(this.GetBuildingChest("Input")); // Mill - public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(this.GetBuildingChest("Output")); // Mill + public NetRef input => NetRefWrapperCache.GetCachedWrapperFor(base.GetBuildingChest("Input")); // Mill + public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(base.GetBuildingChest("Output")); // Mill /********* diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs index c0cd97b73..7857aaf23 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class BushFacade : Bush, IRewriteFacade { @@ -21,15 +22,15 @@ public bool inBloom(string season, int dayOfMonth) { // call new method if possible if (season == Game1.currentSeason && dayOfMonth == Game1.dayOfMonth) - return this.inBloom(); + return base.inBloom(); // else mimic old behavior with 1.6 features - if (this.size == Bush.greenTeaBush) + if (base.size == Bush.greenTeaBush) { return - this.getAge() >= Bush.daysToMatureGreenTeaBush + base.getAge() >= Bush.daysToMatureGreenTeaBush && dayOfMonth >= 22 - && (season != "winter" || this.IsSheltered()); + && (season != "winter" || base.IsSheltered()); } switch (season) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs index a8c4c6d46..c5c765981 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CaskFacade.cs @@ -19,7 +19,7 @@ public class CaskFacade : Cask, IRewriteFacade *********/ public bool IsValidCaskLocation(GameLocation location) { - return this.IsValidCaskLocation(); + return base.IsValidCaskLocation(); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs index dd069c6fe..8b11a9ad2 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs @@ -10,6 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class CharacterFacade : Character, IRewriteFacade { @@ -18,37 +19,37 @@ public class CharacterFacade : Character, IRewriteFacade *********/ public int getStandingX() { - return this.StandingPixel.X; + return base.StandingPixel.X; } public int getStandingY() { - return this.StandingPixel.Y; + return base.StandingPixel.Y; } public Point getStandingXY() { - return this.StandingPixel; + return base.StandingPixel; } public Vector2 getTileLocation() { - return this.Tile; + return base.Tile; } public Point getTileLocationPoint() { - return this.TilePoint; + return base.TilePoint; } public int getTileX() { - return this.TilePoint.X; + return base.TilePoint.X; } public int getTileY() { - return this.TilePoint.Y; + return base.TilePoint.Y; } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs index e4abd8d37..7c18b047f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs @@ -51,12 +51,12 @@ public ChestFacade(int coins, List items, Vector2 location, bool giftbox = public void destroyAndDropContents(Vector2 pointToDropAt, GameLocation location) { - this.destroyAndDropContents(pointToDropAt); + base.destroyAndDropContents(pointToDropAt); } public void dumpContents(GameLocation location) { - this.dumpContents(); + base.dumpContents(); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs index 51178ea9e..f507249d5 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs @@ -10,6 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class FarmAnimalFacade : FarmAnimal, IRewriteFacade { @@ -18,7 +19,7 @@ public class FarmAnimalFacade : FarmAnimal, IRewriteFacade *********/ public bool isCoopDweller() { - FarmAnimalData? data = this.GetAnimalData(); + FarmAnimalData? data = base.GetAnimalData(); return data?.House == "Coop"; } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs index 8150fd04d..6452db005 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs @@ -15,6 +15,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] public class FarmerFacade : Farmer, IRewriteFacade { /********* @@ -34,12 +35,12 @@ public class FarmerFacade : Farmer, IRewriteFacade *********/ public void addQuest(int questID) { - this.addQuest(questID.ToString()); + base.addQuest(questID.ToString()); } public void changePants(Color color) { - this.changePantsColor(color); + base.changePantsColor(color); } public void changePantStyle(int whichPants, bool is_customization_screen = false) @@ -49,27 +50,27 @@ public void changePantStyle(int whichPants, bool is_customization_screen = false public void changeShirt(int whichShirt, bool is_customization_screen = false) { - this.changeShirt(whichShirt.ToString()); + base.changeShirt(whichShirt.ToString()); } public void changeShoeColor(int which) { - this.changeShoeColor(which.ToString()); + base.changeShoeColor(which.ToString()); } public void completeQuest(int questID) { - this.completeQuest(questID.ToString()); + base.completeQuest(questID.ToString()); } public bool couldInventoryAcceptThisObject(int index, int stack, int quality = 0) { - return this.couldInventoryAcceptThisItem(index.ToString(), stack, quality); + return base.couldInventoryAcceptThisItem(index.ToString(), stack, quality); } public int GetEffectsOfRingMultiplier(int ring_index) { - return this.GetEffectsOfRingMultiplier(ring_index.ToString()); + return base.GetEffectsOfRingMultiplier(ring_index.ToString()); } public int getItemCount(int item_index, int min_price = 0) @@ -81,7 +82,7 @@ public int getItemCount(int item_index, int min_price = 0) public bool hasBuff(int whichBuff) { - return this.hasBuff(whichBuff.ToString()); + return base.hasBuff(whichBuff.ToString()); } public bool hasItemInInventory(int itemIndex, int quantity, int minPrice = 0) @@ -91,24 +92,24 @@ public bool hasItemInInventory(int itemIndex, int quantity, int minPrice = 0) switch (itemIndex) { case 858: - return this.QiGems >= quantity; + return base.QiGems >= quantity; case 73: return Game1.netWorldState.Value.GoldenWalnuts >= quantity; default: - return this.getItemCount(ItemRegistry.type_object + itemIndex) >= quantity; + return base.getItemCount(ItemRegistry.type_object + itemIndex) >= quantity; } } public bool hasQuest(int id) { - return this.hasQuest(id.ToString()); + return base.hasQuest(id.ToString()); } public bool isWearingRing(int ringIndex) { - return this.isWearingRing(ringIndex.ToString()); + return base.isWearingRing(ringIndex.ToString()); } public bool removeItemsFromInventory(int index, int stack) @@ -118,7 +119,7 @@ public bool removeItemsFromInventory(int index, int stack) switch (index) { case 858: - this.QiGems -= stack; + base.QiGems -= stack; return true; case 73: @@ -152,7 +153,7 @@ public bool removeItemsFromInventory(int index, int stack) public void removeQuest(int questID) { - this.removeQuest(questID.ToString()); + base.removeQuest(questID.ToString()); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs index 4486117ba..19091c60c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs @@ -11,13 +11,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class FarmerTeamFacade : FarmerTeam, IRewriteFacade { /********* ** Accessors *********/ - public NetObjectList junimoChest => InventoryToNetObjectList.GetCachedWrapperFor(this.GetOrCreateGlobalInventory(FarmerTeam.GlobalInventoryId_JunimoChest)); + public NetObjectList junimoChest => InventoryToNetObjectList.GetCachedWrapperFor(base.GetOrCreateGlobalInventory(FarmerTeam.GlobalInventoryId_JunimoChest)); /********* diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs index e5c8c9f42..802280120 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs @@ -17,7 +17,7 @@ public class FenceFacade : Fence, IRewriteFacade *********/ public void toggleGate(GameLocation location, bool open, bool is_toggling_counterpart = false, Farmer? who = null) { - this.toggleGate(open, is_toggling_counterpart, who); + base.toggleGate(open, is_toggling_counterpart, who); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs index 933dc5538..0aebc33cd 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs @@ -13,6 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class FruitTreeFacade : FruitTree, IRewriteFacade { @@ -23,7 +24,7 @@ public NetString fruitSeason { get { - List? seasons = this.GetData()?.Seasons; + List? seasons = base.GetData()?.Seasons; string value = seasons?.Count > 0 ? string.Join(",", seasons) : string.Empty; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs index f8ba103ce..7b6e5f245 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FurnitureFacade.cs @@ -29,12 +29,12 @@ public static Furniture Constructor(int which, Vector2 tile) public void AddLightGlow(GameLocation location) { - this.AddLightGlow(); + base.AddLightGlow(); } public void addLights(GameLocation environment) { - this.addLights(); + base.addLights(); } public static Furniture GetFurnitureInstance(int index, Vector2? position = null) @@ -44,17 +44,17 @@ public static Furniture GetFurnitureInstance(int index, Vector2? position = null public void removeLights(GameLocation environment) { - this.removeLights(); + base.removeLights(); } public void RemoveLightGlow(GameLocation location) { - this.RemoveLightGlow(); + base.RemoveLightGlow(); } public void setFireplace(GameLocation location, bool playSound = true, bool broadcast = false) { - this.setFireplace(playSound, broadcast); + base.setFireplace(playSound, broadcast); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs index a04095e3c..8b9de1c74 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs @@ -18,6 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class GameLocationFacade : GameLocation, IRewriteFacade { @@ -26,7 +27,7 @@ public class GameLocationFacade : GameLocation, IRewriteFacade *********/ public NetCollection getCharacters() { - return this.characters; + return base.characters; } public virtual int getExtraMillisecondsPerInGameMinuteForThisLocation() @@ -36,17 +37,17 @@ public virtual int getExtraMillisecondsPerInGameMinuteForThisLocation() public int getNumberBuildingsConstructed(string name) { - return base.getNumberBuildingsConstructed(name, false); + return base.getNumberBuildingsConstructed(name, includeUnderConstruction: false); } public string GetSeasonForLocation() { - return this.GetSeasonKey(); + return base.GetSeasonKey(); } public bool isTileLocationOpenIgnoreFrontLayers(Location tile) { - return this.map.RequireLayer("Buildings").Tiles[tile.X, tile.Y] == null && !this.isWaterTile(tile.X, tile.Y); + return base.map.RequireLayer("Buildings").Tiles[tile.X, tile.Y] == null && !base.isWaterTile(tile.X, tile.Y); } public bool isTileLocationTotallyClearAndPlaceable(int x, int y) @@ -57,18 +58,18 @@ public bool isTileLocationTotallyClearAndPlaceable(int x, int y) public bool isTileLocationTotallyClearAndPlaceable(Vector2 v) { Vector2 pixel = new Vector2((v.X * Game1.tileSize) + Game1.tileSize / 2, (v.Y * Game1.tileSize) + Game1.tileSize / 2); - foreach (Furniture f in this.furniture) + foreach (Furniture f in base.furniture) { if (f.furniture_type != Furniture.rug && !f.isPassable() && f.GetBoundingBox().Contains((int)pixel.X, (int)pixel.Y) && !f.AllowPlacementOnThisTile((int)v.X, (int)v.Y)) return false; } - return this.isTileOnMap(v) && !this.isTileOccupied(v) && this.isTilePassable(new Location((int)v.X, (int)v.Y), Game1.viewport) && base.isTilePlaceable(v); + return base.isTileOnMap(v) && !this.isTileOccupied(v) && base.isTilePassable(new Location((int)v.X, (int)v.Y), Game1.viewport) && base.isTilePlaceable(v); } public bool isTileLocationTotallyClearAndPlaceableIgnoreFloors(Vector2 v) { - return this.isTileOnMap(v) && !this.isTileOccupiedIgnoreFloors(v) && this.isTilePassable(new Location((int)v.X, (int)v.Y), Game1.viewport) && base.isTilePlaceable(v); + return base.isTileOnMap(v) && !this.isTileOccupiedIgnoreFloors(v) && base.isTilePassable(new Location((int)v.X, (int)v.Y), Game1.viewport) && base.isTilePlaceable(v); } public bool isTileOccupied(Vector2 tileLocation, string characterToIgnore = "", bool ignoreAllCharacters = false) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs index d23439189..db6ff7b88 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/JunimoHutFacade.cs @@ -12,13 +12,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class JunimoHutFacade : JunimoHut, IRewriteFacade { /********* ** Accessors *********/ - public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(this.GetBuildingChest("Output")); + public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(base.GetBuildingChest("Output")); /********* diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs index 0ddbb7394..28e901025 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public abstract class NpcFacade : NPC, IRewriteFacade { @@ -21,19 +22,19 @@ public bool isBirthday(string season, int day) { // call new method if possible if (season == Game1.currentSeason && day == Game1.dayOfMonth) - return this.isBirthday(); + return base.isBirthday(); // else replicate old behavior return - this.Birthday_Season != null - && this.Birthday_Season == season - && this.Birthday_Day == day; + base.Birthday_Season != null + && base.Birthday_Season == season + && base.Birthday_Day == day; } public void showTextAboveHead(string Text, int spriteTextColor = -1, int style = NPC.textStyle_none, int duration = 3000, int preTimer = 0) { Color? color = spriteTextColor != -1 ? SpriteText.getColorFromIndex(spriteTextColor) : null; - this.showTextAboveHead(Text, color, style, duration, preTimer); + base.showTextAboveHead(Text, color, style, duration, preTimer); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs index c61c9f994..4b6fa2254 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs @@ -39,7 +39,7 @@ public static SObject Constructor(Vector2 tileLocation, int parentSheetIndex, in public static SObject Constructor(Vector2 tileLocation, int parentSheetIndex, string? Givenname, bool canBeSetDown, bool canBeGrabbed, bool isHoedirt, bool isSpawnedObject) { - SObject obj = new SObject(parentSheetIndex.ToString(), 1); + SObject obj = new(parentSheetIndex.ToString(), 1); if (Givenname != null && obj.name is (null or "Error Item")) obj.name = Givenname; @@ -54,12 +54,12 @@ public static SObject Constructor(Vector2 tileLocation, int parentSheetIndex, st public void ApplySprinkler(GameLocation location, Vector2 tile) { - this.ApplySprinkler(tile); + base.ApplySprinkler(tile); } public void DayUpdate(GameLocation location) { - this.DayUpdate(); + base.DayUpdate(); } public Rectangle getBoundingBox(Vector2 tileLocation) @@ -69,32 +69,32 @@ public Rectangle getBoundingBox(Vector2 tileLocation) public bool isForage(GameLocation location) { - return this.isForage(); + return base.isForage(); } public bool minutesElapsed(int minutes, GameLocation environment) { - return this.minutesElapsed(minutes); + return base.minutesElapsed(minutes); } public bool onExplosion(Farmer who, GameLocation location) { - return this.onExplosion(who); + return base.onExplosion(who); } public void performRemoveAction(Vector2 tileLocation, GameLocation environment) { - this.performRemoveAction(); + base.performRemoveAction(); } public bool performToolAction(Tool t, GameLocation location) { - return this.performToolAction(t); + return base.performToolAction(t); } public void updateWhenCurrentLocation(GameTime time, GameLocation environment) { - this.updateWhenCurrentLocation(time); + base.updateWhenCurrentLocation(time); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs index 355147f3a..bd2567eed 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ResourceClumpFacade.cs @@ -10,13 +10,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class ResourceClumpFacade : ResourceClump, IRewriteFacade { /********* ** Public methods *********/ - public NetVector2 tile => this.netTile; + public NetVector2 tile => base.netTile; /********* diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs index f91490707..43fdcd9a7 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WorldDateFacade.cs @@ -8,6 +8,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class WorldDateFacade : WorldDate, IRewriteFacade { @@ -16,8 +17,8 @@ public class WorldDateFacade : WorldDate, IRewriteFacade *********/ public new string Season { - get => this.SeasonKey; - set => this.SeasonKey = value; + get => base.SeasonKey; + set => base.SeasonKey = value; } From 1e2c49de8f31bbfcc86666f3fa33ce019174c44d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Feb 2024 23:01:35 -0500 Subject: [PATCH 68/84] fix heuristic rewrites overriding specific ones --- src/SMAPI/Metadata/InstructionMetadata.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index cea30f7a7..7a5808764 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -55,10 +55,6 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr // rewrite for crossplatform compatibility if (rewriteMods) { - // heuristic rewrites - yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); - yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); - // specific versions yield return new ReplaceReferencesRewriter() /**** @@ -227,6 +223,10 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr .MapMethod("!0 StardewValley.Network.NetPausableField`3::op_Implicit(StardewValley.Network.NetPausableField`3)", typeof(NetPausableFieldFacade), nameof(NetPausableFieldFacade.op_Implicit)); + // heuristic rewrites + yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); + yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); + // 32-bit to 64-bit in Stardew Valley 1.5.5 yield return new ArchitectureAssemblyRewriter(); From 593587140e3feac6fdab38aee7af59f3a39f77a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Mar 2024 23:52:27 -0500 Subject: [PATCH 69/84] log more specific error when a console command is unknown This avoids confusion if it failed because the user already had text typed into the console before it scrolled away. --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9548781fe..0ec677608 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -551,7 +551,7 @@ private void OnGameUpdating(GameTime gameTime, Action runGameUpdate) { if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId)) { - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + this.Monitor.Log($"Unknown command '{(!string.IsNullOrWhiteSpace(name) ? name : rawInput)}'; type 'help' for a list of available commands.", LogLevel.Error); continue; } } From c44792d49e6d9bed4c57d7d9eb561c43af5dff67 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Mar 2024 23:52:28 -0500 Subject: [PATCH 70/84] improve rewriters for Stardew Valley 1.6 --- .../Rewriters/ReplaceReferencesRewriter.cs | 5 +- .../StardewValley_1_6/AbigailGameFacade.cs | 36 ++ .../StardewValley_1_6/AnimalHouseFacade.cs | 34 ++ .../BasicProjectileFacade.cs | 61 ++++ .../StardewValley_1_6/BoatTunnelFacade.cs | 31 ++ .../StardewValley_1_6/BootsFacade.cs | 13 + .../StardewValley_1_6/BreakableContainer.cs | 21 ++ .../StardewValley_1_6/BuffsDisplayFacade.cs | 33 ++ .../StardewValley_1_6/BuildingFacade.cs | 32 ++ .../Rewriters/StardewValley_1_6/BushFacade.cs | 10 + .../StardewValley_1_6/CarpenterMenuFacade.cs | 5 + .../StardewValley_1_6/CharacterFacade.cs | 6 + .../StardewValley_1_6/ChestFacade.cs | 17 +- .../StardewValley_1_6/CrabPotFacade.cs | 32 ++ .../StardewValley_1_6/CraftingRecipeFacade.cs | 50 +++ .../Rewriters/StardewValley_1_6/CropFacade.cs | 6 + .../DebuffingProjectileFacade.cs | 46 +++ .../StardewValley_1_6/DelayedActionFacade.cs | 6 + .../DiscreteColorPickerFacade.cs | 39 ++ .../StardewValley_1_6/FarmAnimalFacade.cs | 5 + .../Rewriters/StardewValley_1_6/FarmFacade.cs | 39 ++ .../StardewValley_1_6/FarmerFacade.cs | 131 ++++++- .../StardewValley_1_6/FarmerRendererFacade.cs | 32 ++ .../StardewValley_1_6/FarmerTeamFacade.cs | 9 + .../StardewValley_1_6/FenceFacade.cs | 11 + .../FishTankFurnitureFacade.cs | 38 ++ .../StardewValley_1_6/FishingRodFacade.cs | 49 +++ .../StardewValley_1_6/ForestFacade.cs | 7 +- .../StardewValley_1_6/FruitTreeFacade.cs | 26 ++ .../StardewValley_1_6/Game1Facade.cs | 73 +++- .../StardewValley_1_6/GameLocationFacade.cs | 38 +- .../StardewValley_1_6/GiantCropFacade.cs | 33 ++ .../StardewValley_1_6/HoeDirtFacade.cs | 16 + .../StardewValley_1_6/HudMessageFacade.cs | 11 + .../StardewValley_1_6/IClickableMenuFacade.cs | 2 +- .../ImplicitConversionOperators.cs | 19 + .../LargeTerrainFeatureFacade.cs | 31 ++ .../StardewValley_1_6/LayerFacade.cs | 38 ++ .../StardewValley_1_6/LibraryMuseumFacade.cs | 32 ++ .../StardewValley_1_6/MeleeWeaponFacade.cs | 15 + .../StardewValley_1_6/MineShaftFacade.cs | 37 ++ .../StardewValley_1_6/MultiplayerFacade.cs | 41 +++ .../StardewValley_1_6/NetFieldsFacade.cs | 34 ++ .../StardewValley_1_6/NetWorldStateFacade.cs | 35 ++ .../Rewriters/StardewValley_1_6/NpcFacade.cs | 36 ++ .../StardewValley_1_6/ObjectFacade.cs | 15 + .../StardewValley_1_6/ProfileMenuFacade.cs | 34 ++ .../StardewValley_1_6/ProjectileFacade.cs | 30 ++ .../StardewValley_1_6/QuestFacade.cs | 32 ++ .../Rewriters/StardewValley_1_6/RingFacade.cs | 17 + .../StardewValley_1_6/ShopMenuFacade.cs | 10 + .../Rewriters/StardewValley_1_6/SignFacade.cs | 32 ++ .../StardewValley_1_6/SoundEffectFacade.cs | 33 ++ .../StardewValley_1_6/SpriteTextFacade.cs | 35 +- .../StardewValley_1_6/StatsFacade.cs | 343 ++++++++++++++++++ .../TemporaryAnimatedSpriteFacade.cs | 34 ++ .../StardewValley_1_6/TerrainFeatureFacade.cs | 16 + .../StardewValley_1_6/ToolFactoryFacade.cs | 57 +++ .../Rewriters/StardewValley_1_6/TreeFacade.cs | 6 + .../Rewriters/StardewValley_1_6/TvFacade.cs | 33 ++ .../StardewValley_1_6/UtilityFacade.cs | 79 +++- .../StardewValley_1_6/WallpaperFacade.cs | 40 ++ .../StardewValley_1_6/WateringCanFacade.cs | 40 ++ src/SMAPI/Metadata/InstructionMetadata.cs | 76 +++- 64 files changed, 2247 insertions(+), 36 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AbigailGameFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AnimalHouseFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BasicProjectileFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BoatTunnelFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffsDisplayFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CrabPotFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CraftingRecipeFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DebuffingProjectileFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DiscreteColorPickerFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerRendererFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishTankFurnitureFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GiantCropFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ImplicitConversionOperators.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LargeTerrainFeatureFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LayerFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LibraryMuseumFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MineShaftFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MultiplayerFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldsFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetWorldStateFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProfileMenuFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProjectileFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/QuestFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SignFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SoundEffectFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StatsFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TemporaryAnimatedSpriteFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ToolFactoryFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TvFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WallpaperFacade.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index 30ebc9919..9c9994840 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -220,8 +220,11 @@ public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, str /// The facade type to which to point matching references. /// If the facade has a public constructor with no parameters, whether to rewrite references to empty constructors to use that one. (This is needed because .NET has no way to distinguish between an implicit and explicit constructor.) public ReplaceReferencesRewriter MapFacade(bool mapDefaultConstructor = false) - where TFacade : IRewriteFacade + where TFacade : TFromType, IRewriteFacade { + if (typeof(IRewriteFacade).IsAssignableFrom(typeof(TFromType))) + throw new InvalidOperationException("Can't rewrite a rewrite facade."); + return this.MapFacade(typeof(TFromType).FullName!, typeof(TFacade), mapDefaultConstructor); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AbigailGameFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AbigailGameFacade.cs new file mode 100644 index 000000000..bc6d3fa45 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AbigailGameFacade.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Minigames; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class AbigailGameFacade : AbigailGame, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static AbigailGame Constructor(bool playingWithAbby = false) + { + return new AbigailGame( + playingWithAbby + ? Game1.getCharacterFromName("Abigail") + : null + ); + } + + + /********* + ** Private methods + *********/ + private AbigailGameFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AnimalHouseFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AnimalHouseFacade.cs new file mode 100644 index 000000000..44e70dc2d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/AnimalHouseFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Buildings; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class AnimalHouseFacade : AnimalHouse, IRewriteFacade + { + /********* + ** Public methods + *********/ + public Building getBuilding() + { + return base.GetContainingBuilding(); + } + + + /********* + ** Private methods + *********/ + private AnimalHouseFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BasicProjectileFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BasicProjectileFacade.cs new file mode 100644 index 000000000..cefb0be29 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BasicProjectileFacade.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Projectiles; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BasicProjectileFacade : BasicProjectile, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static BasicProjectile Constructor(int damageToFarmer, int parentSheetIndex, int bouncesTillDestruct, int tailLength, float rotationVelocity, float xVelocity, float yVelocity, Vector2 startingPosition, string collisionSound, string firingSound, bool explode, bool damagesMonsters = false, GameLocation? location = null, Character? firer = null, bool spriteFromObjectSheet = false, onCollisionBehavior? collisionBehavior = null) + { + var projectile = new BasicProjectile( + damageToFarmer: damageToFarmer, + spriteIndex: parentSheetIndex, + bouncesTillDestruct: bouncesTillDestruct, + tailLength: tailLength, + rotationVelocity: rotationVelocity, + xVelocity: xVelocity, + yVelocity: yVelocity, + startingPosition: startingPosition + ); + + projectile.explode.Value = explode; + projectile.collisionSound.Value = collisionSound; + projectile.damagesMonsters.Value = damagesMonsters; + projectile.theOneWhoFiredMe.Set(location, firer); + projectile.itemId.Value = spriteFromObjectSheet ? parentSheetIndex.ToString() : null; + projectile.collisionBehavior = collisionBehavior; + + if (!string.IsNullOrWhiteSpace(firingSound) && location != null) + location.playSound(firingSound); + + return projectile; + } + + public static BasicProjectile Constructor(int damageToFarmer, int parentSheetIndex, int bouncesTillDestruct, int tailLength, float rotationVelocity, float xVelocity, float yVelocity, Vector2 startingPosition) + { + return Constructor(damageToFarmer, parentSheetIndex, bouncesTillDestruct, tailLength, rotationVelocity, xVelocity, yVelocity, startingPosition, "flameSpellHit", "flameSpell", true); + } + + + /********* + ** Private methods + *********/ + private BasicProjectileFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BoatTunnelFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BoatTunnelFacade.cs new file mode 100644 index 000000000..f76f8ca21 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BoatTunnelFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Locations; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BoatTunnelFacade : BoatTunnel, IRewriteFacade + { + /********* + ** Public methods + *********/ + public int GetTicketPrice() + { + return base.TicketPrice; + } + + + /********* + ** Private methods + *********/ + private BoatTunnelFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs index 5c240ef18..a3e1a37f6 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BootsFacade.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; using StardewValley.Objects; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -8,6 +9,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class BootsFacade : Boots, IRewriteFacade { @@ -19,6 +22,16 @@ public static Boots Constructor(int which) return new Boots(which.ToString()); } + public virtual void onEquip() + { + base.onEquip(Game1.player); + } + + public virtual void onUnequip() + { + base.onUnequip(Game1.player); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs index e9eaea59b..244842e68 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BreakableContainer.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; +using StardewValley.Locations; using StardewValley.Objects; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -16,6 +18,25 @@ public class BreakableContainerFacade : BreakableContainer, IRewriteFacade /********* ** Public methods *********/ + public static BreakableContainer Constructor(Vector2 tile, int type, MineShaft mine) + { + var container = BreakableContainer.GetBarrelForMines(tile, mine); + + if (type.ToString() != BreakableContainer.barrelId) + { +#pragma warning disable CS0618 // obsolete code -- it's used for its intended purpose here + container.SetIdAndSprite(type); +#pragma warning restore CS0618 + } + + return container; + } + + public static BreakableContainer Constructor(Vector2 tile, bool isVolcano) + { + return BreakableContainer.GetBarrelForVolcanoDungeon(tile); + } + public void releaseContents(GameLocation location, Farmer who) { base.releaseContents(who); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffsDisplayFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffsDisplayFacade.cs new file mode 100644 index 000000000..3d2cd528e --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuffsDisplayFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class BuffsDisplayFacade : BuffsDisplay, IRewriteFacade + { + /********* + ** Public methods + *********/ + public bool hasBuff(int which) + { + return Game1.player.hasBuff(which.ToString()); + } + + + /********* + ** Private methods + *********/ + private BuffsDisplayFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs index 6c21ee719..eef69d46a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BuildingFacade.cs @@ -1,7 +1,9 @@ +using System; using System.Diagnostics.CodeAnalysis; using Netcode; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; +using StardewValley; using StardewValley.Buildings; using StardewValley.Objects; @@ -22,6 +24,36 @@ public class BuildingFacade : Building, IRewriteFacade public NetRef input => NetRefWrapperCache.GetCachedWrapperFor(base.GetBuildingChest("Input")); // Mill public NetRef output => NetRefWrapperCache.GetCachedWrapperFor(base.GetBuildingChest("Output")); // Mill + public string nameOfIndoors + { + get + { + GameLocation? indoorLocation = base.GetIndoors(); + return indoorLocation is not null + ? indoorLocation.uniqueName.Value + : "null"; + } + } + + public string nameOfIndoorsWithoutUnique => base.GetData()?.IndoorMap ?? "null"; + + + /********* + ** Public methods + *********/ + public string getNameOfNextUpgrade() + { + string type = base.buildingType.Value; + + foreach (var pair in Game1.buildingData) + { + if (string.Equals(type, pair.Value?.BuildingToUpgrade, StringComparison.OrdinalIgnoreCase)) + return pair.Key; + } + + return "well"; // previous default + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs index 7857aaf23..e5d5356c0 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/BushFacade.cs @@ -18,6 +18,16 @@ public class BushFacade : Bush, IRewriteFacade /********* ** Public methods *********/ + public void draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch, Vector2 tileLocation, float yDrawOffset) + { + base.draw(spriteBatch, yDrawOffset); + } + + public void draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch, Vector2 tileLocation) + { + base.draw(spriteBatch); + } + public bool inBloom(string season, int dayOfMonth) { // call new method if possible diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs index 659e58278..11a803e22 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CarpenterMenuFacade.cs @@ -22,6 +22,11 @@ public static CarpenterMenu Constructor(bool magicalConstruction = false) return new CarpenterMenu(magicalConstruction ? Game1.builder_wizard : Game1.builder_robin); } + public void setNewActiveBlueprint() + { + base.SetNewActiveBlueprint(base.Blueprint); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs index 8b11a9ad2..644385d27 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CharacterFacade.cs @@ -17,6 +17,12 @@ public class CharacterFacade : Character, IRewriteFacade /********* ** Public methods *********/ + public new int addedSpeed + { + get => (int)base.addedSpeed; + set => base.addedSpeed = value; + } + public int getStandingX() { return base.StandingPixel.X; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs index 7c18b047f..1604c33fa 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ChestFacade.cs @@ -43,11 +43,15 @@ public static Chest Constructor(Vector2 location) return new Chest { TileLocation = location }; } - public ChestFacade(int parent_sheet_index, Vector2 tile_location, int starting_lid_frame, int lid_frame_count) - : base(parent_sheet_index.ToString(), tile_location, starting_lid_frame, lid_frame_count) { } + public static Chest Constructor(int parent_sheet_index, Vector2 tile_location, int starting_lid_frame, int lid_frame_count) + { + return new Chest(parent_sheet_index.ToString(), tile_location, starting_lid_frame, lid_frame_count); + } - public ChestFacade(int coins, List items, Vector2 location, bool giftbox = false, int giftboxIndex = 0) - : base(items, location, giftbox, giftboxIndex) { } + public static Chest Constructor(int coins, List items, Vector2 location, bool giftbox = false, int giftboxIndex = 0) + { + return new Chest(items, location, giftbox, giftboxIndex); + } public void destroyAndDropContents(Vector2 pointToDropAt, GameLocation location) { @@ -59,6 +63,11 @@ public void dumpContents(GameLocation location) base.dumpContents(); } + public void updateWhenCurrentLocation(GameTime time, GameLocation environment) + { + base.updateWhenCurrentLocation(time); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CrabPotFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CrabPotFacade.cs new file mode 100644 index 000000000..a7795af0f --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CrabPotFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class CrabPotFacade : CrabPot, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static CrabPot Constructor(Vector2 tileLocation, int stack = 1) + { + return new CrabPot(); + } + + /********* + ** Private methods + *********/ + private CrabPotFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CraftingRecipeFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CraftingRecipeFacade.cs new file mode 100644 index 000000000..df5566a09 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CraftingRecipeFacade.cs @@ -0,0 +1,50 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Extensions; +using StardewValley.ItemTypeDefinitions; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class CraftingRecipeFacade : CraftingRecipe, IRewriteFacade + { + /********* + ** Public methods + *********/ + public string getNameFromIndex(int index) + { + return base.getNameFromIndex(index.ToString()); + } + + public int getSpriteIndexFromRawIndex(int index) + { + string itemId = base.getSpriteIndexFromRawIndex(index.ToString()); + ParsedItemData? data = ItemRegistry.GetData(itemId); + + return data.HasTypeObject() + ? data.SpriteIndex + : index; + } + + public static bool isThereSpecialIngredientRule(Object potentialIngredient, int requiredIngredient) + { + return CraftingRecipe.isThereSpecialIngredientRule(potentialIngredient, requiredIngredient.ToString()); + } + + + /********* + ** Private methods + *********/ + private CraftingRecipeFacade() + : base(null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs index d18df7e3f..100b112bd 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/CropFacade.cs @@ -8,6 +8,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class CropFacade : Crop, IRewriteFacade { @@ -24,6 +25,11 @@ public static Crop Constructor(int seedIndex, int tileX, int tileY) return new Crop(seedIndex.ToString(), tileX, tileY, Game1.currentLocation); } + public void newDay(int state, int fertilizer, int xTile, int yTile, GameLocation environment) + { + base.newDay(state); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DebuffingProjectileFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DebuffingProjectileFacade.cs new file mode 100644 index 000000000..38cf0a9ef --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DebuffingProjectileFacade.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Projectiles; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class DebuffingProjectileFacade : DebuffingProjectile, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static DebuffingProjectile Constructor(int debuff, int parentSheetIndex, int bouncesTillDestruct, int tailLength, float rotationVelocity, float xVelocity, float yVelocity, Vector2 startingPosition, GameLocation? location = null, Character? owner = null) + { + return new DebuffingProjectile( + debuff: debuff.ToString(), + spriteIndex: parentSheetIndex, + bouncesTillDestruct: bouncesTillDestruct, + tailLength: tailLength, + rotationVelocity: rotationVelocity, + xVelocity: xVelocity, + yVelocity: yVelocity, + startingPosition: startingPosition, + location: location, + owner: owner + ); + } + + + /********* + ** Private methods + *********/ + private DebuffingProjectileFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs index d6de9253f..6021efaaa 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DelayedActionFacade.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; @@ -15,6 +16,11 @@ public class DelayedActionFacade : DelayedAction, IRewriteFacade /********* ** Public methods *********/ + public new static void functionAfterDelay(Action func, int timer) + { + DelayedAction.functionAfterDelay(func, timer); + } + public static void playSoundAfterDelay(string soundName, int timer, GameLocation? location = null, int pitch = -1) { DelayedAction.playSoundAfterDelay(soundName, timer, location, pitch: pitch); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DiscreteColorPickerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DiscreteColorPickerFacade.cs new file mode 100644 index 000000000..a3397f9b1 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/DiscreteColorPickerFacade.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class DiscreteColorPickerFacade : DiscreteColorPicker, IRewriteFacade + { + /********* + ** Public methods + *********/ + public new int getSelectionFromColor(Color c) + { + return DiscreteColorPicker.getSelectionFromColor(c); + } + + public new Color getColorFromSelection(int selection) + { + return DiscreteColorPicker.getColorFromSelection(selection); + } + + + /********* + ** Private methods + *********/ + public DiscreteColorPickerFacade() + : base(0, 0) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs index f507249d5..ee4df5c8f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmAnimalFacade.cs @@ -23,6 +23,11 @@ public bool isCoopDweller() return data?.House == "Coop"; } + public void warpHome(Farm f, FarmAnimal a) + { + base.warpHome(); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmFacade.cs new file mode 100644 index 000000000..eed21521d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmFacade.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FarmFacade : Farm, IRewriteFacade + { + /********* + ** Public methods + *********/ + public Point GetPetStartLocation() + { + var petBowl = Game1.player?.getPet()?.GetPetBowl(); + if (petBowl is not null) + return new Point(petBowl.tileX - 1, petBowl.tileY + 1); + + var petBowlPosition = base.GetStarterPetBowlLocation(); + return new Point((int)petBowlPosition.X - 1, (int)petBowlPosition.Y + 1); + } + + + /********* + ** Private methods + *********/ + private FarmFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs index 6452db005..ae6bcb63c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerFacade.cs @@ -5,6 +5,7 @@ using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; using StardewValley; +using SObject = StardewValley.Object; #pragma warning disable CS0618 // Type or member is obsolete: this is backwards-compatibility code. #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -13,9 +14,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class FarmerFacade : Farmer, IRewriteFacade { /********* @@ -23,12 +26,55 @@ public class FarmerFacade : Farmer, IRewriteFacade *********/ public NetObjectList items => InventoryToNetObjectList.GetCachedWrapperFor(base.Items); + public int attack => base.buffs.Attack; + public int immunity => base.buffs.Immunity; + public int resilience => base.buffs.Defense; + + public float attackIncreaseModifier => base.buffs.AttackMultiplier; + public float critChanceModifier => base.buffs.CriticalChanceMultiplier; + public float critPowerModifier => base.buffs.CriticalPowerMultiplier; + public float knockbackModifier => base.buffs.KnockbackMultiplier; + public float weaponPrecisionModifier => base.buffs.WeaponPrecisionMultiplier; + public float weaponSpeedModifier => base.buffs.WeaponSpeedMultiplier; + public new IList Items { get => base.Items; set => base.Items.OverwriteWith(value); } + public new int toolPower + { + get => base.toolPower.Value; + set => base.toolPower.Value = value; + } + + public new int toolHold + { + get => base.toolHold.Value; + set => base.toolHold.Value = value; + } + + public int visibleQuestCount + { + get + { + int count = 0; + foreach (var quest in base.team.specialOrders) + { + if (quest?.IsHidden() is false) + count++; + } + + foreach (var quest in base.questLog) + { + if (quest?.IsHidden() is false) + count++; + } + return count; + } + } + /********* ** Public methods @@ -38,6 +84,16 @@ public void addQuest(int questID) base.addQuest(questID.ToString()); } + public bool areAllItemsNull() + { + return base.Items.CountItemStacks() == 0; + } + + public bool caughtFish(int index, int size, bool from_fish_pond = false, int numberCaught = 1) + { + return base.caughtFish(index.ToString(), size, from_fish_pond, numberCaught); + } + public void changePants(Color color) { base.changePantsColor(color); @@ -63,11 +119,26 @@ public void completeQuest(int questID) base.completeQuest(questID.ToString()); } + public void cookedRecipe(int index) + { + base.cookedRecipe(index.ToString()); + } + public bool couldInventoryAcceptThisObject(int index, int stack, int quality = 0) { return base.couldInventoryAcceptThisItem(index.ToString(), stack, quality); } + public void foundArtifact(int index, int number) + { + base.foundArtifact(index.ToString(), number); + } + + public void foundMineral(int index) + { + base.foundMineral(index.ToString()); + } + public int GetEffectsOfRingMultiplier(int ring_index) { return base.GetEffectsOfRingMultiplier(ring_index.ToString()); @@ -85,6 +156,16 @@ public bool hasBuff(int whichBuff) return base.hasBuff(whichBuff.ToString()); } + public bool hasGiftTasteBeenRevealed(NPC npc, int item_index) + { + return base.hasGiftTasteBeenRevealed(npc, item_index.ToString()); + } + + public bool hasItemBeenGifted(NPC npc, int item_index) + { + return base.hasItemBeenGifted(npc, item_index.ToString()); + } + public bool hasItemInInventory(int itemIndex, int quantity, int minPrice = 0) { // minPrice field was always ignored @@ -102,16 +183,51 @@ public bool hasItemInInventory(int itemIndex, int quantity, int minPrice = 0) } } + public bool hasItemInInventoryNamed(string? name) + { + if (name is not null) + { + foreach (Item item in base.Items) + { + if (item?.Name == name) + return true; + } + } + + return false; + } + + public Item? hasItemWithNameThatContains(string name) + { + foreach (Item item in base.Items) + { + if (item?.Name is not null && item.Name.Contains(name)) + return item; + } + + return null; + } + public bool hasQuest(int id) { return base.hasQuest(id.ToString()); } + public bool isMarried() + { + return base.isMarriedOrRoommates(); + } + public bool isWearingRing(int ringIndex) { return base.isWearingRing(ringIndex.ToString()); } + public void removeFirstOfThisItemFromInventory(int parentSheetIndexOfItem) + { + base.removeFirstOfThisItemFromInventory(parentSheetIndexOfItem.ToString()); + } + public bool removeItemsFromInventory(int index, int stack) { if (this.hasItemInInventory(index, stack)) @@ -129,7 +245,7 @@ public bool removeItemsFromInventory(int index, int stack) default: for (int i = 0; i < base.Items.Count; i++) { - if (base.Items[i] is Object obj && obj.parentSheetIndex == index) + if (base.Items[i] is SObject obj && obj.parentSheetIndex == index) { if (obj.Stack > stack) { @@ -156,6 +272,17 @@ public void removeQuest(int questID) base.removeQuest(questID.ToString()); } + public void revealGiftTaste(NPC npc, int parent_sheet_index) + { + base.revealGiftTaste(npc.Name, parent_sheet_index.ToString()); + } + + public void revealGiftTaste(NPC npc, SObject item) + { + if (!item.bigCraftable) + base.revealGiftTaste(npc.Name, item.ItemId); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerRendererFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerRendererFacade.cs new file mode 100644 index 000000000..37a19f18d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerRendererFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FarmerRendererFacade : FarmerRenderer, IRewriteFacade + { + /********* + ** Public methods + *********/ + public void recolorShoes(int which) + { + base.recolorShoes(which.ToString()); + } + + + /********* + ** Private methods + *********/ + private FarmerRendererFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs index 19091c60c..6cc3e628e 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FarmerTeamFacade.cs @@ -21,6 +21,15 @@ public class FarmerTeamFacade : FarmerTeam, IRewriteFacade public NetObjectList junimoChest => InventoryToNetObjectList.GetCachedWrapperFor(base.GetOrCreateGlobalInventory(FarmerTeam.GlobalInventoryId_JunimoChest)); + /********* + ** Public methods + *********/ + public void SetLocalReady(string checkName, bool ready) + { + Game1.netReady.SetLocalReady(checkName, ready); + } + + /********* ** Private methods *********/ diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs index 802280120..7f099e88c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FenceFacade.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; @@ -15,11 +16,21 @@ public class FenceFacade : Fence, IRewriteFacade /********* ** Public methods *********/ + public static Fence Constructor(Vector2 tileLocation, int whichType, bool isGate) + { + return new Fence(tileLocation, whichType.ToString(), isGate); + } + public void toggleGate(GameLocation location, bool open, bool is_toggling_counterpart = false, Farmer? who = null) { base.toggleGate(open, is_toggling_counterpart, who); } + public int getDrawSum(GameLocation location) + { + return base.getDrawSum(); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishTankFurnitureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishTankFurnitureFacade.cs new file mode 100644 index 000000000..5db1b8e26 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishTankFurnitureFacade.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FishTankFurnitureFacade : FishTankFurniture, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static FishTankFurniture Constructor(int which, Vector2 tile, int initialRotations) + { + return new FishTankFurniture(which.ToString(), tile, initialRotations); + } + + public static FishTankFurniture Constructor(int which, Vector2 tile) + { + return new FishTankFurniture(which.ToString(), tile); + } + + + /********* + ** Private methods + *********/ + private FishTankFurnitureFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs new file mode 100644 index 000000000..ebaf4caab --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Tools; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class FishingRodFacade : FishingRod, IRewriteFacade + { + /********* + ** Public methods + *********/ + public int getBaitAttachmentIndex() + { + return int.TryParse(base.GetBait()?.ItemId, out int index) + ? index + : -1; + } + + public int getBobberAttachmentIndex() + { + return int.TryParse(base.GetTackle()?.ItemId, out int index) + ? index + : -1; + } + + public void pullFishFromWater(int whichFish, int fishSize, int fishQuality, int fishDifficulty, bool treasureCaught, bool wasPerfect, bool fromFishPond, bool caughtDouble = false, string itemCategory = "Object") + { + base.pullFishFromWater(whichFish.ToString(), fishSize, fishQuality, fishDifficulty, treasureCaught, wasPerfect, fromFishPond, null, false, caughtDouble); + } + + + /********* + ** Private methods + *********/ + private FishingRodFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs index e702fe1c9..16f8eace8 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ForestFacade.cs @@ -10,6 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class ForestFacade : Forest, IRewriteFacade { @@ -20,7 +21,7 @@ public ResourceClump? log { get { - foreach (ResourceClump clump in this.resourceClumps) + foreach (ResourceClump clump in base.resourceClumps) { if (clump.parentSheetIndex.Value == ResourceClump.hollowLogIndex && (int)clump.Tile.X == 2 && (int)clump.Tile.Y == 6) return clump; @@ -33,11 +34,11 @@ public ResourceClump? log // remove previous value ResourceClump? clump = this.log; if (clump != null) - this.resourceClumps.Remove(clump); + base.resourceClumps.Remove(clump); // add new value if (value != null) - this.resourceClumps.Add(value); + base.resourceClumps.Add(value); } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs index 0aebc33cd..3132ffb91 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FruitTreeFacade.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; using Netcode; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6.Internal; using StardewValley; +using StardewValley.Objects; using StardewValley.TerrainFeatures; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -34,6 +36,30 @@ public NetString fruitSeason } + /********* + ** Public methods + *********/ + public static FruitTree Constructor(int saplingIndex) + { + return new FruitTree(saplingIndex.ToString()); + } + + public static FruitTree Constructor(int saplingIndex, int growthStage) + { + return new FruitTree(saplingIndex.ToString(), growthStage); + } + + public bool IsInSeasonHere(GameLocation location) + { + return base.IsInSeasonHere(); + } + + public void shake(Vector2 tileLocation, bool doEvenIfStillShaking, GameLocation location) + { + base.shake(tileLocation, doEvenIfStillShaking); + } + + /********* ** Private methods *********/ diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs index be532edb0..9d2b394ce 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; @@ -11,14 +13,17 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class Game1Facade : IRewriteFacade + public class Game1Facade : Game1, IRewriteFacade { /********* ** Accessors *********/ - [SuppressMessage("ReSharper", "ValueParameterNotUsed")] - public static bool menuUp { get; set; } // field was mostly unused and always false + public bool gamePadControlsImplemented { get; set; } // never used + public static bool menuUp { get; set; } // mostly unused and always false + public static Color morningColor { get; set; } = Color.LightBlue; // never used /********* @@ -31,17 +36,6 @@ public static bool canHaveWeddingOnDay(int day, string season) && Game1.canHaveWeddingOnDay(day, parsedSeason); } - public static NPC getCharacterFromName(string name, bool mustBeVillager = true, bool useLocationsListOnly = false) - { - return Game1.getCharacterFromName(name, mustBeVillager); - } - - public static string GetSeasonForLocation(GameLocation location) - { - Season season = Game1.GetSeasonForLocation(location); - return season.ToString(); - } - public static void createMultipleObjectDebris(int index, int xTile, int yTile, int number) { Game1.createMultipleObjectDebris(index.ToString(), xTile, yTile, number); @@ -87,6 +81,21 @@ public static void createObjectDebris(int objectIndex, int xTile, int yTile, int Game1.createObjectDebris(objectIndex.ToString(), xTile, yTile, groundLevel, itemQuality, velocityMultiplyer, location); } + public static void createRadialDebris(GameLocation location, int debrisType, int xTile, int yTile, int numberOfChunks, bool resource, int groundLevel = -1, bool item = false, int color = -1) + { + Game1.createRadialDebris( + location: location, + debrisType: debrisType, + xTile: xTile, + yTile: yTile, + numberOfChunks: numberOfChunks, + resource: resource, + groundLevel: groundLevel, + item: item, + color: Debris.getColorForDebris(color) + ); + } + public static void drawDialogue(NPC speaker, string dialogue) { Game1.DrawDialogue(new Dialogue(speaker, null, dialogue)); @@ -97,6 +106,42 @@ public static void drawDialogue(NPC speaker, string dialogue, Texture2D override Game1.DrawDialogue(new Dialogue(speaker, null, dialogue) { overridePortrait = overridePortrait }); } + public static void drawObjectQuestionDialogue(string dialogue, List? choices, int width) + { + Game1.drawObjectQuestionDialogue(dialogue, choices?.ToArray(), width); + } + + public static void drawObjectQuestionDialogue(string dialogue, List? choices) + { + Game1.drawObjectQuestionDialogue(dialogue, choices?.ToArray()); + } + + public static NPC getCharacterFromName(string name, bool mustBeVillager = true, bool useLocationsListOnly = false) + { + return Game1.getCharacterFromName(name, mustBeVillager); + } + + public new static string GetSeasonForLocation(GameLocation location) + { + Season season = Game1.GetSeasonForLocation(location); + return season.ToString(); + } + + public static void playMorningSong() + { + Game1.playMorningSong(); + } + + public static void playSound(string cueName) + { + Game1.playSound(cueName); + } + + public static void playSoundPitched(string cueName, int pitch) + { + Game1.playSound(cueName, pitch); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs index 8b9de1c74..9ba4d883b 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GameLocationFacade.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Netcode; @@ -22,6 +23,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class GameLocationFacade : GameLocation, IRewriteFacade { + /********* + ** Accessors + *********/ + public static int CAROLINES_NECKLACE_ITEM => 191; + + /********* ** Public methods *********/ @@ -35,11 +42,35 @@ public virtual int getExtraMillisecondsPerInGameMinuteForThisLocation() return base.ExtraMillisecondsPerInGameMinute; } + public Object? getFish(float millisecondsAfterNibble, int bait, int waterDepth, Farmer who, double baitPotency, Vector2 bobberTile, string? location = null) + { + return base.getFish(millisecondsAfterNibble, bait.ToString(), waterDepth, who, baitPotency, bobberTile, location) as Object; + } + + public Dictionary GetLocationEvents() + { + return base.TryGetLocationEvents(out _, out Dictionary events) + ? events + : new Dictionary(); + } + + public Point GetMapPropertyPosition(string key, int default_x, int default_y) + { + return base.TryGetMapPropertyAs(key, out Point point) + ? point + : new Point(default_x, default_y); + } + public int getNumberBuildingsConstructed(string name) { return base.getNumberBuildingsConstructed(name, includeUnderConstruction: false); } + public Object getObjectAtTile(int x, int y) + { + return base.getObjectAtTile(x, y); + } + public string GetSeasonForLocation() { return base.GetSeasonKey(); @@ -57,7 +88,7 @@ public bool isTileLocationTotallyClearAndPlaceable(int x, int y) public bool isTileLocationTotallyClearAndPlaceable(Vector2 v) { - Vector2 pixel = new Vector2((v.X * Game1.tileSize) + Game1.tileSize / 2, (v.Y * Game1.tileSize) + Game1.tileSize / 2); + Vector2 pixel = new((v.X * Game1.tileSize) + Game1.tileSize / 2, (v.Y * Game1.tileSize) + Game1.tileSize / 2); foreach (Furniture f in base.furniture) { if (f.furniture_type != Furniture.rug && !f.isPassable() && f.GetBoundingBox().Contains((int)pixel.X, (int)pixel.Y) && !f.AllowPlacementOnThisTile((int)v.X, (int)v.Y)) @@ -98,6 +129,11 @@ public void localSoundAt(string audioName, Vector2 position) base.localSound(audioName, position); } + public void OnStoneDestroyed(int indexOfStone, int x, int y, Farmer who) + { + base.OnStoneDestroyed(indexOfStone.ToString(), x, y, who); + } + public void playSound(string audioName, SoundContext soundContext = SoundContext.Default) { base.playSound(audioName, context: soundContext); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GiantCropFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GiantCropFacade.cs new file mode 100644 index 000000000..89267b95d --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/GiantCropFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class GiantCropFacade : GiantCrop, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static GiantCrop Constructor(int indexOfSmallerVersion, Vector2 tile) + { + return new GiantCrop(indexOfSmallerVersion.ToString(), tile); + } + + + /********* + ** Private methods + *********/ + private GiantCropFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs index 96deb8fe3..c824f2934 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HoeDirtFacade.cs @@ -11,17 +11,28 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "ParameterHidesMember", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class HoeDirtFacade : HoeDirt, IRewriteFacade { /********* ** Public methods *********/ + public bool canPlantThisSeedHere(int objectIndex, int tileX, int tileY, bool isFertilizer = false) + { + return base.canPlantThisSeedHere(objectIndex.ToString(), isFertilizer); + } + public void destroyCrop(Vector2 tileLocation, bool showAnimation, GameLocation location) { base.destroyCrop(showAnimation); } + public Rectangle GetFertilizerSourceRect(int fertilizer) + { + return base.GetFertilizerSourceRect(); + } + public bool paddyWaterCheck(GameLocation location, Vector2 tile_location) { return base.paddyWaterCheck(); @@ -32,6 +43,11 @@ public bool plant(int index, int tileX, int tileY, Farmer who, bool isFertilizer return base.plant(index.ToString(), who, isFertilizer); } + public void updateNeighbors(GameLocation loc, Vector2 tilePos) + { + base.updateNeighbors(); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs index c23096e71..e85c27c7d 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/HudMessageFacade.cs @@ -11,10 +11,21 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] public class HudMessageFacade : HUDMessage, IRewriteFacade { + /********* + ** Accessors + *********/ + public string Message + { + get => base.message; + set => base.message = value; + } + + /********* ** Public methods *********/ diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs index 82dfa10b9..12bce5794 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/IClickableMenuFacade.cs @@ -15,7 +15,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class IClickableMenuFacade : IRewriteFacade + public class IClickableMenuFacade : IClickableMenu, IRewriteFacade { /********* ** Public methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ImplicitConversionOperators.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ImplicitConversionOperators.cs new file mode 100644 index 000000000..c2f7d76ce --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ImplicitConversionOperators.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Network; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Reimplements implicit conversion operators that were removed in 1.6 for sealed classes. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public static class ImplicitConversionOperators + { + public static int NetDirection_ToInt(NetDirection netField) + { + return netField.Value; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LargeTerrainFeatureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LargeTerrainFeatureFacade.cs new file mode 100644 index 000000000..5f487b9f5 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LargeTerrainFeatureFacade.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.TerrainFeatures; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class LargeTerrainFeatureFacade : LargeTerrainFeature, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetVector2 tilePosition => base.netTilePosition; + + + /********* + ** Private methods + *********/ + private LargeTerrainFeatureFacade() + : base(false) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LayerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LayerFacade.cs new file mode 100644 index 000000000..ad6b8cdff --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LayerFacade.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using xTile.Dimensions; +using xTile.Display; +using xTile.Layers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = SuppressReasons.MatchesOriginal)] + public class LayerFacade : Layer, IRewriteFacade + { + /********* + ** Public methods + *********/ + public void Draw(IDisplayDevice displayDevice, Rectangle mapViewport, Location displayOffset, bool wrapAround, int pixelZoom) + { + base.Draw(displayDevice, mapViewport, displayOffset, wrapAround, pixelZoom); + } + + + /********* + ** Private methods + *********/ + private LayerFacade() + : base(null, null, Size.Zero, Size.Zero) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LibraryMuseumFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LibraryMuseumFacade.cs new file mode 100644 index 000000000..620922532 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LibraryMuseumFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Locations; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class LibraryMuseumFacade : LibraryMuseum, IRewriteFacade + { + /********* + ** Public methods + *********/ + public bool museumAlreadyHasArtifact(int index) + { + return LibraryMuseum.HasDonatedArtifact(index.ToString()); + } + + + /********* + ** Private methods + *********/ + private LibraryMuseumFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs index 9c6279c9c..1bbb1285a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MeleeWeaponFacade.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; using StardewValley.Tools; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -8,6 +10,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class MeleeWeaponFacade : MeleeWeapon, IRewriteFacade { @@ -19,6 +22,18 @@ public static MeleeWeapon Constructor(int spriteIndex) return new MeleeWeapon(spriteIndex.ToString()); } + public bool isScythe(int index = -1) + { + return base.isScythe(); // index argument was already ignored + } + + public static Rectangle getSourceRect(int index) + { + return + ItemRegistry.GetData(ItemRegistry.type_weapon + index)?.GetSourceRect() // get actual source rect if possible + ?? Game1.getSourceRectForStandardTileSheet(Tool.weaponsTexture, index, Game1.smallestTileSize, Game1.smallestTileSize); // else pre-1.6 logic + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MineShaftFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MineShaftFacade.cs new file mode 100644 index 000000000..50b71a197 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MineShaftFacade.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Locations; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class MineShaftFacade : MineShaft, IRewriteFacade + { + /********* + ** Public methods + *********/ + public new int getRandomGemRichStoneForThisLevel(int level) + { + string itemId = base.getRandomGemRichStoneForThisLevel(level); + + return int.TryParse(itemId, out int index) + ? index + : Object.mineStoneBrown1Index; // old default value + } + + + /********* + ** Private methods + *********/ + private MineShaftFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MultiplayerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MultiplayerFacade.cs new file mode 100644 index 000000000..8cd63d92c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/MultiplayerFacade.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class MultiplayerFacade : Multiplayer, IRewriteFacade + { + /********* + ** Public methods + *********/ + public void broadcastSprites(GameLocation location, List sprites) + { + var list = new TemporaryAnimatedSpriteList(); + list.AddRange(sprites); + + base.broadcastSprites(location, list); + } + + public void broadcastGlobalMessage(string localization_string_key, bool only_show_if_empty = false, params string[] substitutions) + { + base.broadcastGlobalMessage(localization_string_key, only_show_if_empty, null, substitutions); + } + + + /********* + ** Private methods + *********/ + private MultiplayerFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldsFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldsFacade.cs new file mode 100644 index 000000000..35dc28e2b --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetFieldsFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class NetFieldsFacade : NetFields, IRewriteFacade + { + /********* + ** Public methods + *********/ + [SuppressMessage("ReSharper", "ForCanBeConvertedToForeach", Justification = "Deliberate to include index in field name")] + public void AddFields(params INetSerializable[] fields) + { + for (int i = 0; i < fields.Length; i++) + base.AddField(fields[i]); + } + + + /********* + ** Private methods + *********/ + private NetFieldsFacade() + : base(null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetWorldStateFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetWorldStateFacade.cs new file mode 100644 index 000000000..4576e1fb8 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NetWorldStateFacade.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Network; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class NetWorldStateFacade : NetWorldState, IRewriteFacade + { + /********* + ** Public methods + *********/ + public new NetIntDelta MiniShippingBinsObtained => base.miniShippingBinsObtained; + public new NetIntDelta GoldenWalnutsFound => base.goldenWalnutsFound; + public new NetIntDelta GoldenWalnuts => base.goldenWalnuts; + public new NetBool GoldenCoconutCracked => base.goldenCoconutCracked; + public new NetBool ParrotPlatformsUnlocked => base.parrotPlatformsUnlocked; + public new NetIntDelta LostBooksFound => base.lostBooksFound; + + + /********* + ** Private methods + *********/ + private NetWorldStateFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs index 28e901025..42e05d12c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/NpcFacade.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; using StardewValley.BellsAndWhistles; +using StardewValley.Pathfinding; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -15,9 +18,37 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public abstract class NpcFacade : NPC, IRewriteFacade { + /********* + ** Accessors + *********/ + public new int Gender + { + get => (int)base.Gender; + set => base.Gender = (Gender)value; + } + + /********* ** Public methods *********/ + public static NPC Constructor(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDirection, string name, bool datable, Dictionary schedule, Texture2D portrait) + { + return new NPC(sprite, position, defaultMap, facingDirection, name, datable, portrait); + } + + public static NPC Constructor(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDir, string name, Dictionary schedule, Texture2D portrait, bool eventActor, string? syncedPortraitPath = null) + { + NPC npc = new NPC(sprite, position, defaultMap, facingDir, name, portrait, eventActor); + + if (!string.IsNullOrWhiteSpace(syncedPortraitPath)) + { + npc.Portrait = Game1.content.Load(syncedPortraitPath); + npc.portraitOverridden = true; + } + + return npc; + } + public bool isBirthday(string season, int day) { // call new method if possible @@ -31,6 +62,11 @@ public bool isBirthday(string season, int day) && base.Birthday_Day == day; } + public static void populateRoutesFromLocationToLocationList() + { + WarpPathfindingCache.PopulateCache(); + } + public void showTextAboveHead(string Text, int spriteTextColor = -1, int style = NPC.textStyle_none, int duration = 3000, int preTimer = 0) { Color? color = spriteTextColor != -1 ? SpriteText.getColorFromIndex(spriteTextColor) : null; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs index 4b6fa2254..994dd3190 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs @@ -57,11 +57,26 @@ public void ApplySprinkler(GameLocation location, Vector2 tile) base.ApplySprinkler(tile); } + public void ApplySprinklerAnimation(GameLocation location) + { + base.ApplySprinklerAnimation(); + } + + public new void ConsumeInventoryItem(Farmer who, Item drop_in, int amount) + { + Object.ConsumeInventoryItem(who, drop_in, amount); + } + public void DayUpdate(GameLocation location) { base.DayUpdate(); } + public void farmerAdjacentAction(GameLocation location) + { + base.farmerAdjacentAction(); + } + public Rectangle getBoundingBox(Vector2 tileLocation) { return base.GetBoundingBoxAt((int)tileLocation.X, (int)tileLocation.Y); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProfileMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProfileMenuFacade.cs new file mode 100644 index 000000000..0fff5ff37 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProfileMenuFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ProfileMenuFacade : ProfileMenu, IRewriteFacade + { + /********* + ** Public methods + *********/ + public Character? GetCharacter() + { + return base.Current?.Character; + } + + + /********* + ** Private methods + *********/ + private ProfileMenuFacade() + : base(null, null) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProjectileFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProjectileFacade.cs new file mode 100644 index 000000000..f425ff386 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ProjectileFacade.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Projectiles; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public abstract class ProjectileFacade : Projectile, IRewriteFacade + { + /********* + ** Accessors + *********/ + public static int boundingBoxHeight { get; set; } = Game1.tileSize / 3; // field was never used + + + /********* + ** Private methods + *********/ + private ProjectileFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/QuestFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/QuestFacade.cs new file mode 100644 index 000000000..5f4fed0a9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/QuestFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Quests; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class QuestFacade : Quest, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Quest getQuestFromId(int id) + { + return Quest.getQuestFromId(id.ToString()); + } + + + /********* + ** Private methods + *********/ + private QuestFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs index 2f891f229..84b6e5f8d 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/RingFacade.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; using StardewValley.Objects; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -8,6 +9,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class RingFacade : Ring, IRewriteFacade { @@ -19,6 +21,21 @@ public static Ring Constructor(int which) return new Ring(which.ToString()); } + public virtual bool GetsEffectOfRing(int ring_index) + { + return base.GetsEffectOfRing(ring_index.ToString()); + } + + public virtual void onEquip(Farmer who, GameLocation location) + { + base.onEquip(who); + } + + public virtual void onUnequip(Farmer who, GameLocation location) + { + base.onUnequip(who); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs index 688d8495e..9fa40ea09 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ShopMenuFacade.cs @@ -16,6 +16,16 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class ShopMenuFacade : ShopMenu, IRewriteFacade { + /********* + ** Accessors + *********/ + public string storeContext + { + get => base.ShopId; + set => base.ShopId = value; + } + + /********* ** Public methods *********/ diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SignFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SignFacade.cs new file mode 100644 index 000000000..c25187e23 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SignFacade.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class SignFacade : Sign, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static Sign Constructor(Vector2 tile, int which) + { + return new Sign(tile, which.ToString()); + } + + + /********* + ** Private methods + *********/ + private SignFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SoundEffectFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SoundEffectFacade.cs new file mode 100644 index 000000000..b79f823bc --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SoundEffectFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.Xna.Framework.Audio; +using StardewModdingAPI.Framework.ModLoading.Framework; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class SoundEffectFacade : SoundEffect, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static SoundEffect FromStream(Stream stream) + { + return SoundEffect.FromStream(stream); + } + + + /********* + ** Private methods + *********/ + private SoundEffectFacade() + : base(null, 0, AudioChannels.Mono) + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs index a189f6b06..89f6f059c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/SpriteTextFacade.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley.BellsAndWhistles; -using static StardewValley.BellsAndWhistles.SpriteText; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -13,7 +12,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class SpriteTextFacade : IRewriteFacade + public class SpriteTextFacade : SpriteText, IRewriteFacade { /********* ** Public methods @@ -38,6 +37,20 @@ public static void drawString(SpriteBatch b, string s, int x, int y, int charact ); } + public static void drawStringWithScrollBackground(SpriteBatch b, string s, int x, int y, string placeHolderWidthText = "", float alpha = 1f, int color = -1, ScrollTextAlignment scroll_text_alignment = ScrollTextAlignment.Left) + { + SpriteText.drawStringWithScrollBackground( + b: b, + s: s, + x: x, + y: y, + placeHolderWidthText: placeHolderWidthText, + alpha: alpha, + color: color != -1 ? SpriteText.getColorFromIndex(color) : null, + scroll_text_alignment: scroll_text_alignment + ); + } + public static void drawStringWithScrollCenteredAt(SpriteBatch b, string s, int x, int y, int width, float alpha = 1f, int color = -1, int scrollType = SpriteText.scrollStyle_scroll, float layerDepth = .88f, bool junimoText = false) { SpriteText.drawStringWithScrollCenteredAt( @@ -70,6 +83,24 @@ public static void drawStringWithScrollCenteredAt(SpriteBatch b, string s, int x ); } + public static void drawStringHorizontallyCenteredAt(SpriteBatch b, string s, int x, int y, int characterPosition = maxCharacter, int width = -1, int height = maxHeight, float alpha = 1f, float layerDepth = .88f, bool junimoText = false, int color = -1, int maxWidth = 99999) + { + SpriteText.drawStringHorizontallyCenteredAt( + b: b, + s: s, + x: x, + y: y, + characterPosition: characterPosition, + width: width, + height: height, + alpha: alpha, + layerDepth: layerDepth, + junimoText: junimoText, + color: color != -1 ? SpriteText.getColorFromIndex(color) : null, + maxWidth: maxWidth + ); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StatsFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StatsFacade.cs new file mode 100644 index 000000000..2c927606c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/StatsFacade.cs @@ -0,0 +1,343 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class StatsFacade : Stats, IRewriteFacade + { + /********* + ** Accessors + *********/ + /**** + ** Other fields + ****/ + public new SerializableDictionary specificMonstersKilled + { + get => base.specificMonstersKilled; + } + + //started using this in 1.4 to track stats, rather than the annoying and messy uint fields above + public SerializableDictionary stat_dictionary + { + get => base.Values; + } + + /**** + ** Former uint fields + ****/ + public uint averageBedtime + { + get => base.AverageBedtime; + set => base.AverageBedtime = value; + } + + public uint beveragesMade + { + get => base.BeveragesMade; + set => base.BeveragesMade = value; + } + + public uint caveCarrotsFound + { + get => base.CaveCarrotsFound; + set => base.CaveCarrotsFound = value; + } + + public uint cheeseMade + { + get => base.CheeseMade; + set => base.CheeseMade = value; + } + + public uint chickenEggsLayed + { + get => base.ChickenEggsLayed; + set => base.ChickenEggsLayed = value; + } + + public uint copperFound + { + get => base.CopperFound; + set => base.CopperFound = value; + } + + public uint cowMilkProduced + { + get => base.CowMilkProduced; + set => base.CowMilkProduced = value; + } + + public uint cropsShipped + { + get => base.CropsShipped; + set => base.CropsShipped = value; + } + + public uint daysPlayed + { + get => base.DaysPlayed; + set => base.DaysPlayed = value; + } + + public uint diamondsFound + { + get => base.DiamondsFound; + set => base.DiamondsFound = value; + } + + public uint dirtHoed + { + get => base.DirtHoed; + set => base.DirtHoed = value; + } + + public uint duckEggsLayed + { + get => base.DuckEggsLayed; + set => base.DuckEggsLayed = value; + } + + public uint fishCaught + { + get => base.FishCaught; + set => base.FishCaught = value; + } + + public uint geodesCracked + { + get => base.GeodesCracked; + set => base.GeodesCracked = value; + } + + public uint giftsGiven + { + get => base.GiftsGiven; + set => base.GiftsGiven = value; + } + + public uint goatCheeseMade + { + get => base.GoatCheeseMade; + set => base.GoatCheeseMade = value; + } + + public uint goatMilkProduced + { + get => base.GoatMilkProduced; + set => base.GoatMilkProduced = value; + } + + public uint goldFound + { + get => base.GoldFound; + set => base.GoldFound = value; + } + + public uint goodFriends + { + get => base.GoodFriends; + set => base.GoodFriends = value; + } + + public uint individualMoneyEarned + { + get => base.IndividualMoneyEarned; + set => base.IndividualMoneyEarned = value; + } + + public uint iridiumFound + { + get => base.IridiumFound; + set => base.IridiumFound = value; + } + + public uint ironFound + { + get => base.IronFound; + set => base.IronFound = value; + } + + public uint itemsCooked + { + get => base.ItemsCooked; + set => base.ItemsCooked = value; + } + + public uint itemsCrafted + { + get => base.ItemsCrafted; + set => base.ItemsCrafted = value; + } + + public uint itemsForaged + { + get => base.ItemsForaged; + set => base.ItemsForaged = value; + } + + public uint itemsShipped + { + get => base.ItemsShipped; + set => base.ItemsShipped = value; + } + + public uint monstersKilled + { + get => base.MonstersKilled; + set => base.MonstersKilled = value; + } + + public uint mysticStonesCrushed + { + get => base.MysticStonesCrushed; + set => base.MysticStonesCrushed = value; + } + + public uint notesFound + { + get => base.NotesFound; + set => base.NotesFound = value; + } + + public uint otherPreciousGemsFound + { + get => base.OtherPreciousGemsFound; + set => base.OtherPreciousGemsFound = value; + } + + public uint piecesOfTrashRecycled + { + get => base.PiecesOfTrashRecycled; + set => base.PiecesOfTrashRecycled = value; + } + + public uint preservesMade + { + get => base.PreservesMade; + set => base.PreservesMade = value; + } + + public uint prismaticShardsFound + { + get => base.PrismaticShardsFound; + set => base.PrismaticShardsFound = value; + } + + public uint questsCompleted + { + get => base.QuestsCompleted; + set => base.QuestsCompleted = value; + } + + public uint rabbitWoolProduced + { + get => base.RabbitWoolProduced; + set => base.RabbitWoolProduced = value; + } + + public uint rocksCrushed + { + get => base.RocksCrushed; + set => base.RocksCrushed = value; + } + + public uint seedsSown + { + get => base.SeedsSown; + set => base.SeedsSown = value; + } + + public uint sheepWoolProduced + { + get => base.SheepWoolProduced; + set => base.SheepWoolProduced = value; + } + + public uint slimesKilled + { + get => base.SlimesKilled; + set => base.SlimesKilled = value; + } + + public uint stepsTaken + { + get => base.StepsTaken; + set => base.StepsTaken = value; + } + + public uint stoneGathered + { + get => base.StoneGathered; + set => base.StoneGathered = value; + } + + public uint stumpsChopped + { + get => base.StumpsChopped; + set => base.StumpsChopped = value; + } + + public uint timesFished + { + get => base.TimesFished; + set => base.TimesFished = value; + } + + public uint timesUnconscious + { + get => base.TimesUnconscious; + set => base.TimesUnconscious = value; + } + + public uint totalMoneyGifted + { + get => base.Get("totalMoneyGifted"); + set => base.Set("totalMoneyGifted", value); + } + + public uint trufflesFound + { + get => base.TrufflesFound; + set => base.TrufflesFound = value; + } + + public uint weedsEliminated + { + get => base.WeedsEliminated; + set => base.WeedsEliminated = value; + } + + + /********* + ** Public methods + *********/ + public uint getStat(string label) + { + return base.Get(label); + } + + public void incrementStat(string label, int amount) + { + base.Increment(label, amount); + } + + + + /********* + ** Private methods + *********/ + private StatsFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TemporaryAnimatedSpriteFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TemporaryAnimatedSpriteFacade.cs new file mode 100644 index 000000000..7e5ed0203 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TemporaryAnimatedSpriteFacade.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = SuppressReasons.MatchesOriginal)] + public class TemporaryAnimatedSpriteFacade : TemporaryAnimatedSprite, IRewriteFacade + { + /********* + ** Accessors + *********/ + public new float id + { + get => base.id; + set => base.id = (int)value; + } + + + /********* + ** Private methods + *********/ + private TemporaryAnimatedSpriteFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs index 959af929c..a862933b5 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TerrainFeatureFacade.cs @@ -14,6 +14,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class TerrainFeatureFacade : TerrainFeature, IRewriteFacade { + /********* + ** Accessors + *********/ + public GameLocation currentLocation + { + get => base.Location; + set => base.Location = value; + } + + public Vector2 currentTileLocation + { + get => base.Tile; + set => base.Tile = value; + } + + /********* ** Public methods *********/ diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ToolFactoryFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ToolFactoryFacade.cs new file mode 100644 index 000000000..9fe6ef980 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ToolFactoryFacade.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley; +using StardewValley.Tools; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's ToolFactory methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class ToolFactoryFacade : IRewriteFacade + { + /********* + ** Accessors + *********/ + public const byte axe = 0; + public const byte hoe = 1; + public const byte fishingRod = 2; + public const byte pickAxe = 3; + public const byte wateringCan = 4; + public const byte meleeWeapon = 5; + public const byte slingshot = 6; + + + /********* + ** Public methods + *********/ + public static Tool getToolFromDescription(byte index, int upgradeLevel) + { + Tool? t = null; + switch (index) + { + case axe: t = new Axe(); break; + case hoe: t = new Hoe(); break; + case fishingRod: t = new FishingRod(); break; + case pickAxe: t = new Pickaxe(); break; + case wateringCan: t = new WateringCan(); break; + case meleeWeapon: t = new MeleeWeapon("0"); break; + case slingshot: t = new Slingshot(); break; + } + t.UpgradeLevel = upgradeLevel; + return t; + } + + + /********* + ** Private methods + *********/ + private ToolFactoryFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs index 5ac0aca41..da7271211 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TreeFacade.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; using StardewValley.TerrainFeatures; @@ -31,6 +32,11 @@ public bool fertilize(GameLocation location) return base.fertilize(); } + public bool instantDestroy(Vector2 tileLocation, GameLocation location) + { + return base.instantDestroy(tileLocation); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TvFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TvFacade.cs new file mode 100644 index 000000000..235dc874f --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/TvFacade.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class TvFacade : TV, IRewriteFacade + { + /********* + ** Public methods + *********/ + public static TV Constructor(int which, Vector2 tile) + { + return new TV(which.ToString(), tile); + } + + + /********* + ** Private methods + *********/ + private TvFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs index 026436047..af21d0f06 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/UtilityFacade.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley; +using StardewValley.Buildings; using StardewValley.Extensions; +using SObject = StardewValley.Object; #pragma warning disable CS0618 // Type or member is obsolete: this is backwards-compatibility code. #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -12,14 +16,37 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 { /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] - public class UtilityFacade : IRewriteFacade + public class UtilityFacade : Utility, IRewriteFacade { /********* ** Public methods *********/ - public static DisposableList getAllCharacters() + public static bool doesItemWithThisIndexExistAnywhere(int index, bool bigCraftable = false) + { + bool found = false; + + Utility.ForEachItem(item => + { + found = item is SObject obj && obj.bigCraftable.Value == bigCraftable && obj.ParentSheetIndex == index; + return !found; + }); + + return found; + } + + public static void ForAllLocations(Action action) + { + Utility.ForEachLocation(location => + { + action(location); + return true; + }); + } + + public new static DisposableList getAllCharacters() { return new DisposableList(Utility.getAllCharacters()); } @@ -30,11 +57,43 @@ public static List getAllCharacters(List list) return list; } + public new static IEnumerable GetHorseWarpRestrictionsForFarmer(Farmer who) + { + Utility.HorseWarpRestrictions restrictions = Utility.GetHorseWarpRestrictionsForFarmer(who); + + if (restrictions.HasFlag(Utility.HorseWarpRestrictions.NoOwnedHorse)) + yield return 1; + if (restrictions.HasFlag(Utility.HorseWarpRestrictions.Indoors)) + yield return 2; + if (restrictions.HasFlag(Utility.HorseWarpRestrictions.NoRoom)) + yield return 3; + if (restrictions.HasFlag(Utility.HorseWarpRestrictions.InUse)) + yield return 3; + } + public static T GetRandom(List list, Random? random = null) { return (random ?? Game1.random).ChooseFrom(list); } + public static NPC? getTodaysBirthdayNPC(string season, int day) + { + // use new method if possible + if (season == Game1.currentSeason && day == Game1.dayOfMonth) + return Utility.getTodaysBirthdayNPC(); + + // else replicate old behavior + NPC? found = null; + Utility.ForEachCharacter(npc => + { + if (npc.birthday_Season.Value == season && npc.birthday_Day.Value == day) + found = npc; + + return found is null; + }); + return found; + } + public static bool HasAnyPlayerSeenEvent(int event_number) { return Utility.HasAnyPlayerSeenEvent(event_number.ToString()); @@ -57,11 +116,27 @@ public static bool IsNormalObjectAtParentSheetIndex(Item item, int index) return Utility.IsNormalObjectAtParentSheetIndex(item, index.ToString()); } + public static int numObelisksOnFarm() + { + return Utility.GetObeliskTypesBuilt(); + } + public static int numSilos() { return Game1.GetNumberBuildingsConstructed("Silo"); } + public new static List sparkleWithinArea(Rectangle bounds, int numberOfSparkles, Color sparkleColor, int delayBetweenSparkles = 100, int delayBeforeStarting = 0, string sparkleSound = "") + { + TemporaryAnimatedSpriteList list = Utility.getTemporarySpritesWithinArea(new[] { 10, 11 }, bounds, numberOfSparkles, sparkleColor, delayBetweenSparkles, delayBeforeStarting, sparkleSound); + return list.ToList(); + } + + public static void spreadAnimalsAround(Building b, Farm environment) + { + Utility.spreadAnimalsAround(b, environment); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WallpaperFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WallpaperFacade.cs new file mode 100644 index 000000000..7d56da5f3 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WallpaperFacade.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using Netcode; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.GameData; +using StardewValley.Objects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class WallpaperFacade : Wallpaper, IRewriteFacade + { + /********* + ** Accessors + *********/ + public NetString modDataID => base.itemId; + + + /********* + ** Public methods + *********/ + public virtual ModWallpaperOrFlooring GetModData() + { + return base.GetSetData(); + } + + + /********* + ** Private methods + *********/ + private WallpaperFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs new file mode 100644 index 000000000..5ba6d5e68 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewValley.Tools; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 +{ + /// Maps Stardew Valley 1.5.6's methods to their newer form to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should never be referenced directly by mods. See remarks on for more info. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = SuppressReasons.MatchesOriginal)] + [SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = SuppressReasons.BaseForClarity)] + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] + public class WateringCanFacade : WateringCan, IRewriteFacade + { + /********* + ** Accessors + *********/ + public new int waterCanMax + { + get => base.waterCanMax.Value; + set => base.waterCanMax.Value = value; + } + + public int waterLeft + { + get => base.WaterLeft; + set => base.WaterLeft = value; + } + + + /********* + ** Private methods + *********/ + private WateringCanFacade() + { + RewriteHelper.ThrowFakeConstructorCalled(); + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 7a5808764..85f1402b0 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Netcode; using StardewModdingAPI.Events; @@ -16,16 +18,23 @@ using StardewValley.GameData; using StardewValley.GameData.FloorsAndPaths; using StardewValley.GameData.Movies; +using StardewValley.GameData.SpecialOrders; using StardewValley.Locations; using StardewValley.Menus; +using StardewValley.Minigames; using StardewValley.Mods; +using StardewValley.Network; using StardewValley.Objects; using StardewValley.Pathfinding; +using StardewValley.Projectiles; +using StardewValley.Quests; using StardewValley.SpecialOrders; using StardewValley.SpecialOrders.Objectives; using StardewValley.SpecialOrders.Rewards; using StardewValley.TerrainFeatures; using StardewValley.Tools; +using xTile.Layers; +using static StardewValley.Projectiles.BasicProjectile; using SObject = StardewValley.Object; namespace StardewModdingAPI.Metadata @@ -74,7 +83,16 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr /**** ** Stardew Valley 1.6 ****/ - // moved types + // moved types (audio) + .MapType("StardewValley.AudioCategoryWrapper", typeof(AudioCategoryWrapper)) + .MapType("StardewValley.AudioEngineWrapper", typeof(AudioEngineWrapper)) + .MapType("StardewValley.DummyAudioCategory", typeof(DummyAudioCategory)) + .MapType("StardewValley.DummyAudioEngine", typeof(DummyAudioEngine)) + .MapType("StardewValley.IAudioCategory", typeof(IAudioCategory)) + .MapType("StardewValley.IAudioEngine", typeof(IAudioEngine)) + .MapType("StardewValley.Network.NetAudio/SoundContext", typeof(SoundContext)) + + // moved types (enchantments) .MapType("StardewValley.AmethystEnchantment", typeof(AmethystEnchantment)) .MapType("StardewValley.AquamarineEnchantment", typeof(AquamarineEnchantment)) .MapType("StardewValley.ArchaeologistEnchantment", typeof(ArchaeologistEnchantment)) @@ -111,7 +129,9 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr .MapType("StardewValley.VampiricEnchantment", typeof(VampiricEnchantment)) .MapType("StardewValley.WateringCanEnchantment", typeof(WateringCanEnchantment)) + // moved types (special orders) .MapType("StardewValley.SpecialOrder", typeof(SpecialOrder)) + .MapType("StardewValley.SpecialOrder/QuestDuration", typeof(QuestDuration)) .MapType("StardewValley.SpecialOrder/QuestState", typeof(SpecialOrderStatus)) .MapType("StardewValley.CollectObjective", typeof(CollectObjective)) @@ -132,11 +152,18 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr .MapType("StardewValley.OrderReward", typeof(OrderReward)) .MapType("StardewValley.ResetEventReward", typeof(ResetEventReward)) - .MapType("StardewValley.IOAudioEngine", typeof(IAudioEngine)) + // moved types (other) + .MapType("LocationWeather", typeof(LocationWeather)) + .MapType("WaterTiles", typeof(WaterTiles)) + .MapType("StardewValley.Game1/MusicContext", typeof(MusicContext)) .MapType("StardewValley.ModDataDictionary", typeof(ModDataDictionary)) .MapType("StardewValley.ModHooks", typeof(ModHooks)) - .MapType("StardewValley.Network.NetAudio/SoundContext", typeof(SoundContext)) + .MapType("StardewValley.Network.IWorldState", typeof(NetWorldState)) .MapType("StardewValley.PathFindController", typeof(PathFindController)) + .MapType("StardewValley.SchedulePathDescription", typeof(SchedulePathDescription)) + + // deleted delegates + .MapType("StardewValley.DelayedAction/delayedBehavior", typeof(Action)) // field renames .MapFieldName(typeof(FloorPathData), "ID", nameof(FloorPathData.Id)) @@ -149,11 +176,15 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr // general API changes // note: types are mapped before members, regardless of the order listed here - .MapType("StardewValley.Buildings.Mill", typeof(Building)) + .MapFacade() + .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() @@ -163,43 +194,76 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr .MapFacade() .MapFacade() .MapFacade() + .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() .MapFacade() .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() .MapFacade() .MapFacade() .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() .MapFacade() + .MapType("StardewValley.Buildings.Mill", typeof(Building)) + .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() + .MapFacade() .MapFacade() .MapFacade() + .MapFacade() + .MapFacade() + .MapFacade() .MapFacade() .MapFacade() .MapFacade() + .MapFacade() .MapFacade() .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() .MapFacade() + .MapFacade() .MapFacade() + .MapFacade("StardewValley.Tools.ToolFactory", typeof(ToolFactoryFacade)) .MapFacade() + .MapFacade() .MapFacade() .MapFacade("Microsoft.Xna.Framework.Graphics.ViewportExtensions", typeof(ViewportExtensionsFacade)) + .MapFacade() + .MapFacade() .MapFacade() - .MapMethod("System.Void StardewValley.BellsAndWhistles.SpriteText::drawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.String,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Boolean,System.Int32,System.String,System.Int32,StardewValley.BellsAndWhistles.SpriteText/ScrollTextAlignment)", typeof(SpriteTextFacade), nameof(SpriteTextFacade.drawString)) // may not get rewritten by the MapFacade above due to the ScrollTextAlignment enum also being de-nested in 1.6 too + + // Mono.Cecil seems to have trouble resolving rewritten signatures which include a nested type like `StardewValley.BellsAndWhistles.SpriteText/ScrollTextAlignment` + .MapMethod("System.Void StardewValley.BellsAndWhistles.SpriteText::drawString(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.String,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Boolean,System.Int32,System.String,System.Int32,StardewValley.BellsAndWhistles.SpriteText/ScrollTextAlignment)", typeof(SpriteTextFacade), nameof(SpriteTextFacade.drawString)) + .MapMethod("System.Void StardewValley.BellsAndWhistles.SpriteText::drawStringWithScrollBackground(Microsoft.Xna.Framework.Graphics.SpriteBatch,System.String,System.Int32,System.Int32,System.String,System.Single,System.Int32,StardewValley.BellsAndWhistles.SpriteText/ScrollTextAlignment)", typeof(SpriteTextFacade), nameof(SpriteTextFacade.drawStringWithScrollBackground)) + .MapMethod("System.Void StardewValley.Projectiles.BasicProjectile::.ctor(System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Single,Microsoft.Xna.Framework.Vector2,System.String,System.String,System.Boolean,System.Boolean,StardewValley.GameLocation,StardewValley.Character,System.Boolean,StardewValley.Projectiles.BasicProjectile/onCollisionBehavior)", typeof(BasicProjectileFacade), nameof(BasicProjectileFacade.Constructor), new[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(float), typeof(float), typeof(float), typeof(Vector2), typeof(string), typeof(string), typeof(bool), typeof(bool), typeof(GameLocation), typeof(Character), typeof(bool), typeof(onCollisionBehavior) }) + .MapMethod("System.String StardewValley.LocalizedContentManager::LanguageCodeString(StardewValley.LocalizedContentManager/LanguageCode)", typeof(LocalizedContentManagerFacade), nameof(LocalizedContentManager.LanguageCodeString)) // BuildableGameLocation merged into GameLocation .MapFacade("StardewValley.Locations.BuildableGameLocation", typeof(BuildableGameLocationFacade)) @@ -220,7 +284,7 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr // implicit NetField conversions removed .MapMethod("Netcode.NetFieldBase`2::op_Implicit", typeof(NetFieldBaseFacade<,>), "op_Implicit") .MapMethod("System.Int64 Netcode.NetLong::op_Implicit(Netcode.NetLong)", typeof(NetLongFacade), nameof(NetLongFacade.op_Implicit)) - + .MapMethod("System.Int32 StardewValley.Network.NetDirection::op_Implicit(StardewValley.Network.NetDirection)", typeof(ImplicitConversionOperators), nameof(ImplicitConversionOperators.NetDirection_ToInt)) .MapMethod("!0 StardewValley.Network.NetPausableField`3::op_Implicit(StardewValley.Network.NetPausableField`3)", typeof(NetPausableFieldFacade), nameof(NetPausableFieldFacade.op_Implicit)); // heuristic rewrites From c54dcaf44b8cce2d9e2d30d43676e1c446761ef0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Mar 2024 23:52:28 -0500 Subject: [PATCH 71/84] update mod compatibility list --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 347 ++++++++++++++++++++-- 1 file changed, 320 insertions(+), 27 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 0f90abd3c..f788c3e0b 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -58,57 +58,46 @@ "ID": "Entoarox.AdvancedLocationLoader", "Default | UpdateKey": "Nexus:2270" }, - "Content Patcher": { "ID": "Pathoschild.ContentPatcher", "Default | UpdateKey": "Nexus:1915" }, - "Custom Farming Redux": { "ID": "Platonymous.CustomFarming", "Default | UpdateKey": "Nexus:991" }, - "Custom Shirts": { "ID": "Platonymous.CustomShirts", "Default | UpdateKey": "Nexus:2416" }, - "Entoarox Framework": { "ID": "Entoarox.EntoaroxFramework", "Default | UpdateKey": "Nexus:2269" }, - "JSON Assets": { "ID": "spacechase0.JsonAssets", "Default | UpdateKey": "Nexus:1720" }, - "Mail Framework": { "ID": "DIGUS.MailFrameworkMod", "Default | UpdateKey": "Nexus:1536" }, - "MTN": { "ID": "SgtPickles.MTN", "Default | UpdateKey": "Nexus:2256" }, - "PyTK": { "ID": "Platonymous.Toolkit", "Default | UpdateKey": "Nexus:1726" }, - "SpaceCore": { "ID": "spacechase0.SpaceCore", "Default | UpdateKey": "Nexus:1348" }, - "Stardust Core": { "ID": "Omegasis.StardustCore", "Default | UpdateKey": "Nexus:2341" }, - "TMXL Map Toolkit": { "ID": "Platonymous.TMXLoader", "Default | UpdateKey": "Nexus:1820" @@ -122,37 +111,31 @@ "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." }, - "Bee House Flower Range Fix": { "ID": "kirbylink.beehousefix", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4." }, - "Colored Chests": { "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." }, - "Error Handler": { "ID": "SMAPI.ErrorHandler", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "its error handling was integrated into Stardew Valley 1.6." }, - "Modder Serialization Utility": { "ID": "SerializerUtils-0-1", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it's no longer maintained or used." }, - "No Debug Mode": { "ID": "NoDebugMode", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." }, - "Split Screen": { "ID": "Ilyaki.SplitScreen", "~ | Status": "Obsolete", @@ -162,11 +145,331 @@ /********* ** Broke in SDV 1.6 *********/ + "24-Hour Clock Harmony": { + "ID": "pepoluan.24h", + "~0.3.1 | Status": "AssumeBroken", + "~0.3.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "All Chests Menu": { + "ID": "aedenthorn.AllChestsMenu", + "~0.3.1 | Status": "AssumeBroken", + "~0.3.1 | StatusReasonDetails": "fails at runtime" + }, + "Animal Dialogue Framework": { + "ID": "aedenthorn.AnimalDialogueFramework", + "~0.1.1 | Status": "AssumeBroken", + "~0.1.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "A Quality Mod": { + "ID": "spacechase0.AQualityMod", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Backstory Questions Framework": { + "ID": "spacechase0.BackstoryQuestionsFramework", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Better Elevator": { + "ID": "aedenthorn.BetterElevator", + "~2.1.0 | Status": "AssumeBroken", + "~2.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Better Crab Pots": { + "ID": "EpicBellyFlop45.BetterCrabPots", + "~2.1.0 | Status": "AssumeBroken", + "~2.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Better Horse Flute": { + "ID": "AnthonyMaciel.BetterHorseFlute", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Better Quality More Seeds": { + "ID": "SpaceBaby.BetterQualityMoreSeeds", + "~2.0.0-6 | Status": "AssumeBroken", + "~2.0.0-6 | StatusReasonDetails": "asset edits fail at runtime" + }, + "Bulk Animal Purchase": { + "ID": "aedenthorn.BulkAnimalPurchase", + "~1.1.2 | Status": "AssumeBroken", + "~1.1.2 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Build On Any Tile": { + "ID": "Esca.BuildOnAnyTile", + "~1.1.2 | Status": "AssumeBroken", + "~1.1.2 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Categories in Recipes": { + "ID": "Traktori.CategoriesInRecipes", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Change Horse Sounds": { + "ID": "CF.ChangeHorseSounds", + "~1.3.1 | Status": "AssumeBroken", + "~1.3.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Configure Machine Speed": { + "ID": "BayesianBandit.ConfigureMachineSpeed", + "~2.0.0-beta.2 | Status": "AssumeBroken", + "~2.0.0-beta.2 | StatusReasonDetails": "causes runtime errors" + }, + "Crop Walker": { + "ID": "MindMeltMax.CropWalker", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Custom Dungeon Floors": { + "ID": "Aedenthorn.CustomMonsterFloors", + "~0.7.0 | Status": "AssumeBroken", + "~0.7.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Custom Starter Package": { + "ID": "aedenthorn.CustomStarterPackage", + "~0.2.0 | Status": "AssumeBroken", + "~0.2.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Custom Tool Effect": { + "ID": "ZaneYork.CustomToolEffect", + "~1.1.0 | Status": "AssumeBroken", + "~1.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Death Tweaks": { + "ID": "aedenthorn.DeathTweaks", + "~0.1.1 | Status": "AssumeBroken", + "~0.1.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Enemy health Bars": { + "ID": "Speeder.HealthBars", + "~1.9.1 | Status": "AssumeBroken", + "~1.9.1 | StatusReasonDetails": "causes runtime errors" + }, + "Extreme Weather": { + "ID": "BlaDe.ExtremeWeather", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "causes runtime crash" + }, + "Fast Loads": { + "ID": "spajus.fastloads", + "~1.0.3 | Status": "AssumeBroken", + "~1.0.3 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Fixed Weapons Damage": { + "ID": "BlueSight.FixedWeaponsDamage", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches and asset edits fail at runtime" + }, + "Flower Rain": { + "ID": "spacechase0.FlowerRain", + "~1.1.4 | Status": "AssumeBroken", + "~1.1.4 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Friendly Divorce": { + "ID": "aedenthorn.FriendlyDivorce", + "~0.3.0 | Status": "AssumeBroken", + "~0.3.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Furniture Recolor": { + "ID": "aedenthorn.FurnitureRecolor", + "~0.1.0 | Status": "AssumeBroken", + "~0.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Hibernation": { + "ID": "Shockah.Hibernation", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Mayo Hats": { + "ID": "spacechase0.MayoHats", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" + }, + "Misophonia Accessibility": { + "ID": "TheFluffyRobot.MisophoniaAccessibility", + "~3.0.1 | Status": "AssumeBroken", + "~3.0.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Mod Updater": { + "ID": "Platonymous.ModUpdater", + "~1.0.6 | Status": "AssumeBroken", + "~1.0.6 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "More Giant Crops": { + "ID": "spacechase0.MoreGiantCrops", + "~1.2.0 | Status": "AssumeBroken", + "~1.2.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "More Multiplayer Info": { + "ID": "cheesysteak.moremultiplayerinfo", + "~3.0.1 | Status": "AssumeBroken", + "~3.0.1 | StatusReasonDetails": "causes runtime errors" + }, + "Move Between Buildings": { + "ID": "Vilaboa.MoveBetweenBuildings", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "causes runtime errors" + }, + "Movie Theater Tweaks": { + "ID": "aedenthorn.MovieTheatreTweaks", + "~0.2.0 | Status": "AssumeBroken", + "~0.2.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "MultiSave": { + "ID": "aedenthorn.MultiSave", + "~0.1.8 | Status": "AssumeBroken", + "~0.1.8 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Musical Paths": { + "ID": "aedenthorn.MusicalPaths", + "~0.1.1 | Status": "AssumeBroken", + "~0.1.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "NPC Clothing": { + "ID": "aedenthorn.NPCClothing", + "~0.1.0 | Status": "AssumeBroken", + "~0.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "One Click Shed Reloader": { + "ID": "BitwiseJonMods.OneClickShedReloader", + "~1.1.1 | Status": "AssumeBroken", + "~1.1.1 | StatusReasonDetails": "causes runtime errors" + }, + "One Sprinkler One Scarecrow": { + "ID": "mizzion.onesprinkleronescarecrow", + "~2.1.0 | Status": "AssumeBroken", + "~2.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Ore Increase V3": { + "ID": "OreIncreaseV3", + "~3.0.0 | Status": "AssumeBroken", + "~3.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Pacifist Valley": { + "ID": "Aedenthorn.PacifistValley", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Passable Crops": { + "ID": "NCarigon.PassableCrops", + "~1.0.9 | Status": "AssumeBroken", + "~1.0.9 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Pelican TTS": { + "ID": "Platonymous.PelicanTTS", + "~1.13.2 | Status": "AssumeBroken", + "~1.13.2 | StatusReasonDetails": "asset edits fail at runtime" + }, + "Persistent Mines": { + "ID": "spacechase0.PersistentMines", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "affected by breaking changes in the SpaceCore mod API" + }, + "Placeable Mine Shafts": { + "ID": "Aedenthorn.PlaceShaft", + "~2.0.0 | Status": "AssumeBroken", + "~2.0.0 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" + }, + "Platonic Partners and Friendships": { + "ID": "Amaranthacyan.PPAFSMAPI", + "~2.2.3 | Status": "AssumeBroken", + "~2.2.3 | StatusReasonDetails": "asset edits fail at runtime" + }, + "Qi Chest": { + "ID": "spacechase0.QiChest", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "causes crash during game launch due to Harmony" + }, + "Quest Time Limits": { + "ID": "aedenthorn.QuestTimeLimits", + "~0.1.3 | Status": "AssumeBroken", + "~0.1.3 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Riverland Farm But You Need to Build Bridges": { + "ID": "idermailer.riverfarmButBridge", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Screenshot Everywhere": { + "ID": "Gaiadin.Stardew-Screenshot-Everywhere", + "~1.4.1 | Status": "AssumeBroken", + "~1.4.1 | StatusReasonDetails": "causes runtime errors" + }, + "SeaCliff Farm": { + "ID": "freethejunimos.seaclifffarm", + "~1.0.2 | Status": "AssumeBroken", + "~1.0.2 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Seed Maker - Better Quality More Seeds": { + "ID": "vabrell.sm_bqms", + "~1.3.3 | Status": "AssumeBroken", + "~1.3.3 | StatusReasonDetails": "asset edits fail at runtime" + }, + "Seed Maker Tweaks": { + "ID": "aedenthorn.SeedMakerTweaks", + "~0.2.0 | Status": "AssumeBroken", + "~0.2.0 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" + }, + "Shut Up": { + "ID": "gekox.shutUp", + "~1.1.0 | Status": "AssumeBroken", + "~1.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Sixty-Nine Shirt": { + "ID": "aedenthorn.SixtyNine", + "~0.2.69 | Status": "AssumeBroken", + "~0.2.69 | StatusReasonDetails": "causes runtime errors" + }, "Skip Intro": { "ID": "Pathoschild.SkipIntro", "~1.9.9-alpha.20220227 | Status": "AssumeBroken", "~1.9.9-alpha.20220227 | StatusReasonDetails": "causes crash during game launch" }, + "Skull Cavern Drill": { + "ID": "S1mmyy.SkullCavernDrill", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" + }, + "Skull Cavern Drill Redux": { + "ID": "NetworkOverflow.SkullCavernDrillRedux", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" + }, + "Split Screen Manager": { + "ID": "RomenH.SplitScreenManager", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Stardew Hack": { + "ID": "bcmpinc.StardewHack", + "~6.0.0 | Status": "AssumeBroken", + "~6.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Statue of Generosity": { + "ID": "spacechase0.StatueOfGenerosity", + "~1.1.3 | Status": "AssumeBroken", + "~1.1.3 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Statue Shorts": { + "ID": "aedenthorn.StatueShorts", + "~0.1.0 | Status": "AssumeBroken", + "~0.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Swizzy Meads": { + "ID": "SwizzyStudios.SwizzyMeads", + "~1.0.1-alpha | Status": "AssumeBroken", + "~1.0.1-alpha | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "This Mod Is Organic": { + "ID": "SweetPanda.Organic", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" + }, + "Wealth is Health": { + "ID": "QiTheMysterious.WealthIsHealth", + "~0.1.2 | Status": "AssumeBroken", + "~0.1.2 | StatusReasonDetails": "causes runtime errors" + }, /********* ** Broke in SMAPI 3.14.0 @@ -205,11 +508,6 @@ "~2.0.1 | Status": "AssumeBroken", "~2.0.1 | StatusReasonDetails": "requires 'Microsoft.Xna.Framework.Audio.AudioCategory' which doesn't exist in MonoGame" }, - "Stardew Hack": { - "ID": "bcmpinc.StardewHack", - "~5.1.0 | Status": "AssumeBroken", - "~5.1.0 | StatusReasonDetails": "runtime error when initializing due to an API change between .NET Framework and .NET 5" - }, "Stardew Valley Expanded": { "ID": "FlashShifter.SVECode", "~1.13.11 | Status": "AssumeBroken", @@ -279,11 +577,6 @@ "~4.1.0 | Status": "AssumeBroken", "~4.1.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author }, - "Tree Spread": { - "ID": "bcmpinc.TreeSpread", - "~4.2.0 | Status": "AssumeBroken", - "~4.2.0 | StatusReasonDetails": "causes Harmony patching errors for other mods" // requested by the mod author - }, "Wear More Rings": { "ID": "bcmpinc.WearMoreRings", "~4.1.0 | Status": "AssumeBroken", From c93415f71f75d4d7ee1e6409968eca771257682b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 3 Mar 2024 13:07:50 -0500 Subject: [PATCH 72/84] add workaround for ModDrop's prerelease version parsing --- docs/release-notes.md | 1 + .../Clients/ModDrop/ModDropClient.cs | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8cbc4cd16..7da904747 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,6 +23,7 @@ * For the web UI: * Fixed uploaded log/JSON file expiry alway shown as renewed. + * Fixed update check for mods with a prerelease version tag not recognized by the ModDrop API. SMAPI now parses the version itself if needed. ## 3.18.6 Released 05 October 2023 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs index f5a5f930a..1bb3f1c1e 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels; @@ -73,8 +74,30 @@ public ModDropClient(string userAgent, string apiUrl, string modUrlFormat) if (file.IsOld || file.IsDeleted || file.IsHidden) continue; + // ModDrop drops the version prerelease tag if it's not in their whitelist of allowed suffixes. For + // example, "1.0.0-alpha" is fine but "1.0.0-sdvalpha" will have version field "1.0.0". + // + // If the version is non-prerelease but the file's display name contains a prerelease version, parse it + // out of the name instead. + string version = file.Version; + if (file.Name.Contains(version + "-") && SemanticVersion.TryParse(version, out ISemanticVersion? parsedVersion) && !parsedVersion.IsPrerelease()) + { + string[] parts = file.Name.Split(' '); + if (parts.Length == 1) + continue; // can't safely parse name without spaces (e.g. "mod-1.0.0-release" may not be version 1.0.0-release) + + foreach (string part in parts) + { + if (part.StartsWith(version + "-") && SemanticVersion.TryParse(part, out parsedVersion)) + { + version = parsedVersion.ToString(); + break; + } + } + } + downloads.Add( - new GenericModDownload(file.Name, file.Description, file.Version) + new GenericModDownload(file.Name, file.Description, version) ); } From df1a577e103e147f45c52bdb426bb6ec2af7d589 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 4 Mar 2024 20:12:26 -0500 Subject: [PATCH 73/84] add usage note to AssetRequestedEventArgs.Load* about returning cached instances --- src/SMAPI/Events/AssetRequestedEventArgs.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index d6561028b..5ad483e96 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -71,6 +71,7 @@ internal void SetMod(IModMetadata mod) /// /// The asset doesn't need to exist in the game's Content folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder. /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. + /// Do not return a cached instance. SMAPI takes ownership of the returned asset and may edit, resize, or dispose it as needed. Returning a cached instance may cause object-disposed errors or cache poisoning (where reloading the asset doesn't undo previously applied edits). If you need a reference to the final edited asset, use the event. /// /// public void LoadFrom(Func load, AssetLoadPriority priority, string? onBehalfOf = null) @@ -90,13 +91,7 @@ public void LoadFrom(Func load, AssetLoadPriority priority, string? onBe /// The expected data type. The main supported types are , , dictionaries, and lists; other types may be supported by the game's content pipeline. /// The relative path to the file in your mod folder. /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. - /// - /// Usage notes: - /// - /// The asset doesn't need to exist in the game's Content folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder. - /// Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use instead to avoid those limitations and improve mod compatibility. - /// - /// + /// public void LoadFromModFile(string relativePath, AssetLoadPriority priority) where TAsset : notnull { From 5265563f26978f2159a843ea1577ce802267a71e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Mar 2024 00:06:48 -0500 Subject: [PATCH 74/84] update for watering can change in latest 1.6 alpha --- .../Rewriters/StardewValley_1_6/WateringCanFacade.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs index 5ba6d5e68..45104b6aa 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/WateringCanFacade.cs @@ -16,12 +16,6 @@ public class WateringCanFacade : WateringCan, IRewriteFacade /********* ** Accessors *********/ - public new int waterCanMax - { - get => base.waterCanMax.Value; - set => base.waterCanMax.Value = value; - } - public int waterLeft { get => base.WaterLeft; From 6068d6ce2ab890a555321cbc17ce396c7b2ccc91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Mar 2024 17:20:28 -0500 Subject: [PATCH 75/84] clarify error message when SMAPI blocks a map load --- src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 7244a20e9..8043c74ae 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -345,7 +345,7 @@ private bool TryFixAndValidateLoadedAsset(IAssetInfo info, [NotNullWhen(true) // handle mismatch if (loadedMap.TileSheets.Count <= vanillaSheet.Index || loadedMap.TileSheets[vanillaSheet.Index].Id != vanillaSheet.Id) { - mod.LogAsMod($"SMAPI found an issue with a '{info.Name}' map load: {this.GetOnBehalfOfLabel(loader.OnBehalfOf, parenthetical: false) ?? "mod"} reordered the original tilesheets, which often causes crashes.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help.", LogLevel.Error); + mod.LogAsMod($"SMAPI blocked a '{info.Name}' map load: {this.GetOnBehalfOfLabel(loader.OnBehalfOf, parenthetical: false) ?? "mod"} reordered the original tilesheets, which often causes crashes.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help.", LogLevel.Error); return false; } } From 8e5a758e848a8fdf905b26591fb091c0de0dbdf0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Mar 2024 17:21:33 -0500 Subject: [PATCH 76/84] fix error when switching from a custom to vanilla language --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 3ae023d99..be455e7df 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -95,7 +95,11 @@ public void Propagate(IList contentManagers, IDictionary Date: Sun, 10 Mar 2024 13:55:18 -0400 Subject: [PATCH 77/84] allow only rewriting members if they're not resolveable This lets SMAPI handle instance<->static changes which don't otherwise change the signature, without rewriting references that are already correct (which can cause 'invalid program' errors). --- .../Framework/OnlyIfNotResolvedAttribute.cs | 8 ++ .../ModLoading/Rewriters/MappedMember.cs | 9 +++ .../Rewriters/ReplaceReferencesRewriter.cs | 74 ++++++++++++------- 3 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs diff --git a/src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs b/src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs new file mode 100644 index 000000000..330f76223 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// An attribute which indicates that the member should only be rewritten if the reference is currently broken. For example, this can be used for an instance-to-static change where the method signature and behavior doesn't otherwise change. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = false)] + internal class OnlyIfNotResolvedAttribute : Attribute { } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs new file mode 100644 index 000000000..1abf31d15 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// The member to map a facade method to as part of . + /// The target member to use. + /// Whether to only rewrite the method if the reference is currently broken. For example, this can be used for an instance-to-static change where the method signature and behavior doesn't otherwise change. + internal record MappedMember(MemberInfo Member, bool OnlyIfNotResolved); +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index 9c9994840..4ab2d96e1 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -22,6 +22,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// /// /// + /// By default, matching members will always be rewritten. For example, this lets you map behavior changes even + /// if the member reference could still be resolved. You can override that by adding + /// to the facade member (or setting the equivalent argument when + /// using the mapping methods). + /// + /// + /// /// To auto-map members to a facade type: /// /// @@ -44,7 +51,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// /// When adding a facade for a type with a required constructor, you'll need a constructor on the facade type. /// This should be private and will never be called (unless you want to rewrite references to the original - /// constructors per the above). + /// constructors per the above). You can call in the + /// private constructor to enforce that facades aren't constructed manually. /// /// internal class ReplaceReferencesRewriter : BaseInstructionHandler @@ -56,7 +64,7 @@ internal class ReplaceReferencesRewriter : BaseInstructionHandler private readonly Dictionary TypeMap = new(); /// The new members to reference, indexed by the old member's full name. - private readonly Dictionary MemberMap = new(); + private readonly Dictionary MemberMap = new(); /********* @@ -91,7 +99,8 @@ public ReplaceReferencesRewriter MapType(string fromFullName, Type toType) /// The full field name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::Tile. /// The new type which will have the field. /// The new field name to reference. - public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, string toName) + /// + public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, string toName, bool onlyIfNotResolved = false) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -115,14 +124,15 @@ public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, stri throw new InvalidOperationException($"Required field {toType.FullName}::{toName} could not be found."); // add mapping - return this.MapMember(fromFullName, toField, "field"); + return this.MapMember(fromFullName, toField, "field", onlyIfNotResolved); } /// Rewrite field references to point to another field with the field and parent type. /// The type which has the old and new fields. /// The field name. /// The new field name to reference. - public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string toName) + /// + public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string toName, bool onlyIfNotResolved = false) { // validate parameters if (type is null) @@ -147,14 +157,15 @@ public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string // add mapping string fromFullName = $"{this.FormatCecilType(toField.FieldType)} {this.FormatCecilType(type)}::{fromName}"; - return this.MapMember(fromFullName, toField, "field"); + return this.MapMember(fromFullName, toField, "field", onlyIfNotResolved); } /// Rewrite field references to point to a property with the same return type. /// The full field name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::Tile. /// The new type which will have the field. /// The new field name to reference. - public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type toType, string toName) + /// + public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type toType, string toName, bool onlyIfNotResolved = false) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -178,7 +189,8 @@ public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type to throw new InvalidOperationException($"Required property {toType.FullName}::{toName} could not be found."); // add mapping - return this.MapMember(fromFullName, toProperty, "field-to-property"); + onlyIfNotResolved = onlyIfNotResolved || toProperty.GetCustomAttribute() is not null; + return this.MapMember(fromFullName, toProperty, "field-to-property", onlyIfNotResolved); } /// Rewrite method references to point to another method with the same signature. @@ -186,7 +198,8 @@ public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type to /// The new type which will have the method. /// The new method name to reference. /// The method's parameter types to disambiguate between overloads, if needed. - public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, string toName, Type[]? parameterTypes = null) + /// + public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, string toName, Type[]? parameterTypes = null, bool onlyIfNotResolved = false) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -200,7 +213,7 @@ public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, str MethodInfo? method; try { - method = parameterTypes != null + method = parameterTypes is not null ? toType.GetMethod(toName, parameterTypes) : toType.GetMethod(toName); } @@ -212,7 +225,8 @@ public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, str throw new InvalidOperationException($"Required method {toType.FullName}::{toName} could not be found."); // add mapping - return this.MapMember(fromFullName, method, "method"); + onlyIfNotResolved = onlyIfNotResolved || method.GetCustomAttribute() is not null; + return this.MapMember(fromFullName, method, "method", onlyIfNotResolved); } /// Rewrite field, property, constructor, and method references to point to a matching equivalent on another class. @@ -238,19 +252,20 @@ public ReplaceReferencesRewriter MapFacade(string fromTypeName, Type toType, boo foreach (PropertyInfo property in toType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) { string propertyType = this.FormatCecilType(property.PropertyType); + bool onlyIfNotResolved = property.GetCustomAttribute() is not null; // add getter MethodInfo? get = property.GetMethod; - if (get != null) - this.MapMember($"{propertyType} {fromTypeName}::get_{property.Name}()", get, "method"); + if (get is not null) + this.MapMember($"{propertyType} {fromTypeName}::get_{property.Name}()", get, "method", onlyIfNotResolved); // add setter MethodInfo? set = property.SetMethod; - if (set != null) - this.MapMember($"System.Void {fromTypeName}::set_{property.Name}({propertyType})", set, "method"); + if (set is not null) + this.MapMember($"System.Void {fromTypeName}::set_{property.Name}({propertyType})", set, "method", onlyIfNotResolved); // add field => property - this.MapMember($"{propertyType} {fromTypeName}::{property.Name}", property, "field-to-property"); + this.MapMember($"{propertyType} {fromTypeName}::{property.Name}", property, "field-to-property", onlyIfNotResolved); } // methods @@ -259,17 +274,19 @@ public ReplaceReferencesRewriter MapFacade(string fromTypeName, Type toType, boo if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue; // handled via properties above + bool onlyIfNotResolved = method.GetCustomAttribute() is not null; + // map method { string fromFullName = $"{this.FormatCecilType(method.ReturnType)} {fromTypeName}::{method.Name}({this.FormatCecilParameterList(method.GetParameters())})"; - this.MapMember(fromFullName, method, "method"); + this.MapMember(fromFullName, method, "method", onlyIfNotResolved); } // map constructor to static methods if (method.IsStatic && method.Name == "Constructor") { string fromFullName = $"System.Void {fromTypeName}::.ctor({this.FormatCecilParameterList(method.GetParameters())})"; - this.MapMember(fromFullName, method, "method"); + this.MapMember(fromFullName, method, "method", onlyIfNotResolved); } } @@ -283,7 +300,9 @@ public ReplaceReferencesRewriter MapFacade(string fromTypeName, Type toType, boo if (!mapDefaultConstructor && parameters.Length == 0) continue; - this.MapMember(fromFullName, constructor, "constructor"); + bool onlyIfNotResolved = constructor.GetCustomAttribute() is not null; + + this.MapMember(fromFullName, constructor, "constructor", onlyIfNotResolved); } return this; @@ -313,7 +332,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio return false; // get target member - if (!this.MemberMap.TryGetValue(fromMember.FullName, out MemberInfo? toMember)) + if (!this.MemberMap.TryGetValue(fromMember.FullName, out MappedMember? mapData)) { // If this is a generic type, there's two cases where the above might not match: // 1. we mapped an open generic type like "Netcode.NetFieldBase`2::op_Implicit" without specific @@ -325,12 +344,16 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // "Netcode.NetFieldBase`2" (without type args) by using `GetElementType().FullName` instead. if (fromMember.DeclaringType is not GenericInstanceType) return false; - if (!this.MemberMap.TryGetValue($"{fromMember.DeclaringType.GetElementType().FullName}::{fromMember.Name}", out toMember)) + if (!this.MemberMap.TryGetValue($"{fromMember.DeclaringType.GetElementType().FullName}::{fromMember.Name}", out mapData)) return false; } + // apply options + if (mapData.OnlyIfNotResolved && fromMember.Resolve() is not null) + return false; + // apply - switch (toMember) + switch (mapData.Member) { // constructor case ConstructorInfo toConstructor: @@ -379,7 +402,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio else if (instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld) toPropMethod = toProperty.SetMethod; - if (toPropMethod != null) + if (toPropMethod is not null) { instruction.OpCode = OpCodes.Call; instruction.Operand = module.ImportReference(toPropMethod); @@ -400,7 +423,8 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio /// The full member name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::getTileLocation(). /// The new member to reference. /// A human-readable label for the reference type, like 'field' or 'method'. - private ReplaceReferencesRewriter MapMember(string fromFullName, MemberInfo toMember, string typeLabel) + /// + private ReplaceReferencesRewriter MapMember(string fromFullName, MemberInfo toMember, string typeLabel, bool onlyIfNotResolved) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -409,7 +433,7 @@ private ReplaceReferencesRewriter MapMember(string fromFullName, MemberInfo toMe throw new InvalidOperationException($"The replacement {typeLabel} for '{fromFullName}' can't be null."); // add mapping - if (!this.MemberMap.TryAdd(fromFullName, toMember)) + if (!this.MemberMap.TryAdd(fromFullName, new MappedMember(toMember, onlyIfNotResolved))) throw new InvalidOperationException($"The '{fromFullName}' {typeLabel} is already mapped."); return this; From abc562f1ab7705abe57801498335fbfb8cef2c38 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Mar 2024 13:55:18 -0400 Subject: [PATCH 78/84] fix 'invalid program' error in rewritten mods which reference LocalizedContentManager.LanguageCodeString correctly --- .../Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs index 41e71a278..f38cf2625 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs @@ -14,6 +14,7 @@ public class LocalizedContentManagerFacade : LocalizedContentManager, IRewriteFa /********* ** Public methods *********/ + [OnlyIfNotResolved] // the only change is instance->static, so don't rewrite references that are already correct to avoid 'invalid program' errors public new string LanguageCodeString(LanguageCode code) { return LocalizedContentManager.LanguageCodeString(code); From 57f680c4ec8e3010a85e0c68ff09120cd1e47d14 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Mar 2024 22:19:42 -0400 Subject: [PATCH 79/84] add asset propagation for the new Data/JukeboxTracks asset --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index be455e7df..a979760c2 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -379,6 +379,10 @@ static ISet GetWarpSet(GameLocation location) ItemRegistry.ResetCache(); return true; + case "data/jukeboxtracks": // Game1.LoadContent + Game1.jukeboxTrackData = DataLoader.JukeboxTracks(content); + return true; + case "data/locationcontexts": // Game1.LoadContent Game1.locationContextData = DataLoader.LocationContexts(content); return true; From 473f4f68133ec1e7339f39e2eac0e3aff00efe62 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Mar 2024 14:04:05 -0400 Subject: [PATCH 80/84] remove onlyIfNotResolved options in rewriters and make it the default --- .../Framework/OnlyIfNotResolvedAttribute.cs | 8 --- .../ModLoading/Rewriters/MappedMember.cs | 9 --- .../Rewriters/ReplaceReferencesRewriter.cs | 61 +++++++------------ .../LocalizedContentManagerFacade.cs | 1 - 4 files changed, 22 insertions(+), 57 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs diff --git a/src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs b/src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs deleted file mode 100644 index 330f76223..000000000 --- a/src/SMAPI/Framework/ModLoading/Framework/OnlyIfNotResolvedAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.ModLoading.Framework -{ - /// An attribute which indicates that the member should only be rewritten if the reference is currently broken. For example, this can be used for an instance-to-static change where the method signature and behavior doesn't otherwise change. - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = false)] - internal class OnlyIfNotResolvedAttribute : Attribute { } -} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs deleted file mode 100644 index 1abf31d15..000000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MappedMember.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Reflection; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// The member to map a facade method to as part of . - /// The target member to use. - /// Whether to only rewrite the method if the reference is currently broken. For example, this can be used for an instance-to-static change where the method signature and behavior doesn't otherwise change. - internal record MappedMember(MemberInfo Member, bool OnlyIfNotResolved); -} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs index 4ab2d96e1..14c8ddb99 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/ReplaceReferencesRewriter.cs @@ -21,12 +21,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// call the new members. /// /// - /// - /// By default, matching members will always be rewritten. For example, this lets you map behavior changes even - /// if the member reference could still be resolved. You can override that by adding - /// to the facade member (or setting the equivalent argument when - /// using the mapping methods). - /// + /// Member mappings are only used in cases where the reference to the original member can't be resolved. /// /// /// To auto-map members to a facade type: @@ -64,7 +59,7 @@ internal class ReplaceReferencesRewriter : BaseInstructionHandler private readonly Dictionary TypeMap = new(); /// The new members to reference, indexed by the old member's full name. - private readonly Dictionary MemberMap = new(); + private readonly Dictionary MemberMap = new(); /********* @@ -99,8 +94,7 @@ public ReplaceReferencesRewriter MapType(string fromFullName, Type toType) /// The full field name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::Tile. /// The new type which will have the field. /// The new field name to reference. - /// - public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, string toName, bool onlyIfNotResolved = false) + public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, string toName) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -124,15 +118,14 @@ public ReplaceReferencesRewriter MapField(string fromFullName, Type toType, stri throw new InvalidOperationException($"Required field {toType.FullName}::{toName} could not be found."); // add mapping - return this.MapMember(fromFullName, toField, "field", onlyIfNotResolved); + return this.MapMember(fromFullName, toField, "field"); } /// Rewrite field references to point to another field with the field and parent type. /// The type which has the old and new fields. /// The field name. /// The new field name to reference. - /// - public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string toName, bool onlyIfNotResolved = false) + public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string toName) { // validate parameters if (type is null) @@ -157,15 +150,14 @@ public ReplaceReferencesRewriter MapFieldName(Type type, string fromName, string // add mapping string fromFullName = $"{this.FormatCecilType(toField.FieldType)} {this.FormatCecilType(type)}::{fromName}"; - return this.MapMember(fromFullName, toField, "field", onlyIfNotResolved); + return this.MapMember(fromFullName, toField, "field"); } /// Rewrite field references to point to a property with the same return type. /// The full field name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::Tile. /// The new type which will have the field. /// The new field name to reference. - /// - public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type toType, string toName, bool onlyIfNotResolved = false) + public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type toType, string toName) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -189,8 +181,7 @@ public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type to throw new InvalidOperationException($"Required property {toType.FullName}::{toName} could not be found."); // add mapping - onlyIfNotResolved = onlyIfNotResolved || toProperty.GetCustomAttribute() is not null; - return this.MapMember(fromFullName, toProperty, "field-to-property", onlyIfNotResolved); + return this.MapMember(fromFullName, toProperty, "field-to-property"); } /// Rewrite method references to point to another method with the same signature. @@ -198,8 +189,7 @@ public ReplaceReferencesRewriter MapFieldToProperty(string fromFullName, Type to /// The new type which will have the method. /// The new method name to reference. /// The method's parameter types to disambiguate between overloads, if needed. - /// - public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, string toName, Type[]? parameterTypes = null, bool onlyIfNotResolved = false) + public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, string toName, Type[]? parameterTypes = null) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -225,8 +215,7 @@ public ReplaceReferencesRewriter MapMethod(string fromFullName, Type toType, str throw new InvalidOperationException($"Required method {toType.FullName}::{toName} could not be found."); // add mapping - onlyIfNotResolved = onlyIfNotResolved || method.GetCustomAttribute() is not null; - return this.MapMember(fromFullName, method, "method", onlyIfNotResolved); + return this.MapMember(fromFullName, method, "method"); } /// Rewrite field, property, constructor, and method references to point to a matching equivalent on another class. @@ -252,20 +241,19 @@ public ReplaceReferencesRewriter MapFacade(string fromTypeName, Type toType, boo foreach (PropertyInfo property in toType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) { string propertyType = this.FormatCecilType(property.PropertyType); - bool onlyIfNotResolved = property.GetCustomAttribute() is not null; // add getter MethodInfo? get = property.GetMethod; if (get is not null) - this.MapMember($"{propertyType} {fromTypeName}::get_{property.Name}()", get, "method", onlyIfNotResolved); + this.MapMember($"{propertyType} {fromTypeName}::get_{property.Name}()", get, "method"); // add setter MethodInfo? set = property.SetMethod; if (set is not null) - this.MapMember($"System.Void {fromTypeName}::set_{property.Name}({propertyType})", set, "method", onlyIfNotResolved); + this.MapMember($"System.Void {fromTypeName}::set_{property.Name}({propertyType})", set, "method"); // add field => property - this.MapMember($"{propertyType} {fromTypeName}::{property.Name}", property, "field-to-property", onlyIfNotResolved); + this.MapMember($"{propertyType} {fromTypeName}::{property.Name}", property, "field-to-property"); } // methods @@ -274,19 +262,17 @@ public ReplaceReferencesRewriter MapFacade(string fromTypeName, Type toType, boo if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue; // handled via properties above - bool onlyIfNotResolved = method.GetCustomAttribute() is not null; - // map method { string fromFullName = $"{this.FormatCecilType(method.ReturnType)} {fromTypeName}::{method.Name}({this.FormatCecilParameterList(method.GetParameters())})"; - this.MapMember(fromFullName, method, "method", onlyIfNotResolved); + this.MapMember(fromFullName, method, "method"); } // map constructor to static methods if (method.IsStatic && method.Name == "Constructor") { string fromFullName = $"System.Void {fromTypeName}::.ctor({this.FormatCecilParameterList(method.GetParameters())})"; - this.MapMember(fromFullName, method, "method", onlyIfNotResolved); + this.MapMember(fromFullName, method, "method"); } } @@ -300,9 +286,7 @@ public ReplaceReferencesRewriter MapFacade(string fromTypeName, Type toType, boo if (!mapDefaultConstructor && parameters.Length == 0) continue; - bool onlyIfNotResolved = constructor.GetCustomAttribute() is not null; - - this.MapMember(fromFullName, constructor, "constructor", onlyIfNotResolved); + this.MapMember(fromFullName, constructor, "constructor"); } return this; @@ -332,7 +316,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio return false; // get target member - if (!this.MemberMap.TryGetValue(fromMember.FullName, out MappedMember? mapData)) + if (!this.MemberMap.TryGetValue(fromMember.FullName, out MemberInfo? mappedToMethod)) { // If this is a generic type, there's two cases where the above might not match: // 1. we mapped an open generic type like "Netcode.NetFieldBase`2::op_Implicit" without specific @@ -344,16 +328,16 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // "Netcode.NetFieldBase`2" (without type args) by using `GetElementType().FullName` instead. if (fromMember.DeclaringType is not GenericInstanceType) return false; - if (!this.MemberMap.TryGetValue($"{fromMember.DeclaringType.GetElementType().FullName}::{fromMember.Name}", out mapData)) + if (!this.MemberMap.TryGetValue($"{fromMember.DeclaringType.GetElementType().FullName}::{fromMember.Name}", out mappedToMethod)) return false; } // apply options - if (mapData.OnlyIfNotResolved && fromMember.Resolve() is not null) + if (fromMember.Resolve() is not null) return false; // apply - switch (mapData.Member) + switch (mappedToMethod) { // constructor case ConstructorInfo toConstructor: @@ -423,8 +407,7 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio /// The full member name, like Microsoft.Xna.Framework.Vector2 StardewValley.Character::getTileLocation(). /// The new member to reference. /// A human-readable label for the reference type, like 'field' or 'method'. - /// - private ReplaceReferencesRewriter MapMember(string fromFullName, MemberInfo toMember, string typeLabel, bool onlyIfNotResolved) + private ReplaceReferencesRewriter MapMember(string fromFullName, MemberInfo toMember, string typeLabel) { // validate parameters if (string.IsNullOrWhiteSpace(fromFullName)) @@ -433,7 +416,7 @@ private ReplaceReferencesRewriter MapMember(string fromFullName, MemberInfo toMe throw new InvalidOperationException($"The replacement {typeLabel} for '{fromFullName}' can't be null."); // add mapping - if (!this.MemberMap.TryAdd(fromFullName, new MappedMember(toMember, onlyIfNotResolved))) + if (!this.MemberMap.TryAdd(fromFullName, toMember)) throw new InvalidOperationException($"The '{fromFullName}' {typeLabel} is already mapped."); return this; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs index f38cf2625..41e71a278 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/LocalizedContentManagerFacade.cs @@ -14,7 +14,6 @@ public class LocalizedContentManagerFacade : LocalizedContentManager, IRewriteFa /********* ** Public methods *********/ - [OnlyIfNotResolved] // the only change is instance->static, so don't rewrite references that are already correct to avoid 'invalid program' errors public new string LanguageCodeString(LanguageCode code) { return LocalizedContentManager.LanguageCodeString(code); From 57f1106e0276f15eb9294d0c38de002997202699 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Mar 2024 22:59:02 -0400 Subject: [PATCH 81/84] add option to log more technical info for rewriting --- docs/release-notes.md | 4 ++ src/SMAPI/Framework/Logging/LogManager.cs | 14 +++++-- .../Framework/ModLoading/AssemblyLoader.cs | 34 ++++++++++++++--- .../Finders/ReferenceToInvalidMemberFinder.cs | 37 ++++++++++++++----- src/SMAPI/Framework/Models/SConfig.cs | 8 +++- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/Metadata/InstructionMetadata.cs | 5 ++- src/SMAPI/SMAPI.config.json | 7 ++++ 8 files changed, 89 insertions(+), 24 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7da904747..22387549f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,6 +25,10 @@ * Fixed uploaded log/JSON file expiry alway shown as renewed. * Fixed update check for mods with a prerelease version tag not recognized by the ModDrop API. SMAPI now parses the version itself if needed. +* For SMAPI developers: + * Improved compatibility rewriters. + * Added `LogTechnicalDetailsForBrokenMods` option in `smapi-internal/config.json` which adds more technical info to the SMAPI log when a mod is broken. This is mainly useful for creating compatibility rewriters. + ## 3.18.6 Released 05 October 2023 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index cd17d7c30..a9901a61d 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -260,7 +260,8 @@ public void LogSettingsHeader(SConfig settings) /// The loaded mods. /// The mods which could not be loaded. /// Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access. - public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, IModMetadata[] loadedMods, IModMetadata[] skippedMods, bool logParanoidWarnings) + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, IModMetadata[] loadedMods, IModMetadata[] skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods) { // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); @@ -299,7 +300,7 @@ public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, } // log mod warnings - this.LogModWarnings(loaded, skippedMods, logParanoidWarnings); + this.LogModWarnings(loaded, skippedMods, logParanoidWarnings, logTechnicalDetailsForBrokenMods); } /// @@ -316,8 +317,9 @@ public void Dispose() /// The loaded mods. /// The mods which could not be loaded. /// Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access. + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifests aren't guaranteed non-null at this point in the loading process.")] - private void LogModWarnings(IEnumerable mods, IModMetadata[] skippedMods, bool logParanoidWarnings) + private void LogModWarnings(IEnumerable mods, IModMetadata[] skippedMods, bool logParanoidWarnings, bool logTechnicalDetailsForBrokenMods) { // get mods with warnings IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); @@ -345,7 +347,11 @@ private void LogModWarnings(IEnumerable mods, IModMetadata[] skipp { foreach (IModMetadata mod in list.OrderBy(p => p.DisplayName)) { - string message = $" - {mod.DisplayName}{(" " + mod.Manifest?.Version?.ToString()).TrimEnd()} because {mod.Error}"; + string technicalInfo = logTechnicalDetailsForBrokenMods + ? $" (ID: {mod.Manifest?.UniqueID ?? "???"}, path: {mod.RelativeDirectoryPath})" + : ""; + + string message = $" - {mod.DisplayName}{(" " + mod.Manifest?.Version?.ToString()).TrimEnd()}{technicalInfo} because {mod.Error}"; // duplicate mod: log first one only, don't show redundant version if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest()) diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 67467d58f..2f03e7ded 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -11,6 +12,7 @@ using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; namespace StardewModdingAPI.Framework.ModLoading { @@ -47,6 +49,9 @@ internal class AssemblyLoader : IDisposable /// Whether to rewrite mods for compatibility. private readonly bool RewriteMods; + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + private readonly bool LogTechnicalDetailsForBrokenMods; + /********* ** Public methods @@ -56,10 +61,12 @@ internal class AssemblyLoader : IDisposable /// Encapsulates monitoring and logging. /// Whether to detect paranoid mode issues. /// Whether to rewrite mods for compatibility. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods) + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, bool rewriteMods, bool logTechnicalDetailsForBrokenMods) { this.Monitor = monitor; this.RewriteMods = rewriteMods; + this.LogTechnicalDetailsForBrokenMods = logTechnicalDetailsForBrokenMods; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); // init resolver @@ -82,8 +89,18 @@ public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMo } // init rewriters - this.InstructionHandlers = new InstructionMetadata().GetHandlers(paranoidMode, this.RewriteMods).ToArray(); - + Stopwatch? timer = null; + if (logTechnicalDetailsForBrokenMods) + { + timer = new(); + timer.Start(); + } + this.InstructionHandlers = new InstructionMetadata().GetHandlers(paranoidMode, this.RewriteMods, logTechnicalDetailsForBrokenMods).ToArray(); + if (logTechnicalDetailsForBrokenMods) + { + timer!.Stop(); + monitor.Log($"[SMAPI] Initialized rewriters in {timer.ElapsedMilliseconds}ms"); + } } /// Preprocess and load an assembly. @@ -459,9 +476,14 @@ private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandle return; // format messages - string phrase = handler.Phrases.Any() - ? string.Join(", ", handler.Phrases) - : handler.DefaultPhrase; + string phrase; + if (!handler.Phrases.Any()) + phrase = handler.DefaultPhrase; + else if (this.LogTechnicalDetailsForBrokenMods && result == InstructionHandleResult.NotCompatible) + phrase = "\n - " + string.Join(";\n - ", handler.Phrases.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); + else + phrase = string.Join(", ", handler.Phrases.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); + this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", phrase)); } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs index e5625c637..42e5cb82c 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs @@ -17,16 +17,21 @@ internal class ReferenceToInvalidMemberFinder : BaseInstructionHandler /// The assembly names to which to heuristically detect broken references. private readonly ISet ValidateReferencesToAssemblies; + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + private readonly bool LogTechnicalDetailsForBrokenMods; + /********* ** Public methods *********/ /// Construct an instance. /// The assembly names to which to heuristically detect broken references. - public ReferenceToInvalidMemberFinder(ISet validateReferencesToAssemblies) + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + public ReferenceToInvalidMemberFinder(ISet validateReferencesToAssemblies, bool logTechnicalDetailsForBrokenMods) : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; + this.LogTechnicalDetailsForBrokenMods = logTechnicalDetailsForBrokenMods; } /// @@ -40,11 +45,11 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio // wrong return type if (targetField != null && !RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"); + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {this.GetMemberDisplayName(fieldRef)} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"); // missing else if (targetField == null || targetField.HasConstant || !RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, targetField.DeclaringType)) - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {this.GetMemberDisplayName(fieldRef)} (no such field)"); return false; } @@ -60,21 +65,21 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio { MethodDefinition[]? candidateMethods = methodRef.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodRef.Name).ToArray(); if (candidateMethods?.Any() is true && candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {this.GetMemberDisplayName(methodDef)} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); } // missing else if (methodDef is null) { - string phrase; + string typeName; if (this.IsProperty(methodRef)) - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + typeName = "property"; else if (methodRef.Name == ".ctor") - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + typeName = "constructor"; else - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + typeName = "method"; - this.MarkFlag(InstructionHandleResult.NotCompatible, phrase); + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {this.GetMemberDisplayName(methodRef)} (no such {typeName})"); } } @@ -100,6 +105,20 @@ private bool IsUnsupported(MethodReference method) method.DeclaringType.Name.Contains("["); // array methods } + /// Get the member name to show in logged messages. + /// The member reference. + private string GetMemberDisplayName(MemberReference memberRef) + { + if (this.LogTechnicalDetailsForBrokenMods) + return memberRef.FullName; + + string name = memberRef.Name; + if (memberRef is PropertyReference) + name = name[4..]; // remove `get_` or `set_` prefix + + return $"{memberRef.DeclaringType.FullName}.{name}"; + } + /// Get a shorter type name for display. /// The type reference. private string GetFriendlyTypeName(TypeReference type) diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b347f7d71..470e5501e 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -22,6 +22,7 @@ internal class SConfig [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", [nameof(LogNetworkTraffic)] = false, + [nameof(LogTechnicalDetailsForBrokenMods)] = false, [nameof(RewriteMods)] = true, [nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux, [nameof(SuppressHarmonyDebugMode)] = true @@ -76,6 +77,9 @@ internal class SConfig /// Whether SMAPI should log network traffic. Best combined with , which includes network metadata. public bool LogNetworkTraffic { get; set; } + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + public bool LogTechnicalDetailsForBrokenMods { get; set; } + /// The colors to use for text written to the SMAPI console. public ColorSchemeConfig ConsoleColors { get; set; } @@ -107,12 +111,13 @@ internal class SConfig /// /// /// + /// /// /// /// /// /// - public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate) + public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, bool? logTechnicalDetailsForBrokenMods, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate) { this.DeveloperMode = developerMode; this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)]; @@ -125,6 +130,7 @@ public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsole this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(this.RewriteMods)]; this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(this.UseCaseInsensitivePaths)]; this.LogNetworkTraffic = logNetworkTraffic ?? (bool)SConfig.DefaultValues[nameof(this.LogNetworkTraffic)]; + this.LogTechnicalDetailsForBrokenMods = logTechnicalDetailsForBrokenMods ?? (bool)SConfig.DefaultValues[nameof(this.LogTechnicalDetailsForBrokenMods)]; this.ConsoleColors = consoleColors; this.SuppressHarmonyDebugMode = suppressHarmonyDebugMode ?? (bool)SConfig.DefaultValues[nameof(this.SuppressHarmonyDebugMode)]; this.SuppressUpdateChecks = new HashSet(suppressUpdateChecks ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 0ec677608..c138c85c4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1716,7 +1716,7 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin // load mods IList skippedMods = new List(); - using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) + using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods, this.Settings.LogTechnicalDetailsForBrokenMods)) { // init HashSet suppressUpdateChecks = this.Settings.SuppressUpdateChecks; @@ -1741,7 +1741,7 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin this.ModRegistry.AreAllModsLoaded = true; // log mod info - this.LogManager.LogModInfo(loaded, loadedContentPacks, loadedMods, skippedMods.ToArray(), this.Settings.ParanoidWarnings); + this.LogManager.LogModInfo(loaded, loadedContentPacks, loadedMods, skippedMods.ToArray(), this.Settings.ParanoidWarnings, this.Settings.LogTechnicalDetailsForBrokenMods); // initialize translations this.ReloadTranslations(loaded); diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 85f1402b0..3a134ccac 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -56,7 +56,8 @@ internal class InstructionMetadata /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. /// Whether to detect paranoid mode issues. /// Whether to get handlers which rewrite mods for compatibility. - public IEnumerable GetHandlers(bool paranoidMode, bool rewriteMods) + /// Whether to include more technical details about broken mods in the TRACE logs. This is mainly useful for creating compatibility rewriters. + public IEnumerable GetHandlers(bool paranoidMode, bool rewriteMods, bool logTechnicalDetailsForBrokenMods) { /**** ** rewrite CIL to fix incompatible code @@ -304,7 +305,7 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr ** detect mod issues ****/ // broken code - yield return new ReferenceToInvalidMemberFinder(this.ValidateReferencesToAssemblies); + yield return new ReferenceToInvalidMemberFinder(this.ValidateReferencesToAssemblies, logTechnicalDetailsForBrokenMods); // code which may impact game stability yield return new FieldFinder(typeof(SaveGame).FullName!, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer); diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 1bd467d97..26a3caaad 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -92,6 +92,13 @@ in future SMAPI versions. */ "LogNetworkTraffic": false, + /** + * Whether to include more technical details about broken mods in the TRACE logs. This is + * mainly useful for creating compatibility rewriters, it's not useful to most players or mod + * authors. + */ + "LogTechnicalDetailsForBrokenMods": false, + /** * The colors to use for text written to the SMAPI console. * From c31ede0cc1b620ca9de647ba788589891521a82e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Mar 2024 22:59:02 -0400 Subject: [PATCH 82/84] update mod compatibility list & rewriters for final 1.6.0 release --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 40 +++++++++++++++++++ .../StardewValley_1_6/FishingRodFacade.cs | 19 ++++++++- .../StardewValley_1_6/Game1Facade.cs | 25 ++++++++++++ .../StardewValley_1_6/ObjectFacade.cs | 2 +- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index f788c3e0b..ff6cc02b3 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -190,6 +190,11 @@ "~2.0.0-6 | Status": "AssumeBroken", "~2.0.0-6 | StatusReasonDetails": "asset edits fail at runtime" }, + "Betwitched": { + "ID": "b_wandert.Betwitched", + "~0.9.0 | Status": "AssumeBroken", + "~0.9.0 | StatusReasonDetails": "breaks loading the Forest location" + }, "Bulk Animal Purchase": { "ID": "aedenthorn.BulkAnimalPurchase", "~1.1.2 | Status": "AssumeBroken", @@ -250,11 +255,26 @@ "~1.0.0 | Status": "AssumeBroken", "~1.0.0 | StatusReasonDetails": "causes runtime crash" }, + "Farmageddon": { + "ID": "maxvollmer.farmageddon", + "~3.0.0 | Status": "AssumeBroken", + "~3.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "Fast Loads": { "ID": "spajus.fastloads", "~1.0.3 | Status": "AssumeBroken", "~1.0.3 | StatusReasonDetails": "Harmony patches fail at runtime" }, + "Fish Exclusions": { + "ID": "GZhynko.FishExclusions", + "~1.1.5 | Status": "AssumeBroken", + "~1.1.5 | StatusReasonDetails": "Harmony patches fail at runtime" + }, + "Fishing Progression": { + "ID": "chadlymasterson.fishingprogression", + "~1.0.1 | Status": "AssumeBroken", + "~1.0.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "Fixed Weapons Damage": { "ID": "BlueSight.FixedWeaponsDamage", "~1.0.0 | Status": "AssumeBroken", @@ -280,11 +300,21 @@ "~1.0.0 | Status": "AssumeBroken", "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" }, + "Hugs and Kisses": { + "ID": "aedenthorn.HugsAndKisses", + "~0.4.0 | Status": "AssumeBroken", + "~0.4.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "Mayo Hats": { "ID": "spacechase0.MayoHats", "~1.0.0 | Status": "AssumeBroken", "~1.0.0 | StatusReasonDetails": "affected by breaking changes in the Json Assets mod API" }, + "Mayo Mart": { + "ID": "aedenthorn.MayoMart", + "~0.1.0 | Status": "AssumeBroken", + "~0.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "Misophonia Accessibility": { "ID": "TheFluffyRobot.MisophoniaAccessibility", "~3.0.1 | Status": "AssumeBroken", @@ -340,6 +370,11 @@ "~2.1.0 | Status": "AssumeBroken", "~2.1.0 | StatusReasonDetails": "Harmony patches fail at runtime" }, + "Ore Increaser": { + "ID": "crazywig.oreincrease", + "~1.0.0 | Status": "AssumeBroken", + "~1.0.0 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "Ore Increase V3": { "ID": "OreIncreaseV3", "~3.0.0 | Status": "AssumeBroken", @@ -460,6 +495,11 @@ "~1.0.1-alpha | Status": "AssumeBroken", "~1.0.1-alpha | StatusReasonDetails": "Harmony patches fail at runtime" }, + "The Adventurer's Life Expanded": { + "ID": "HamioDracny.TALE.SMAPI", + "~1.2.1 | Status": "AssumeBroken", + "~1.2.1 | StatusReasonDetails": "Harmony patches fail at runtime" + }, "This Mod Is Organic": { "ID": "SweetPanda.Organic", "~1.0.0 | Status": "AssumeBroken", diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs index ebaf4caab..6fc662279 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/FishingRodFacade.cs @@ -1,6 +1,9 @@ +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewValley.Tools; +using SObject = StardewValley.Object; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member: This is internal code to support rewriters and shouldn't be called directly. @@ -15,6 +18,16 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters.StardewValley_1_6 [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = SuppressReasons.UsedViaRewriting)] public class FishingRodFacade : FishingRod, IRewriteFacade { + /********* + ** Accessors + *********/ + public bool caughtDoubleFish + { + get => base.numberOfFishCaught > 1; + set => base.numberOfFishCaught = value ? Math.Max(2, base.numberOfFishCaught) : 1; + } + + /********* ** Public methods *********/ @@ -27,14 +40,16 @@ public int getBaitAttachmentIndex() public int getBobberAttachmentIndex() { - return int.TryParse(base.GetTackle()?.ItemId, out int index) + List? tackle = base.GetTackle(); + + return tackle?.Count > 0 && int.TryParse(tackle[0]?.ItemId, out int index) ? index : -1; } public void pullFishFromWater(int whichFish, int fishSize, int fishQuality, int fishDifficulty, bool treasureCaught, bool wasPerfect, bool fromFishPond, bool caughtDouble = false, string itemCategory = "Object") { - base.pullFishFromWater(whichFish.ToString(), fishSize, fishQuality, fishDifficulty, treasureCaught, wasPerfect, fromFishPond, null, false, caughtDouble); + base.pullFishFromWater(whichFish.ToString(), fishSize, fishQuality, fishDifficulty, treasureCaught, wasPerfect, fromFishPond, null, false, caughtDouble ? 2 : 1); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs index 9d2b394ce..a8e6f1697 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/Game1Facade.cs @@ -121,12 +121,37 @@ public static NPC getCharacterFromName(string name, bool mustBeVillager = true, return Game1.getCharacterFromName(name, mustBeVillager); } + public static int getModeratelyDarkTime() + { + return Game1.getModeratelyDarkTime(Game1.currentLocation); + } + public new static string GetSeasonForLocation(GameLocation location) { Season season = Game1.GetSeasonForLocation(location); return season.ToString(); } + public static int getStartingToGetDarkTime() + { + return Game1.getStartingToGetDarkTime(Game1.currentLocation); + } + + public static int getTrulyDarkTime() + { + return Game1.getTrulyDarkTime(Game1.currentLocation); + } + + public static bool isDarkOut() + { + return Game1.isDarkOut(Game1.currentLocation); + } + + public static bool isStartingToGetDarkOut() + { + return Game1.isStartingToGetDarkOut(Game1.currentLocation); + } + public static void playMorningSong() { Game1.playMorningSong(); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs index 994dd3190..16e872a10 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StardewValley_1_6/ObjectFacade.cs @@ -74,7 +74,7 @@ public void DayUpdate(GameLocation location) public void farmerAdjacentAction(GameLocation location) { - base.farmerAdjacentAction(); + base.farmerAdjacentAction(Game1.player); } public Rectangle getBoundingBox(Vector2 tileLocation) From f8daca5a8d1699697bb5b8ca7e709127017ea791 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Mar 2024 00:08:03 -0400 Subject: [PATCH 83/84] update Content Patcher schema --- docs/release-notes.md | 1 + src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 22387549f..975f01e49 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * Fixed redundant `TRACE` logs for a broken mod which references members with the wrong types. * For the web UI: + * Updated JSON validator for Content Patcher 2.0.0. * Fixed uploaded log/JSON file expiry alway shown as renewed. * Fixed update check for mods with a prerelease version tag not recognized by the ModDrop API. SMAPI now parses the version itself if needed. diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index bd9e74272..6dc433e29 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -14,7 +14,7 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.", "type": "string", - "pattern": "^1\\.29\\.[0-9]+$", + "pattern": "^2\\.0\\.[0-9]+$", "@errorMessages": { "pattern": "Incorrect value '@value'. You should always use the latest format version (currently 1.29.0) to enable the latest features, avoid obsolete behavior, and reduce load times." } From 679dfb30e7245ab9bb858d5e0f25931ef04d7b3e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Mar 2024 00:08:23 -0400 Subject: [PATCH 84/84] prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 13 +++++++------ src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/build/common.targets b/build/common.targets index cba06cbd2..18feca830 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed. - 3.18.6 + 4.0.0 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index 975f01e49..3413ae041 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,20 +1,22 @@ ← [README](README.md) # Release notes -## Upcoming release for Stardew Valley 1.6 +## 4.0.0 +Released 19 March 2024 for Stardew Valley 1.6.0 or later. See [release highlights](https://www.patreon.com/posts/100388693). + * For players: * Updated for Stardew Valley 1.6. * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). * Improved performance. - * Improved compatibility rewriting to handle more cases (thanks to SinZ!). - * Removed the bundled `ErrorHandler` mod (now integrated into Stardew Valley 1.6). + * Improved compatibility rewriting to handle more cases (thanks to SinZ for his contributions!). + * Removed the bundled `ErrorHandler` mod, which is now integrated into Stardew Valley 1.6. * Removed obsolete console commands: `list_item_types` (no longer needed) and `player_setimmunity` (broke in 1.6 and rarely used). * Removed support for seamlessly updating from SMAPI 2.11.3 and earlier (released in 2019). _If needed, you can update to SMAPI 3.18.0 first and then install the latest version._ * For mod authors: * Updated to .NET 6. - * Added `RenderingStep` and `RenderedStep` events, which let you handle a specific step in the game's render cycle. + * Added [`RenderingStep` and `RenderedStep` events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Display.RenderingStep), which let you handle a specific step in the game's render cycle. * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!). * Removed all deprecated APIs. * SMAPI no longer intercepts output written to the console. Mods which directly access `Console` will be listed under mod warnings. @@ -27,8 +29,7 @@ * Fixed update check for mods with a prerelease version tag not recognized by the ModDrop API. SMAPI now parses the version itself if needed. * For SMAPI developers: - * Improved compatibility rewriters. - * Added `LogTechnicalDetailsForBrokenMods` option in `smapi-internal/config.json` which adds more technical info to the SMAPI log when a mod is broken. This is mainly useful for creating compatibility rewriters. + * Added `LogTechnicalDetailsForBrokenMods` option in `smapi-internal/config.json`, which adds more technical info to the SMAPI log when a mod is broken. This is mainly useful for creating compatibility rewriters. ## 3.18.6 Released 05 October 2023 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index a97124e50..8267c9ae5 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.18.6", + "Version": "4.0.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.18.6" + "MinimumApiVersion": "4.0.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 94a1ce79c..e45015093 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.18.6", + "Version": "4.0.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.18.6" + "MinimumApiVersion": "4.0.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index ee33045b4..9d836b3b9 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -49,7 +49,7 @@ internal static class EarlyConstants internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "3.18.6"; + internal static string RawApiVersion = "4.0.0"; } /// Contains SMAPI's constants and assumptions.