diff options
Diffstat (limited to 'src')
59 files changed, 2025 insertions, 445 deletions
diff --git a/src/SMAPI.Common/Models/ModSeachModel.cs b/src/SMAPI.Common/Models/ModSeachModel.cs index 13b05d2d..3c33d0b6 100644 --- a/src/SMAPI.Common/Models/ModSeachModel.cs +++ b/src/SMAPI.Common/Models/ModSeachModel.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Common.Models /// <summary>The namespaced mod keys to search.</summary> public string[] ModKeys { get; set; } + /// <summary>Whether to allow non-semantic versions, instead of returning an error for those.</summary> + public bool AllowInvalidVersions { get; set; } + /********* ** Public methods @@ -24,9 +27,11 @@ namespace StardewModdingAPI.Common.Models /// <summary>Construct an instance.</summary> /// <param name="modKeys">The namespaced mod keys to search.</param> - public ModSearchModel(IEnumerable<string> modKeys) + /// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param> + public ModSearchModel(IEnumerable<string> modKeys, bool allowInvalidVersions) { this.ModKeys = modKeys.ToArray(); + this.AllowInvalidVersions = allowInvalidVersions; } } } diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs index 1c713b47..084f56a3 100644 --- a/src/SMAPI.Common/SemanticVersionImpl.cs +++ b/src/SMAPI.Common/SemanticVersionImpl.cs @@ -22,6 +22,9 @@ namespace StardewModdingAPI.Common /// <summary>An optional prerelease tag.</summary> public string Tag { get; } + /// <summary>A regex pattern matching a version within a larger string.</summary> + internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?"; + /// <summary>A regular expression matching a semantic version string.</summary> /// <remarks> /// This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, @@ -30,7 +33,7 @@ namespace StardewModdingAPI.Common /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); /// - doesn't allow '+build' suffixes. /// </remarks> - internal static readonly Regex Regex = new Regex(@"^(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + internal static readonly Regex Regex = new Regex($@"^{SemanticVersionImpl.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); /********* ** Public methods diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index 2542a286..6e796461 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -71,12 +71,12 @@ else else x-terminal-emulator -e "$LAUNCHER" fi + elif $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" elif $COMMAND xfce4-terminal 2>/dev/null; then xfce4-terminal -e "$LAUNCHER" elif $COMMAND gnome-terminal 2>/dev/null; then gnome-terminal -e "$LAUNCHER" - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" elif $COMMAND konsole 2>/dev/null; then konsole -e "$LAUNCHER" elif $COMMAND terminal 2>/dev/null; then diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 83f0dcbd..7e8bbfc3 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -67,6 +67,10 @@ <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>false</Private> </Reference> + <Reference Include="Netcode" Condition="Exists('$(GamePath)\Netcode.dll')"> + <HintPath>$(GamePath)\Netcode.dll</HintPath> + <Private>False</Private> + </Reference> <Reference Include="Stardew Valley"> <HintPath>$(GamePath)\Stardew Valley.exe</HintPath> <Private>false</Private> diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 91f38a29..8393ab61 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata> <id>Pathoschild.Stardew.ModBuildConfig</id> - <version>2.0.2</version> + <version>2.0.3-alpha20180307</version> <title>Build package for SMAPI mods</title> <authors>Pathoschild</authors> <owners>Pathoschild</owners> @@ -26,6 +26,9 @@ 2.0.2: - Fixed compatibility issue on Linux. + + 2.0.3: + - Added support for Stardew Valley 1.3. </releaseNotes> </metadata> <files> diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 14a519fb..a6f42b98 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -54,7 +54,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player // apply quality if (match.Item is Object obj) +#if STARDEW_VALLEY_1_3 + obj.Quality = quality; +#else obj.quality = quality; +#endif else if (match.Item is Tool tool) tool.UpgradeLevel = quality; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index 5d098593..aa4fd105 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -36,7 +36,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player switch (target) { case "hair": +#if STARDEW_VALLEY_1_3 + Game1.player.hairstyleColor.Value = color; +#else Game1.player.hairstyleColor = color; +#endif monitor.Log("OK, your hair color is updated.", LogLevel.Info); break; @@ -46,7 +50,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player break; case "pants": +#if STARDEW_VALLEY_1_3 + Game1.player.pantsColor.Value = color; +#else Game1.player.pantsColor = color; +#endif monitor.Log("OK, your pants color is updated.", LogLevel.Info); break; } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs index 5b1225e8..71e17f71 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -1,4 +1,4 @@ -using StardewValley; +using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { @@ -39,7 +39,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player case "farm": if (!string.IsNullOrWhiteSpace(name)) { +#if STARDEW_VALLEY_1_3 + Game1.player.farmName.Value = args[1]; +#else Game1.player.farmName = args[1]; +#endif monitor.Log($"OK, your farm's name is now {Game1.player.farmName}.", LogLevel.Info); } else diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs index da117006..c83c3b07 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs @@ -1,4 +1,4 @@ -using StardewValley; +using StardewValley; using StardewValley.Locations; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -21,7 +21,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { int level = (Game1.currentLocation as MineShaft)?.mineLevel ?? 0; monitor.Log($"OK, warping you to mine level {level + 1}.", LogLevel.Info); +#if STARDEW_VALLEY_1_3 + Game1.enterMine(level + 1); +#else Game1.enterMine(false, level + 1, ""); +#endif } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs index 1024b7b6..5947af1a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -26,7 +26,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World // handle level = Math.Max(1, level); monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info); +#if STARDEW_VALLEY_1_3 + Game1.enterMine(level); +#else Game1.enterMine(true, level, ""); +#endif } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index b5fe9f2f..9c0981c4 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.Xna.Framework; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; @@ -95,39 +95,90 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // fruit products if (item.category == SObject.FruitsCategory) { - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, new SObject(348, 1) + // wine +#if STARDEW_VALLEY_1_3 + SObject wine = + new SObject(348, 1) + { + Name = $"{item.Name} Wine", + Price = item.price * 3 + }; + wine.preserve.Value = SObject.PreserveType.Wine; + wine.preservedParentSheetIndex.Value = item.parentSheetIndex; +#else + SObject wine = new SObject(348, 1) { name = $"{item.Name} Wine", price = item.price * 3, preserve = SObject.PreserveType.Wine, preservedParentSheetIndex = item.parentSheetIndex - }); - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, new SObject(344, 1) + }; +#endif + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset + id, wine); + + // jelly +#if STARDEW_VALLEY_1_3 + SObject jelly = new SObject(344, 1) + { + Name = $"{item.Name} Jelly", + Price = 50 + item.Price * 2 + }; + jelly.preserve.Value = SObject.PreserveType.Jelly; + jelly.preservedParentSheetIndex.Value = item.parentSheetIndex; +#else + SObject jelly = new SObject(344, 1) { name = $"{item.Name} Jelly", price = 50 + item.Price * 2, preserve = SObject.PreserveType.Jelly, preservedParentSheetIndex = item.parentSheetIndex - }); + }; +#endif + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 2 + id, jelly); } // vegetable products else if (item.category == SObject.VegetableCategory) { - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, new SObject(350, 1) + // juice +#if STARDEW_VALLEY_1_3 + SObject juice = new SObject(350, 1) + { + Name = $"{item.Name} Juice", + Price = (int)(item.price * 2.25d) + }; + juice.preserve.Value = SObject.PreserveType.Juice; + juice.preservedParentSheetIndex.Value = item.parentSheetIndex; +#else + SObject juice = new SObject(350, 1) { name = $"{item.Name} Juice", price = (int)(item.price * 2.25d), preserve = SObject.PreserveType.Juice, preservedParentSheetIndex = item.parentSheetIndex - }); - yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, new SObject(342, 1) + }; +#endif + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 3 + id, juice); + + // pickled +#if STARDEW_VALLEY_1_3 + SObject pickled = new SObject(342, 1) + { + Name = $"Pickled {item.Name}", + Price = 50 + item.Price * 2 + }; + pickled.preserve.Value = SObject.PreserveType.Pickle; + pickled.preservedParentSheetIndex.Value = item.parentSheetIndex; +#else + SObject pickled = new SObject(342, 1) { name = $"Pickled {item.Name}", price = 50 + item.Price * 2, preserve = SObject.PreserveType.Pickle, preservedParentSheetIndex = item.parentSheetIndex - }); + }; +#endif + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 4 + id, pickled); } // flower honey @@ -160,6 +211,19 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // yield honey if (type != null) { +#if STARDEW_VALLEY_1_3 + SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) + { + Name = "Wild Honey" + }; + honey.honeyType.Value = type; + + if (type != SObject.HoneyType.Wild) + { + honey.Name = $"{item.Name} Honey"; + honey.Price += item.Price * 2; + } +#else SObject honey = new SObject(Vector2.Zero, 340, item.Name + " Honey", false, true, false, false) { name = "Wild Honey", @@ -170,6 +234,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework honey.name = $"{item.Name} Honey"; honey.price += item.price * 2; } +#endif yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 5 + id, honey); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index f4b3884d..785af01a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.5.2", + "Version": "2.5.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index dcb4ec52..abae7db7 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -64,14 +64,15 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>Fetch version metadata for the given mods.</summary> /// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param> + /// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param> [HttpGet] - public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys) + public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys, bool allowInvalidVersions = false) { string[] modKeysArray = modKeys?.Split(',').ToArray(); if (modKeysArray == null || !modKeysArray.Any()) return new Dictionary<string, ModInfoModel>(); - return await this.PostAsync(new ModSearchModel(modKeysArray)); + return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions)); } /// <summary>Fetch version metadata for the given mods.</summary> @@ -79,7 +80,8 @@ namespace StardewModdingAPI.Web.Controllers [HttpPost] public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search) { - // sort & filter keys + // parse model + bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) .Distinct(StringComparer.CurrentCultureIgnoreCase) .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) @@ -106,12 +108,20 @@ namespace StardewModdingAPI.Web.Controllers // fetch mod info result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - + // fetch info ModInfoModel info = await repository.GetModInfoAsync(modID); - if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) - info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + // validate + if (info.Error == null) + { + if (info.Version == null) + info = new ModInfoModel(info.Name, info.Version, info.Url, "Mod has no version number."); + if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'."); + } + + // cache & return + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); return info; }); } diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index 6772672c..029553ce 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -12,9 +12,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /********* ** Properties *********/ - /// <summary>The base URL for the Chucklefish mod site.</summary> - private readonly string BaseUrl; - /// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary> private readonly string ModPageUrlFormat; @@ -31,7 +28,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> public ChucklefishClient(string userAgent, string baseUrl, string modPageUrlFormat) { - this.BaseUrl = baseUrl; this.ModPageUrlFormat = modPageUrlFormat; this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); } @@ -59,7 +55,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish doc.LoadHtml(html); // extract mod info - string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); + string url = this.GetModUrl(id); string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; if (name.StartsWith("[SMAPI] ")) name = name.Substring("[SMAPI] ".Length); @@ -79,5 +75,18 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish { this.Client?.Dispose(); } + + + /********* + ** Private methods + *********/ + /// <summary>Get the full mod page URL for a given ID.</summary> + /// <param name="id">The mod ID.</param> + private string GetModUrl(uint id) + { + UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); + builder.Path += string.Format(this.ModPageUrlFormat, id); + return builder.Uri.ToString(); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 1a7011e2..adec41be 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -3,7 +3,7 @@ using Pathoschild.Http.Client; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { - /// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary> + /// <summary>An HTTP client for fetching mod metadata from the Nexus Mods API.</summary> internal class NexusClient : INexusClient { /********* diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs index 2b04104f..cd52c72b 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs @@ -17,5 +17,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// <summary>The mod's web URL.</summary> [JsonProperty("mod_page_uri")] public string Url { get; set; } + + /// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary> + [JsonIgnore] + public string Error { get; set; } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs new file mode 100644 index 00000000..d0597965 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs @@ -0,0 +1,106 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus +{ + /// <summary>An HTTP client for fetching mod metadata from the Nexus website.</summary> + internal class NexusWebScrapeClient : INexusClient + { + /********* + ** Properties + *********/ + /// <summary>The URL for a Nexus web page excluding the base URL, where {0} is the mod ID.</summary> + private readonly string ModUrlFormat; + + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="userAgent">The user agent for the Nexus Mods API client.</param> + /// <param name="baseUrl">The base URL for the Nexus Mods site.</param> + /// <param name="modUrlFormat">The URL for a Nexus Mods web page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> + public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat) + { + this.ModUrlFormat = modUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// <summary>Get metadata about a mod.</summary> + /// <param name="id">The Nexus mod ID.</param> + /// <returns>Returns the mod info if found, else <c>null</c>.</returns> + public async Task<NexusMod> GetModAsync(uint id) + { + // fetch HTML + string html; + try + { + html = await this.Client + .GetAsync(string.Format(this.ModUrlFormat, id)) + .AsString(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return null; + } + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // handle Nexus error message + HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); + if (node != null) + { + string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries); + string errorCode = errorParts[0]; + string errorText = errorParts.Length > 1 ? errorParts[1] : null; + switch (errorCode.Trim().ToLower()) + { + case "not found": + return null; + + default: + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText})." }; + } + } + + // extract mod info + string url = this.GetModUrl(id); + string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim(); + string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); + + return new NexusMod + { + Name = name, + Version = version, + Url = url + }; + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the full mod page URL for a given ID.</summary> + /// <param name="id">The mod ID.</param> + private string GetModUrl(uint id) + { + UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); + builder.Path += string.Format(this.ModUrlFormat, id); + return builder.Uri.ToString(); + } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 23a1baa4..9e44f163 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using StardewModdingAPI.Common; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing @@ -29,7 +30,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing private readonly Regex ModListStartPattern = new Regex(@"^Loaded \d+ mods:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary> - private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| (?<description>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + /// <remarks>The author name and description are optional.</remarks> + private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>" + SemanticVersionImpl.UnboundedVersionPattern + @")(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// <summary>A regex pattern matching the start of SMAPI's content pack list.</summary> private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -53,6 +55,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing return new ParsedLog { IsValid = false, + RawText = logText, Error = "The log is empty." }; } @@ -61,7 +64,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing ParsedLog log = new ParsedLog { IsValid = true, - Messages = this.CollapseRepeats(this.GetMessages(logText)).ToArray(), + RawText = logText, + Messages = this.CollapseRepeats(this.GetMessages(logText)).ToArray() }; // parse log messages @@ -152,7 +156,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing { IsValid = false, Error = ex.Message, - RawTextIfError = logText + RawText = logText }; } catch (Exception ex) @@ -161,7 +165,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing { IsValid = false, Error = $"Parsing the log file failed. Technical details:\n{ex}", - RawTextIfError = logText + RawText = logText }; } } diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs index 31ef2fe1..a82b6a1b 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// <summary>An error message indicating why the log file is invalid.</summary> public string Error { get; set; } - /// <summary>The raw text if <see cref="IsValid"/> is false.</summary> - public string RawTextIfError { get; set; } + /// <summary>The raw log text.</summary> + public string RawText { get; set; } /**** ** Log data diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index cfa757ab..e1dc0fcc 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -39,9 +39,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories try { NexusMod mod = await this.Client.GetModAsync(nexusID); - return mod != null - ? new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url) - : new ModInfoModel("Found no mod with this ID."); + if (mod == null) + return new ModInfoModel("Found no mod with this ID."); + if (mod.Error != null) + return new ModInfoModel(mod.Error); + return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index bc3e9f7b..d7d4d074 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -81,8 +81,13 @@ namespace StardewModdingAPI.Web password: api.GitHubPassword )); - services.AddSingleton<INexusClient>(new NexusClient( - userAgent: api.NexusUserAgent, + //services.AddSingleton<INexusClient>(new NexusClient( + // userAgent: api.NexusUserAgent, + // baseUrl: api.NexusBaseUrl, + // modUrlFormat: api.NexusModUrlFormat + //)); + services.AddSingleton<INexusClient>(new NexusWebScrapeClient( + userAgent: userAgent, baseUrl: api.NexusBaseUrl, modUrlFormat: api.NexusModUrlFormat )); @@ -147,7 +152,8 @@ namespace StardewModdingAPI.Web )); // shortcut redirects - redirects.Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index")); + redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility")); + redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); // redirect legacy canimod.com URLs var wikiRedirects = new Dictionary<string, string[]> diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index be9f74a0..d2d8004e 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -17,16 +17,16 @@ @using StardewModdingAPI.Web.Framework.LogParsing.Models @model StardewModdingAPI.Web.ViewModels.LogParserModel @section Head { - <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180101" /> + <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180225" /> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script> - <script src="~/Content/js/log-parser.js?r=20180101"></script> + <script src="~/Content/js/log-parser.js?r=20180225"></script> <script> $(function() { smapi.logParser({ logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)), showPopup: @Json.Serialize(Model.ParsedLog == null), - showMods: @Json.Serialize(Model.ParsedLog?.Mods?.ToDictionary(p => GetSlug(p.Name), p => true), new JsonSerializerSettings { Formatting = Formatting.None }), + showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }), showLevels: { trace: false, debug: false, @@ -78,13 +78,13 @@ <caption> Installed mods: <span class="notice txt"><i>click any mod to filter</i></span> - <span class="notice btn txt" v-on:click="showAllMods" v-if="stats.modsHidden > 0">show all</span> - <span class="notice btn txt" v-on:click="hideAllMods" v-if="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span> + <span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span> + <span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span> </caption> @foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null)) { <tr v-on:click="toggleMod('@GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@GetSlug(mod.Name)'] }"> - <td><input type="checkbox" v-bind:checked="showMods['@GetSlug(mod.Name)']" v-if="anyModsHidden" /></td> + <td><input type="checkbox" v-bind:checked="showMods['@GetSlug(mod.Name)']" v-show="anyModsHidden" /></td> <td> @mod.Name @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) @@ -92,7 +92,7 @@ <div class="content-packs"> @foreach (var contentPack in contentPackList) { - <text>+@contentPack.Name @contentPack.Version</text> + <text>+ @contentPack.Name @contentPack.Version</text><br /> } </div> } @@ -127,9 +127,9 @@ <table id="log"> @foreach (var message in Model.ParsedLog.Messages) { - string levelStr = @message.Level.ToString().ToLower(); + string levelStr = message.Level.ToString().ToLower(); - <tr class="@levelStr mod" v-if="showMods['@message.Mod'] && showLevels['@levelStr']"> + <tr class="@levelStr mod" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')"> <td>@message.Time</td> <td>@message.Level.ToString().ToUpper()</td> <td data-title="@message.Mod">@message.Mod</td> @@ -137,7 +137,7 @@ </tr> if (message.Repeated > 0) { - <tr class="@levelStr mod mod-repeat" v-if="showMods['@message.Mod'] && showLevels['@levelStr']"> + <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')"> <td colspan="3"></td> <td><i>repeats [@message.Repeated] times.</i></td> </tr> @@ -155,7 +155,7 @@ else if (Model.ParsedLog?.IsValid == false) </div> <h3>Raw log</h3> - <pre>@Model.ParsedLog.RawTextIfError</pre> + <pre>@Model.ParsedLog.RawText</pre> } <div id="upload-area"> diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 9758f4a7..3cf72ddb 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -18,7 +18,7 @@ "LogParserUrl": null // see top note }, "ApiClients": { - "UserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "UserAgent": "SMAPI/{0} (+https://smapi.io)", "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 87a70391..38a75a80 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -62,6 +62,7 @@ smapi.logParser = function (data, sectionUrl) { updateModFilters(); }, + showAllMods: function () { for (var key in this.showMods) { if (this.showMods.hasOwnProperty(key)) { @@ -70,6 +71,7 @@ smapi.logParser = function (data, sectionUrl) { } updateModFilters(); }, + hideAllMods: function () { for (var key in this.showMods) { if (this.showMods.hasOwnProperty(key)) { @@ -77,6 +79,10 @@ smapi.logParser = function (data, sectionUrl) { } } updateModFilters(); + }, + + filtersAllow: function(modId, level) { + return this.showMods[modId] !== false && this.showLevels[level] !== false; } } }); diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index fe9fdf9b..d91fa5fb 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -37,13 +37,28 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.5.2"); + public static ISemanticVersion ApiVersion { get; } = +#if STARDEW_VALLEY_1_3 + new SemanticVersion($"2.6-alpha.{DateTime.UtcNow:yyyyMMddHHmm}"); +#else + new SemanticVersion($"2.5.3"); +#endif /// <summary>The minimum supported version of Stardew Valley.</summary> - public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); + public static ISemanticVersion MinimumGameVersion { get; } = +#if STARDEW_VALLEY_1_3 + new GameVersion("1.3.0.4"); +#else + new SemanticVersion("1.2.33"); +#endif /// <summary>The maximum supported version of Stardew Valley.</summary> - public static ISemanticVersion MaximumGameVersion { get; } = null; + public static ISemanticVersion MaximumGameVersion { get; } = +#if STARDEW_VALLEY_1_3 + null; +#else + new SemanticVersion("1.2.33"); +#endif /// <summary>The path to the game folder.</summary> public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -169,7 +184,12 @@ namespace StardewModdingAPI /// <summary>Get the name of a save directory for the current player.</summary> private static string GetSaveFolderName() { - string prefix = new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray()); + string prefix = +#if STARDEW_VALLEY_1_3 + new string(Game1.player.name.Value.Where(char.IsLetterOrDigit).ToArray()); +#else + new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray()); +#endif return $"{prefix}_{Game1.uniqueIDForThisGame}"; } diff --git a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs index fb8c821e..78ba38fa 100644 --- a/src/SMAPI/Events/EventArgsGameLocationsChanged.cs +++ b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using StardewValley; @@ -11,7 +11,7 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// <summary>The current list of game locations.</summary> - public List<GameLocation> NewLocations { get; } + public IList<GameLocation> NewLocations { get; } /********* @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Events *********/ /// <summary>Construct an instance.</summary> /// <param name="newLocations">The current list of game locations.</param> - public EventArgsGameLocationsChanged(List<GameLocation> newLocations) + public EventArgsGameLocationsChanged(IList<GameLocation> newLocations) { this.NewLocations = newLocations; } diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs index a5325b76..75b9b8cd 100644 --- a/src/SMAPI/Events/EventArgsInput.cs +++ b/src/SMAPI/Events/EventArgsInput.cs @@ -18,9 +18,11 @@ namespace StardewModdingAPI.Events /// <summary>The current cursor position.</summary> public ICursorPosition Cursor { get; } +#if !STARDEW_VALLEY_1_3 /// <summary>Whether the input is considered a 'click' by the game for enabling action.</summary> [Obsolete("Use " + nameof(EventArgsInput.IsActionButton) + " or " + nameof(EventArgsInput.IsUseToolButton) + " instead")] // deprecated in SMAPI 2.1 public bool IsClick => this.IsActionButton; +#endif /// <summary>Whether the input should trigger actions on the affected tile.</summary> public bool IsActionButton { get; } diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs index 1ee02842..b85ae9db 100644 --- a/src/SMAPI/Events/EventArgsInventoryChanged.cs +++ b/src/SMAPI/Events/EventArgsInventoryChanged.cs @@ -12,7 +12,11 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// <summary>The player's inventory.</summary> +#if STARDEW_VALLEY_1_3 + public IList<Item> Inventory { get; } +#else public List<Item> Inventory { get; } +#endif /// <summary>The added items.</summary> public List<ItemStackChange> Added { get; } @@ -30,7 +34,13 @@ namespace StardewModdingAPI.Events /// <summary>Construct an instance.</summary> /// <param name="inventory">The player's inventory.</param> /// <param name="changedItems">The inventory changes.</param> - public EventArgsInventoryChanged(List<Item> inventory, List<ItemStackChange> changedItems) + public EventArgsInventoryChanged( +#if STARDEW_VALLEY_1_3 + IList<Item> inventory, +#else + List<Item> inventory, +#endif + List<ItemStackChange> changedItems) { this.Inventory = inventory; this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs index 058999e9..180e9d78 100644 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs @@ -1,6 +1,11 @@ -using System; +using System; using Microsoft.Xna.Framework; +#if STARDEW_VALLEY_1_3 +using System.Collections.Generic; +using Netcode; +#else using StardewValley; +#endif using Object = StardewValley.Object; namespace StardewModdingAPI.Events @@ -12,7 +17,11 @@ namespace StardewModdingAPI.Events ** Accessors *********/ /// <summary>The current list of objects in the current location.</summary> +#if STARDEW_VALLEY_1_3 + public IDictionary<Vector2, NetRef<Object>> NewObjects { get; } +#else public SerializableDictionary<Vector2, Object> NewObjects { get; } +#endif /********* @@ -20,7 +29,13 @@ namespace StardewModdingAPI.Events *********/ /// <summary>Construct an instance.</summary> /// <param name="newObjects">The current list of objects in the current location.</param> - public EventArgsLocationObjectsChanged(SerializableDictionary<Vector2, Object> newObjects) + public EventArgsLocationObjectsChanged( +#if STARDEW_VALLEY_1_3 + IDictionary<Vector2, NetRef<Object>> newObjects +#else + SerializableDictionary<Vector2, Object> newObjects +#endif + ) { this.NewObjects = newObjects; } diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/ContentCore.cs index fa51bd53..85b8db8f 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -19,9 +19,9 @@ using StardewValley; namespace StardewModdingAPI.Framework { - /// <summary>A thread-safe content manager which intercepts assets being loaded to let SMAPI mods inject or edit them.</summary> + /// <summary>A thread-safe content handler which loads assets with support for mod injection and editing.</summary> /// <remarks> - /// This is the centralised content manager which manages all game assets. The game and mods don't use this class + /// This is the centralised content logic which manages all game assets. The game and mods don't use this class /// directly; instead they use one of several <see cref="ContentManagerShim"/> instances, which proxy requests to /// this class. That ensures that when the game disposes one content manager, the others can continue unaffected. /// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously. @@ -30,22 +30,25 @@ namespace StardewModdingAPI.Framework /// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset /// keys, and the game and mods only know about asset names. The content manager handles resolving them. /// </remarks> - internal class SContentManager : LocalizedContentManager + internal class ContentCore : IDisposable { /********* ** Properties *********/ + /// <summary>The underlying content manager.</summary> + private readonly LocalizedContentManager Content; + /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; /// <summary>The underlying asset cache.</summary> private readonly ContentCache Cache; - /// <summary>The private <see cref="LocalizedContentManager"/> method which generates the locale portion of an asset name.</summary> - private readonly IReflectedMethod GetKeyLocale; + /// <summary>The locale codes used in asset keys indexed by enum value.</summary> + private readonly IDictionary<LocalizedContentManager.LanguageCode, string> Locales; - /// <summary>The language codes used in asset keys.</summary> - private readonly IDictionary<string, LanguageCode> KeyLocales; + /// <summary>The language enum values indexed by locale code.</summary> + private readonly IDictionary<string, LocalizedContentManager.LanguageCode> LanguageCodes; /// <summary>Provides metadata for core game assets.</summary> private readonly CoreAssets CoreAssets; @@ -66,15 +69,17 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// <summary>The current language as a constant.</summary> + public LocalizedContentManager.LanguageCode Language => this.Content.GetCurrentLanguage(); + /// <summary>Interceptors which provide the initial versions of matching assets.</summary> - internal IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>(); + public IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>(); /// <summary>Interceptors which edit matching assets after they're loaded.</summary> - internal IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>(); + public IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>(); /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary> - internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.Content.RootDirectory); /********* ** Public methods @@ -89,18 +94,26 @@ namespace StardewModdingAPI.Framework /// <param name="languageCodeOverride">The current language code for which to localise content.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="reflection">Simplifies access to private code.</param> - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) - : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) + public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) { // init this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Cache = new ContentCache(this, reflection); - this.GetKeyLocale = reflection.GetMethod(this, "languageCode"); + this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture, languageCodeOverride); + this.Cache = new ContentCache(this.Content, reflection); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); // get asset data - this.CoreAssets = new CoreAssets(this.NormaliseAssetName); - this.KeyLocales = this.GetKeyLocales(reflection); + this.CoreAssets = new CoreAssets(this.NormaliseAssetName, reflection); + this.Locales = this.GetKeyLocales(reflection); + this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + } + + /// <summary>Get a new content manager which defers loading to the content core.</summary> + /// <param name="name">The content manager's name for logs (if any).</param> + /// <param name="rootDirectory">The root directory to search for content (or <c>null</c>. for the default)</param> + public ContentManagerShim CreateContentManager(string name, string rootDirectory = null) + { + return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture, this.Content.LanguageCodeOverride); } /**** @@ -153,7 +166,14 @@ namespace StardewModdingAPI.Framework /// <summary>Get the current content locale.</summary> public string GetLocale() { - return this.GetKeyLocale.Invoke<string>(); + return this.GetLocale(this.Content.GetCurrentLanguage()); + } + + /// <summary>The locale for a language.</summary> + /// <param name="language">The language.</param> + public string GetLocale(LocalizedContentManager.LanguageCode language) + { + return this.Locales[language]; } /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary> @@ -177,18 +197,15 @@ namespace StardewModdingAPI.Framework /// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> /// <typeparam name="T">The expected asset type.</typeparam> /// <param name="assetName">The asset path relative to the content directory.</param> - public override T Load<T>(string assetName) - { - return this.LoadFor<T>(assetName, this); - } - - /// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> - /// <typeparam name="T">The expected asset type.</typeparam> - /// <param name="assetName">The asset path relative to the content directory.</param> /// <param name="instance">The content manager instance for which to load the asset.</param> + /// <param name="language">The language code for which to load content.</param> /// <exception cref="ArgumentException">The <paramref name="assetName"/> is empty or contains invalid characters.</exception> /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> - public T LoadFor<T>(string assetName, ContentManager instance) + public T Load<T>(string assetName, ContentManager instance +#if STARDEW_VALLEY_1_3 + , LocalizedContentManager.LanguageCode language +#endif + ) { // normalise asset key this.AssertValidAssetKeyFormat(assetName); @@ -196,7 +213,11 @@ namespace StardewModdingAPI.Framework // load game content if (!assetName.StartsWith(this.ModContentPrefix)) +#if STARDEW_VALLEY_1_3 + return this.LoadImpl<T>(assetName, instance, language); +#else return this.LoadImpl<T>(assetName, instance); +#endif // load mod content SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); @@ -206,7 +227,11 @@ namespace StardewModdingAPI.Framework { // try cache if (this.IsLoaded(assetName)) +#if STARDEW_VALLEY_1_3 + return this.LoadImpl<T>(assetName, instance, language); +#else return this.LoadImpl<T>(assetName, instance); +#endif // get file FileInfo file = this.GetModFile(assetName); @@ -218,7 +243,11 @@ namespace StardewModdingAPI.Framework { // XNB file case ".xnb": +#if STARDEW_VALLEY_1_3 + return this.LoadImpl<T>(assetName, instance, language); +#else return this.LoadImpl<T>(assetName, instance); +#endif // unpacked map case ".tbin": @@ -339,7 +368,7 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in removeAssetNames) { - if (this.CoreAssets.ReloadForKey(this, key)) + if (this.CoreAssets.ReloadForKey(Game1.content, key)) // use an intercepted content manager reloaded++; } @@ -379,11 +408,10 @@ namespace StardewModdingAPI.Framework ** Disposal ****/ /// <summary>Dispose held resources.</summary> - /// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param> - protected override void Dispose(bool disposing) + public void Dispose() { this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - base.Dispose(disposing); + this.Content.Dispose(); } /**** @@ -398,29 +426,48 @@ namespace StardewModdingAPI.Framework /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> /// <param name="reflection">Simplifies access to private game code.</param> - private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection) + private IDictionary<LocalizedContentManager.LanguageCode, string> GetKeyLocales(Reflector reflection) { - // get the private code field directly to avoid changed-code logic - IReflectedField<LanguageCode> codeField = reflection.GetField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); - - // remember previous settings - LanguageCode previousCode = codeField.GetValue(); - string previousOverride = this.LanguageCodeOverride; +#if !STARDEW_VALLEY_1_3 + IReflectedField<LocalizedContentManager.LanguageCode> codeField = reflection.GetField<LocalizedContentManager.LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); + LocalizedContentManager.LanguageCode previousCode = codeField.GetValue(); +#endif + string previousOverride = this.Content.LanguageCodeOverride; - // create locale => code map - IDictionary<string, LanguageCode> map = new Dictionary<string, LanguageCode>(StringComparer.InvariantCultureIgnoreCase); - this.LanguageCodeOverride = null; - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + try { - codeField.SetValue(code); - map[this.GetKeyLocale.Invoke<string>()] = code; - } + // temporarily disable language override + this.Content.LanguageCodeOverride = null; + + // create locale => code map + IReflectedMethod languageCodeString = reflection +#if STARDEW_VALLEY_1_3 + .GetMethod(this.Content, "languageCodeString"); +#else + .GetMethod(this.Content, "languageCode"); +#endif + IDictionary<LocalizedContentManager.LanguageCode, string> map = new Dictionary<LocalizedContentManager.LanguageCode, string>(); + foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) + { +#if STARDEW_VALLEY_1_3 + map[code] = languageCodeString.Invoke<string>(code); +#else + codeField.SetValue(code); + map[code] = languageCodeString.Invoke<string>(); +#endif + } - // restore previous settings - codeField.SetValue(previousCode); - this.LanguageCodeOverride = previousOverride; + return map; + } + finally + { + // restore previous settings + this.Content.LanguageCodeOverride = previousOverride; +#if !STARDEW_VALLEY_1_3 + codeField.SetValue(previousCode); +#endif - return map; + } } /// <summary>Get the asset name from a cache key.</summary> @@ -444,7 +491,7 @@ namespace StardewModdingAPI.Framework if (lastSepIndex >= 0) { string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); - if (this.KeyLocales.ContainsKey(suffix)) + if (this.LanguageCodes.ContainsKey(suffix)) { assetName = cacheKey.Substring(0, lastSepIndex); localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); @@ -466,7 +513,7 @@ namespace StardewModdingAPI.Framework private bool IsNormalisedKeyLoaded(string normalisedAssetName) { return this.Cache.ContainsKey(normalisedAssetName) - || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset + || this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}"); // translated asset } /// <summary>Track that a content manager loaded an asset.</summary> @@ -486,7 +533,12 @@ namespace StardewModdingAPI.Framework /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> /// <param name="instance">The content manager instance for which to load the asset.</param> - private T LoadImpl<T>(string assetName, ContentManager instance) + /// <param name="language">The language code for which to load content.</param> + private T LoadImpl<T>(string assetName, ContentManager instance +#if STARDEW_VALLEY_1_3 + , LocalizedContentManager.LanguageCode language +#endif + ) { return this.WithWriteLock(() => { @@ -494,7 +546,13 @@ namespace StardewModdingAPI.Framework if (this.IsNormalisedKeyLoaded(assetName)) { this.TrackAssetLoader(assetName, instance); - return base.Load<T>(assetName); + return this.Content + +#if STARDEW_VALLEY_1_3 + .Load<T>(assetName, language); +#else + .Load<T>(assetName); +#endif } // load asset @@ -503,14 +561,30 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load<T>(assetName); + data = this.Content +#if STARDEW_VALLEY_1_3 + .Load<T>(assetName, language); +#else + .Load<T>(assetName); +#endif } else { data = this.AssetsBeingLoaded.Track(assetName, () => { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName); + string locale = +#if STARDEW_VALLEY_1_3 + this.GetLocale(language); +#else + this.GetLocale(); +#endif + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader<T>(info) +#if STARDEW_VALLEY_1_3 + ?? new AssetDataForObject(info, this.Content.Load<T>(assetName, language), this.NormaliseAssetName); +#else + ?? new AssetDataForObject(info, this.Content.Load<T>(assetName), this.NormaliseAssetName); +#endif asset = this.ApplyEditors<T>(info, asset); return (T)asset.Data; }); diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs index d46f23a3..8f88fc2d 100644 --- a/src/SMAPI/Framework/ContentManagerShim.cs +++ b/src/SMAPI/Framework/ContentManagerShim.cs @@ -1,15 +1,17 @@ +using System; +using System.Globalization; using StardewValley; namespace StardewModdingAPI.Framework { - /// <summary>A minimal content manager which defers to SMAPI's main content manager.</summary> + /// <summary>A minimal content manager which defers to SMAPI's core content logic.</summary> internal class ContentManagerShim : LocalizedContentManager { /********* ** Properties *********/ - /// <summary>SMAPI's underlying content manager.</summary> - private readonly SContentManager ContentManager; + /// <summary>SMAPI's core content logic.</summary> + private readonly ContentCore ContentCore; /********* @@ -23,12 +25,16 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="contentManager">SMAPI's underlying content manager.</param> + /// <param name="contentCore">SMAPI's core content logic.</param> /// <param name="name">The content manager's name for logs (if any).</param> - public ContentManagerShim(SContentManager contentManager, string name) - : base(contentManager.ServiceProvider, contentManager.RootDirectory, contentManager.CurrentCulture, contentManager.LanguageCodeOverride) + /// <param name="serviceProvider">The service provider to use to locate services.</param> + /// <param name="rootDirectory">The root directory to search for content.</param> + /// <param name="currentCulture">The current culture for which to localise content.</param> + /// <param name="languageCodeOverride">The current language code for which to localise content.</param> + public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride) + : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { - this.ContentManager = contentManager; + this.ContentCore = contentCore; this.Name = name; } @@ -37,14 +43,58 @@ namespace StardewModdingAPI.Framework /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> public override T Load<T>(string assetName) { - return this.ContentManager.LoadFor<T>(assetName, this); +#if STARDEW_VALLEY_1_3 + return this.Load<T>(assetName, LocalizedContentManager.CurrentLanguageCode); +#else + return this.ContentCore.Load<T>(assetName, this); +#endif } +#if STARDEW_VALLEY_1_3 + /// <summary>Load an asset that has been processed by the content pipeline.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="language">The language code for which to load content.</param> + public override T Load<T>(string assetName, LanguageCode language) + { + return this.ContentCore.Load<T>(assetName, this, language); + } + + /// <summary>Load the base asset without localisation.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + public override T LoadBase<T>(string assetName) + { + return this.Load<T>(assetName, LanguageCode.en); + } +#endif + + /// <summary>Inject an asset into the cache.</summary> + /// <typeparam name="T">The type of asset to inject.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="value">The asset value.</param> + public void Inject<T>(string assetName, T value) + { + this.ContentCore.Inject<T>(assetName, value, this); + } + +#if STARDEW_VALLEY_1_3 + /// <summary>Create a new content manager for temporary use.</summary> + public override LocalizedContentManager CreateTemporary() + { + return this.ContentCore.CreateContentManager("(temporary)"); + } +#endif + + + /********* + ** Protected methods + *********/ /// <summary>Dispose held resources.</summary> /// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param> protected override void Dispose(bool disposing) { - this.ContentManager.DisposeFor(this); + this.ContentCore.DisposeFor(this); } } } diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 1884afe9..e5022212 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -49,21 +49,31 @@ namespace StardewModdingAPI.Framework /// <param name="gameVersion">The game version string.</param> private static string GetSemanticVersionString(string gameVersion) { +#if STARDEW_VALLEY_1_3 + if(gameVersion.StartsWith("1.3.0.")) + return new SemanticVersion(1, 3, 0, "alpha." + gameVersion.Substring("1.3.0.".Length)).ToString(); +#endif + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) ? semanticVersion : gameVersion; } - /// <summary>Convert a game version string to a semantic version string.</summary> - /// <param name="gameVersion">The game version string.</param> - private static string GetGameVersionString(string gameVersion) + /// <summary>Convert a semantic version string to the equivalent game version string.</summary> + /// <param name="semanticVersion">The semantic version string.</param> + private static string GetGameVersionString(string semanticVersion) { + #if STARDEW_VALLEY_1_3 + if(semanticVersion.StartsWith("1.3-alpha.")) + return "1.3.0." + semanticVersion.Substring("1.3-alpha.".Length); + #endif + foreach (var mapping in GameVersion.VersionMap) { - if (mapping.Value.Equals(gameVersion, StringComparison.InvariantCultureIgnoreCase)) + if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase)) return mapping.Key; } - return gameVersion; + return semanticVersion; } } } diff --git a/src/SMAPI/Framework/ModData/ModDataField.cs b/src/SMAPI/Framework/ModData/ModDataField.cs index fa8dd6d0..df906103 100644 --- a/src/SMAPI/Framework/ModData/ModDataField.cs +++ b/src/SMAPI/Framework/ModData/ModDataField.cs @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.ModData { // update key case ModDataFieldKey.UpdateKey: - return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(); + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); // non-manifest fields case ModDataFieldKey.AlternativeUrl: diff --git a/src/SMAPI/Framework/ModData/ModDataRecord.cs b/src/SMAPI/Framework/ModData/ModDataRecord.cs index 79a954f7..56275f53 100644 --- a/src/SMAPI/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ModDataRecord.cs @@ -106,10 +106,10 @@ namespace StardewModdingAPI.Framework.ModData /// <summary>Get a semantic local version for update checks.</summary> /// <param name="version">The remote version to normalise.</param> - public string GetLocalVersionForUpdateChecks(string version) + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) - ? newVersion + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) + ? new SemanticVersion(newVersion) : version; } @@ -117,6 +117,11 @@ namespace StardewModdingAPI.Framework.ModData /// <param name="version">The remote version to normalise.</param> public string GetRemoteVersionForUpdateChecks(string version) { + // normalise version if possible + if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + version = parsed.ToString(); + + // fetch remote version return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) ? newVersion : version; diff --git a/src/SMAPI/Framework/ModData/ModDatabase.cs b/src/SMAPI/Framework/ModData/ModDatabase.cs index 332c5c48..3fd68440 100644 --- a/src/SMAPI/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI/Framework/ModData/ModDatabase.cs @@ -157,7 +157,7 @@ namespace StardewModdingAPI.Framework.ModData && ( snapshot.Author == null || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + || (manifest.ExtraFields != null && manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) ) && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); diff --git a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs index 7f49790d..deb12bdc 100644 --- a/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs +++ b/src/SMAPI/Framework/ModData/ParsedModDataRecord.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.ModData *********/ /// <summary>Get a semantic local version for update checks.</summary> /// <param name="version">The remote version to normalise.</param> - public string GetLocalVersionForUpdateChecks(string version) + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) { return this.DataRecord.GetLocalVersionForUpdateChecks(version); } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 7d8bec1e..c7d4c39e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -22,8 +22,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Properties *********/ - /// <summary>SMAPI's underlying content manager.</summary> - private readonly SContentManager ContentManager; + /// <summary>SMAPI's core content logic.</summary> + private readonly ContentCore ContentCore; + + /// <summary>The content manager for this mod.</summary> + private readonly ContentManagerShim ContentManager; /// <summary>The absolute path to the mod folder.</summary> private readonly string ModFolderPath; @@ -39,10 +42,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Accessors *********/ /// <summary>The game's current locale code (like <c>pt-BR</c>).</summary> - public string CurrentLocale => this.ContentManager.GetLocale(); + public string CurrentLocale => this.ContentCore.GetLocale(); /// <summary>The game's current locale as an enum value.</summary> - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentCore.Language; /// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary> internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>(); @@ -61,14 +64,16 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="contentManager">SMAPI's underlying content manager.</param> + /// <param name="contentCore">SMAPI's core content logic.</param> + /// <param name="contentManager">The content manager for this mod.</param> /// <param name="modFolderPath">The absolute path to the mod folder.</param> /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The friendly mod name for use in errors.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + public ContentHelper(ContentCore contentCore, ContentManagerShim contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { + this.ContentCore = contentCore; this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; @@ -100,10 +105,10 @@ namespace StardewModdingAPI.Framework.ModHelpers throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path - string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName); + string assetName = this.ContentCore.GetAssetNameFromFilePath(file.FullName); // try cache - if (this.ContentManager.IsLoaded(assetName)) + if (this.ContentCore.IsLoaded(assetName)) return this.ContentManager.Load<T>(assetName); // fix map tilesheets @@ -119,7 +124,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.FixCustomTilesheetPaths(map, key); // inject map - this.ContentManager.Inject(assetName, map, this.ContentManager); + this.ContentManager.Inject(assetName, map); return (T)(object)map; } @@ -141,7 +146,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [Pure] public string NormaliseAssetName(string assetName) { - return this.ContentManager.NormaliseAssetName(assetName); + return this.ContentCore.NormaliseAssetName(assetName); } /// <summary>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.</summary> @@ -153,11 +158,11 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentManager.NormaliseAssetName(key); + return this.ContentCore.NormaliseAssetName(key); case ContentSource.ModFolder: FileInfo file = this.GetModFile(key); - return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName)); + return this.ContentCore.NormaliseAssetName(this.ContentCore.GetAssetNameFromFilePath(file.FullName)); default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -172,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); - return this.ContentManager.InvalidateCache(asset => asset.AssetNameEquals(actualKey)); + return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)); } /// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary> @@ -181,7 +186,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache<T>() { this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); - return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); } /// <summary>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.</summary> @@ -190,7 +195,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache(Func<IAssetInfo, bool> predicate) { this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); - return this.ContentManager.InvalidateCache(predicate); + return this.ContentCore.InvalidateCache(predicate); } /********* @@ -202,7 +207,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] private void AssertValidAssetKeyFormat(string key) { - this.ContentManager.AssertValidAssetKeyFormat(key); + this.ContentCore.AssertValidAssetKeyFormat(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } @@ -230,7 +235,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get map info if (!map.TileSheets.Any()) return; - mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + mapKey = this.ContentCore.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets @@ -336,7 +341,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetModFile(string path) { // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, this.ContentCore.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); // try with default extension @@ -355,7 +360,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetContentFolderFile(string key) { // get file path - string path = Path.Combine(this.ContentManager.FullRootDirectory, key); + string path = Path.Combine(this.ContentCore.FullRootDirectory, key); if (!path.EndsWith(".xnb")) path += ".xnb"; diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 81453003..e5bf47f6 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ); } - +#if !STARDEW_VALLEY_1_3 /**** ** Obsolete ****/ @@ -221,6 +221,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.DeprecationManager.Warn($"{nameof(IReflectionHelper)}.GetPrivate*", "2.3", DeprecationLevel.Notice); return (IPrivateMethod)this.GetMethod(type, name, required); } +#endif /********* diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index 4378798c..d85a9a28 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Mono.Cecil; namespace StardewModdingAPI.Framework.ModLoading diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index ccbd053e..a60f63da 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -16,17 +16,20 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Properties *********/ + /// <summary>Encapsulates monitoring and logging.</summary> + private readonly IMonitor Monitor; + + /// <summary>Whether to enable developer mode logging.</summary> + private readonly bool IsDeveloperMode; + /// <summary>Metadata for mapping assemblies to the current platform.</summary> private readonly PlatformAssemblyMap AssemblyMap; /// <summary>A type => assembly lookup for types which should be rewritten.</summary> private readonly IDictionary<string, Assembly> TypeAssemblies; - /// <summary>Encapsulates monitoring and logging.</summary> - private readonly IMonitor Monitor; - - /// <summary>Whether to enable developer mode logging.</summary> - private readonly bool IsDeveloperMode; + /// <summary>A minimal assembly definition resolver which resolves references to known loaded assemblies.</summary> + private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver; /********* @@ -41,6 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor = monitor; this.IsDeveloperMode = isDeveloperMode; this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); + this.AssemblyDefinitionResolver = new AssemblyDefinitionResolver(); // generate type => assembly lookup for types which should be rewritten this.TypeAssemblies = new Dictionary<string, Assembly>(); @@ -69,9 +73,8 @@ namespace StardewModdingAPI.Framework.ModLoading // get referenced local assemblies AssemblyParseResult[] assemblies; { - AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); HashSet<string> visitedAssemblyNames = new HashSet<string>(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded - assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); + assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, this.AssemblyDefinitionResolver).ToArray(); } // validate load @@ -94,7 +97,10 @@ namespace StardewModdingAPI.Framework.ModLoading if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; + // rewrite assembly bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + + // load assembly if (changed) { if (!oneAssembly) @@ -112,6 +118,9 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } + + // track loaded assembly for definition resolution + this.AssemblyDefinitionResolver.Add(assembly.Definition); } // last assembly loaded is the root @@ -166,7 +175,6 @@ namespace StardewModdingAPI.Framework.ModLoading yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); yield break; } - visitedAssemblyNames.Add(assembly.Name.Name); // yield referenced assemblies @@ -238,10 +246,13 @@ namespace StardewModdingAPI.Framework.ModLoading // check CIL instructions ILProcessor cil = method.Body.GetILProcessor(); - foreach (Instruction instruction in cil.Body.Instructions.ToArray()) + var instructions = cil.Body.Instructions; + // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers + for (int offset = 0; offset < instructions.Count; offset++) { foreach (IInstructionHandler handler in handlers) { + Instruction instruction = instructions[offset]; InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) @@ -270,9 +281,10 @@ namespace StardewModdingAPI.Framework.ModLoading break; case InstructionHandleResult.NotCompatible: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); if (!assumeCompatible) throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found broken code ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); break; case InstructionHandleResult.DetectedGamePatch: diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs new file mode 100644 index 00000000..b5e45742 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// <summary>Finds references to a field, property, or method which returns a different type than the code expects.</summary> + /// <remarks>This implementation is purely heuristic. It should never return a false positive, but won't detect all cases.</remarks> + internal class ReferenceToMemberWithUnexpectedTypeFinder : IInstructionHandler + { + /********* + ** Properties + *********/ + /// <summary>The assembly names to which to heuristically detect broken references.</summary> + private readonly HashSet<string> ValidateReferencesToAssemblies; + + /// <summary>A pattern matching type name substrings to strip for display.</summary> + private readonly Regex StripTypeNamePattern = new Regex(@"`\d+(?=<)", RegexOptions.Compiled); + + + /********* + ** Accessors + *********/ + /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> + public string NounPhrase { get; private set; } = ""; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="validateReferencesToAssemblies">The assembly names to which to heuristically detect broken references.</param> + public ReferenceToMemberWithUnexpectedTypeFinder(string[] validateReferencesToAssemblies) + { + this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); + } + + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) + { + // can't compare generic type parameters between definition and reference + if (fieldRef.FieldType.IsGenericInstance || fieldRef.FieldType.IsGenericParameter) + return InstructionHandleResult.None; + + // get target field + FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); + if (targetField == null) + return InstructionHandleResult.None; + + // validate return type + string actualReturnTypeID = this.GetComparableTypeID(targetField.FieldType); + string expectedReturnTypeID = this.GetComparableTypeID(fieldRef.FieldType); + if (actualReturnTypeID != expectedReturnTypeID) + { + this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType, actualReturnTypeID)}, not {this.GetFriendlyTypeName(fieldRef.FieldType, expectedReturnTypeID)})"; + return InstructionHandleResult.NotCompatible; + } + } + + // method reference + MethodReference methodReference = RewriteHelper.AsMethodReference(instruction); + if (methodReference != null && this.ShouldValidate(methodReference.DeclaringType)) + { + // can't compare generic type parameters between definition and reference + if (methodReference.ReturnType.IsGenericInstance || methodReference.ReturnType.IsGenericParameter) + return InstructionHandleResult.None; + + // get potential targets + MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray(); + if (candidateMethods == null || !candidateMethods.Any()) + return InstructionHandleResult.None; + + // compare return types + MethodDefinition methodDef = methodReference.Resolve(); + if (methodDef == null) + { + this.NounPhrase = $"reference to {methodReference.DeclaringType.FullName}.{methodReference.Name} (no such method)"; + return InstructionHandleResult.NotCompatible; + } + + string expectedReturnType = this.GetComparableTypeID(methodDef.ReturnType); + if (candidateMethods.All(method => this.GetComparableTypeID(method.ReturnType) != expectedReturnType)) + { + this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType, expectedReturnType)})"; + return InstructionHandleResult.NotCompatible; + } + } + + return InstructionHandleResult.None; + } + + + /********* + ** Private methods + *********/ + /// <summary>Whether references to the given type should be validated.</summary> + /// <param name="type">The type reference.</param> + private bool ShouldValidate(TypeReference type) + { + return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// <summary>Get a unique string representation of a type.</summary> + /// <param name="type">The type reference.</param> + private string GetComparableTypeID(TypeReference type) + { + return this.StripTypeNamePattern.Replace(type.FullName, ""); + } + + /// <summary>Get a shorter type name for display.</summary> + /// <param name="type">The type reference.</param> + /// <param name="typeID">The comparable type ID from <see cref="GetComparableTypeID"/>.</param> + private string GetFriendlyTypeName(TypeReference type, string typeID) + { + // most common built-in types + switch (type.FullName) + { + case "System.Boolean": + return "bool"; + case "System.Int32": + return "int"; + case "System.String": + return "string"; + } + + // most common unambiguous namespaces + foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" }) + { + if (type.Namespace == @namespace) + return typeID.Substring(@namespace.Length + 1); + } + + return typeID; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs new file mode 100644 index 00000000..f5e33313 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// <summary>Finds references to a field, property, or method which no longer exists.</summary> + /// <remarks>This implementation is purely heuristic. It should never return a false positive, but won't detect all cases.</remarks> + internal class ReferenceToMissingMemberFinder : IInstructionHandler + { + /********* + ** Properties + *********/ + /// <summary>The assembly names to which to heuristically detect broken references.</summary> + private readonly HashSet<string> ValidateReferencesToAssemblies; + + + /********* + ** Accessors + *********/ + /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> + public string NounPhrase { get; private set; } = ""; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="validateReferencesToAssemblies">The assembly names to which to heuristically detect broken references.</param> + public ReferenceToMissingMemberFinder(string[] validateReferencesToAssemblies) + { + this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); + } + + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="method">The method definition containing the instruction.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) + { + FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); + if (target == null) + { + this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"; + return InstructionHandleResult.NotCompatible; + } + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef)) + { + MethodDefinition target = methodRef.DeclaringType.Resolve()?.Methods.FirstOrDefault(p => p.Name == methodRef.Name); + if (target == null) + { + this.NounPhrase = this.IsProperty(methodRef) + ? $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)" + : $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + return InstructionHandleResult.NotCompatible; + } + } + + return InstructionHandleResult.None; + } + + + /********* + ** Private methods + *********/ + /// <summary>Whether references to the given type should be validated.</summary> + /// <param name="type">The type reference.</param> + private bool ShouldValidate(TypeReference type) + { + return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// <summary>Get whether a method reference is a special case that's not currently supported (e.g. array methods).</summary> + /// <param name="method">The method reference.</param> + private bool IsUnsupported(MethodReference method) + { + return + method.DeclaringType.Name.Contains("["); // array methods + } + + /// <summary>Get whether a method reference is a property getter or setter.</summary> + /// <param name="method">The method reference.</param> + private bool IsProperty(MethodReference method) + { + return method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index ba6dab1a..f878a1b9 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -98,7 +98,7 @@ namespace StardewModdingAPI.Framework.ModLoading case ModStatus.AssumeBroken: { // get reason - string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's outdated"; // get update URLs List<string> updateUrls = new List<string>(); @@ -111,6 +111,9 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.DataRecord.AlternativeUrl != null) updateUrls.Add(mod.DataRecord.AlternativeUrl); + // default update URL + updateUrls.Add("https://smapi.io/compat"); + // build error string error = $"{reasonPhrase}. Please check for a "; if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index a20b8bee..b1fa377a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -14,8 +14,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <summary>The type whose field to which references should be rewritten.</summary> private readonly Type Type; - /// <summary>The field name to rewrite.</summary> - private readonly string FieldName; + /// <summary>The property name.</summary> + private readonly string PropertyName; /********* @@ -24,13 +24,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <summary>Construct an instance.</summary> /// <param name="type">The type whose field to which references should be rewritten.</param> /// <param name="fieldName">The field name to rewrite.</param> - public FieldToPropertyRewriter(Type type, string fieldName) + /// <param name="propertyName">The property name (if different).</param> + public FieldToPropertyRewriter(Type type, string fieldName, string propertyName) : base(type.FullName, fieldName, InstructionHandleResult.None) { this.Type = type; - this.FieldName = fieldName; + this.PropertyName = propertyName; } + /// <summary>Construct an instance.</summary> + /// <param name="type">The type whose field to which references should be rewritten.</param> + /// <param name="fieldName">The field name to rewrite.</param> + public FieldToPropertyRewriter(Type type, string fieldName) + : this(type, fieldName, fieldName) { } + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> @@ -43,8 +50,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return InstructionHandleResult.None; string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.FieldName}")); + MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); + return InstructionHandleResult.Rewritten; } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs new file mode 100644 index 00000000..5e12b46a --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs @@ -0,0 +1,63 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// <summary>Rewrites static field references into constant values.</summary> + /// <typeparam name="TValue">The constant value type.</typeparam> + internal class StaticFieldToConstantRewriter<TValue> : FieldFinder + { + /********* + ** Properties + *********/ + /// <summary>The constant value to replace with.</summary> + private readonly TValue Value; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="type">The type whose field to which references should be rewritten.</param> + /// <param name="fieldName">The field name to rewrite.</param> + /// <param name="value">The constant value to replace with.</param> + public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value) + : base(type.FullName, fieldName, InstructionHandleResult.None) + { + this.Value = value; + } + + /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The instruction to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + cil.Replace(instruction, this.CreateConstantInstruction(cil, this.Value)); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Private methods + *********/ + /// <summary>Create a CIL constant value instruction.</summary> + /// <param name="cil">The CIL processor.</param> + /// <param name="value">The constant value to set.</param> + private Instruction CreateConstantInstruction(ILProcessor cil, object value) + { + if (typeof(TValue) == typeof(int)) + return cil.Create(OpCodes.Ldc_I4, (int)value); + if (typeof(TValue) == typeof(string)) + return cil.Create(OpCodes.Ldstr, (string)value); + throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); + } + } +} diff --git a/src/SMAPI/Framework/Reflection/ReflectedField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs index ad1557bb..fb420dc5 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedField.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs @@ -5,7 +5,10 @@ namespace StardewModdingAPI.Framework.Reflection { /// <summary>A field obtained through reflection.</summary> /// <typeparam name="TValue">The field value type.</typeparam> - internal class ReflectedField<TValue> : IPrivateField<TValue>, IReflectedField<TValue> + internal class ReflectedField<TValue> : IReflectedField<TValue> +#if !STARDEW_VALLEY_1_3 + , IPrivateField<TValue> +#endif { /********* ** Properties diff --git a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs index 376de869..803bc316 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs @@ -4,7 +4,10 @@ using System.Reflection; namespace StardewModdingAPI.Framework.Reflection { /// <summary>A method obtained through reflection.</summary> - internal class ReflectedMethod : IPrivateMethod, IReflectedMethod + internal class ReflectedMethod : IReflectedMethod +#if !STARDEW_VALLEY_1_3 + , IPrivateMethod +#endif { /********* ** Properties diff --git a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs index d6c964c1..4f9d4e19 100644 --- a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs +++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs @@ -5,7 +5,10 @@ namespace StardewModdingAPI.Framework.Reflection { /// <summary>A property obtained through reflection.</summary> /// <typeparam name="TValue">The property value type.</typeparam> - internal class ReflectedProperty<TValue> : IPrivateProperty<TValue>, IReflectedProperty<TValue> + internal class ReflectedProperty<TValue> : IReflectedProperty<TValue> +#if !STARDEW_VALLEY_1_3 + , IPrivateProperty<TValue> +#endif { /********* ** Properties diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 5c45edca..47bc40e6 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -9,6 +9,9 @@ using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; +#if STARDEW_VALLEY_1_3 +using Netcode; +#endif using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; @@ -20,7 +23,9 @@ using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; +#if !STARDEW_VALLEY_1_3 using xTile.Layers; +#endif namespace StardewModdingAPI.Framework { @@ -31,6 +36,16 @@ namespace StardewModdingAPI.Framework ** Properties *********/ /**** + ** Constructor hack + ****/ + /// <summary>A static instance of <see cref="Monitor"/> to use while <see cref="Game1"/> is initialising, which happens before the <see cref="SGame"/> constructor runs.</summary> + internal static IMonitor MonitorDuringInitialisation; + + /// <summary>A static instance of <see cref="Reflection"/> to use while <see cref="Game1"/> is initialising, which happens before the <see cref="SGame"/> constructor runs.</summary> + internal static Reflector ReflectorDuringInitialisation; + + + /**** ** SMAPI state ****/ /// <summary>Encapsulates monitoring and logging.</summary> @@ -145,6 +160,9 @@ namespace StardewModdingAPI.Framework private readonly Action drawFarmBuildings = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); private readonly Action drawHUD = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawHUD)).Invoke(); private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); +#if STARDEW_VALLEY_1_3 + private readonly Action<SpriteBatch> drawOverlays = spriteBatch => SGame.Reflection.GetMethod(SGame.Instance, nameof(SGame.drawOverlays)).Invoke(spriteBatch); +#endif private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming @@ -153,7 +171,7 @@ namespace StardewModdingAPI.Framework ** Accessors *********/ /// <summary>SMAPI's content manager.</summary> - public SContentManager SContentManager { get; } + public ContentCore ContentCore { get; private set; } /// <summary>Whether SMAPI should log more information about the game context.</summary> public bool VerboseLogging { get; set; } @@ -176,16 +194,18 @@ namespace StardewModdingAPI.Framework SGame.Instance = this; SGame.Reflection = reflection; this.OnGameInitialised = onGameInitialised; + if (this.ContentCore == null) // shouldn't happen since CreateContentManager is called first, but let's init here just in case + this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); // set XNA option required by Stardew Valley Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - // override content manager +#if !STARDEW_VALLEY_1_3 + // replace already-created content managers this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); - this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); - this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); - Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); - reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager + this.Content = this.ContentCore.CreateContentManager("SGame.Content"); + reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(this.ContentCore.CreateContentManager("Game1._temporaryContent")); // regenerate value with new content manager +#endif } /**** @@ -196,19 +216,14 @@ namespace StardewModdingAPI.Framework /// <param name="rootDirectory">The root directory to search for content.</param> protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // return default if SMAPI's content manager isn't initialised yet - if (this.SContentManager == null) + // NOTE: this method is called from the Game1 constructor, before the SGame constructor runs. + // Don't depend on anything being initialised at this point. + if (this.ContentCore == null) { - this.Monitor?.Log("SMAPI's content manager isn't initialised; skipping content manager interception.", LogLevel.Trace); - return base.CreateContentManager(serviceProvider, rootDirectory); + this.ContentCore = new ContentCore(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, null, SGame.MonitorDuringInitialisation, SGame.ReflectorDuringInitialisation); + SGame.MonitorDuringInitialisation = null; } - - // return single instance if valid - if (serviceProvider != this.Content.ServiceProvider) - throw new InvalidOperationException("SMAPI uses a single content manager internally. You can't get a new content manager with a different service provider."); - if (rootDirectory != this.Content.RootDirectory) - throw new InvalidOperationException($"SMAPI uses a single content manager internally. You can't get a new content manager with a different root directory (current is {this.Content.RootDirectory}, requested {rootDirectory})."); - return new ContentManagerShim(this.SContentManager, "(generated instance)"); + return this.ContentCore.CreateContentManager("(generated)", rootDirectory); } /// <summary>The method called when the game is updating its state. This happens roughly 60 times per second.</summary> @@ -488,6 +503,7 @@ namespace StardewModdingAPI.Framework if (Context.IsWorldReady) { // raise current location changed + // ReSharper disable once PossibleUnintendedReferenceComparison if (Game1.currentLocation != this.PreviousGameLocation) { if (this.VerboseLogging) @@ -523,7 +539,13 @@ namespace StardewModdingAPI.Framework // raise current location's object list changed if (this.GetHash(Game1.currentLocation.objects) != this.PreviousLocationObjects) - this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged(Game1.currentLocation.objects)); + this.Events.Location_LocationObjectsChanged.Raise(new EventArgsLocationObjectsChanged( +#if STARDEW_VALLEY_1_3 + Game1.currentLocation.objects.FieldDict +#else + Game1.currentLocation.objects +#endif + )); // raise time changed if (Game1.timeOfDay != this.PreviousTime) @@ -650,6 +672,619 @@ namespace StardewModdingAPI.Framework [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")] +#if STARDEW_VALLEY_1_3 + private void DrawImpl(GameTime gameTime) + { + if (Game1.debugMode) + { + if (SGame._fpsStopwatch.IsRunning) + { + float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; + SGame._fpsList.Add(totalSeconds); + while (SGame._fpsList.Count >= 120) + SGame._fpsList.RemoveAt(0); + float num = 0.0f; + foreach (float fps in SGame._fpsList) + num += fps; + SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); + } + SGame._fpsStopwatch.Restart(); + } + else + { + if (SGame._fpsStopwatch.IsRunning) + SGame._fpsStopwatch.Reset(); + SGame._fps = 0.0f; + SGame._fpsList.Clear(); + } + if (SGame._newDayTask != null) + { + this.GraphicsDevice.Clear(this.bgColor); + //base.Draw(gameTime); + } + else + { + if ((double)Game1.options.zoomLevel != 1.0) + this.GraphicsDevice.SetRenderTarget(this.screenWrapper); + if (this.IsSaving) + { + this.GraphicsDevice.Clear(this.bgColor); + IClickableMenu activeClickableMenu = Game1.activeClickableMenu; + if (activeClickableMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + try + { + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + activeClickableMenu.draw(Game1.spriteBatch); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + } + 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(); + } + this.RaisePostRender(); + Game1.spriteBatch.End(); + } + //base.Draw(gameTime); + this.renderScreenBuffer(); + } + else + { + this.GraphicsDevice.Clear(this.bgColor); + if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + try + { + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + this.RaisePostRender(); + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + this.drawOverlays(Game1.spriteBatch); + } + else if ((int)Game1.gameMode == 11) + { + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + 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, (int)byte.MaxValue, 0)); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + this.RaisePostRender(); + Game1.spriteBatch.End(); + } + else if (Game1.currentMinigame != null) + { + Game1.currentMinigame.draw(Game1.spriteBatch); + if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.End(); + } + this.RaisePostRender(needsNewBatch: true); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + this.drawOverlays(Game1.spriteBatch); + } + else if (Game1.showingEndOfNightStuff) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.activeClickableMenu != null) + { + try + { + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + } + this.RaisePostRender(); + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + this.drawOverlays(Game1.spriteBatch); + } + else + { + int num1; + switch (Game1.gameMode) + { + case 3: + num1 = Game1.currentLocation == null ? 1 : 0; + break; + case 6: + num1 = 1; + break; + default: + num1 = 0; + break; + } + if (num1 != 0) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + string str1 = ""; + for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) + str1 += "."; + string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); + string s = str2 + str1; + string str3 = str2 + "... "; + int widthOfString = SpriteText.getWidthOfString(str3); + int height = 64; + int x = 64; + int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; + SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); + Game1.spriteBatch.End(); + if ((double)Game1.options.zoomLevel != 1.0) + { + this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + this.drawOverlays(Game1.spriteBatch); + } + else + { + Viewport viewport1; + if ((int)Game1.gameMode == 0) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + } + else + { + Microsoft.Xna.Framework.Rectangle bounds; + if (Game1.drawLighting) + { + this.GraphicsDevice.SetRenderTarget(Game1.lightmap); + this.GraphicsDevice.Clear(Color.White * 0.0f); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.Name.StartsWith("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight)); + for (int index = 0; index < Game1.currentLightSources.Count; ++index) + { + if (Utility.isOnScreen((Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position), (int)((double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) * 64.0 * 4.0))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D lightTexture = Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture; + Vector2 position = Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position)) / (float)(Game1.options.lightingQuality / 2); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds); + Color color = (Color)((NetFieldBase<Color, NetColor>)Game1.currentLightSources.ElementAt<LightSource>(index).color); + double num2 = 0.0; + bounds = Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num3 = (double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) / (double)(Game1.options.lightingQuality / 2); + int num4 = 0; + double num5 = 0.899999976158142; + spriteBatch.Draw(lightTexture, position, sourceRectangle, color, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + } + } + Game1.spriteBatch.End(); + this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); + } + if (Game1.bloomDay && Game1.bloom != null) + Game1.bloom.BeginDraw(); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + this.Events.Graphics_OnPreRenderEvent.Raise(); + if (Game1.background != null) + Game1.background.draw(Game1.spriteBatch); + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.currentLocation.drawWater(Game1.spriteBatch); + if (Game1.CurrentEvent == null) + { + foreach (NPC character in Game1.currentLocation.characters) + { + if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && !character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num2 = 0.0; + bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num3 = (4.0 + (double)character.yJumpOffset / 40.0) * (double)(float)((NetFieldBase<float, NetFloat>)character.scale); + int num4 = 0; + double num5 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + } + } + } + else + { + foreach (NPC actor in Game1.CurrentEvent.actors) + { + if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num2 = 0.0; + bounds = Game1.shadowTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.shadowTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num3 = (4.0 + (double)actor.yJumpOffset / 40.0) * (double)(float)((NetFieldBase<float, NetFloat>)actor.scale); + int num4 = 0; + double num5 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + } + } + } + Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.CurrentEvent == null) + { + foreach (NPC character in Game1.currentLocation.characters) + { + if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + } + } + else + { + foreach (NPC actor in Game1.CurrentEvent.actors) + { + if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + } + } + if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) + 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), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0)); + Game1.currentLocation.draw(Game1.spriteBatch); + if (!Game1.eventUp || Game1.currentLocation.currentEvent == null || Game1.currentLocation.currentEvent.messageToScreen == null) + ; + 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?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.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 warp in (NetList<Warp, NetRef<Warp>>)Game1.currentLocation.warps) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.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, false, 4); + Game1.mapDisplayDevice.EndScene(); + Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawPlayerHeldObject(Game1.player); + else if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways") || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))) + Game1.drawPlayerHeldObject(Game1.player); + if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + { + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); + Game1.mapDisplayDevice.EndScene(); + } + if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + { + Color color = Color.White; + switch ((int)((double)Game1.toolHold / 600.0) + 2) + { + case 1: + color = Tool.copperColor; + break; + case 2: + color = Tool.steelColor; + break; + case 3: + color = Tool.goldColor; + break; + case 4: + color = 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") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 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") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); + } + if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) + { + foreach (WeatherDebris weatherDebris in Game1.debrisWeather) + weatherDebris.draw(Game1.spriteBatch); + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if ((double)Game1.currentLocation.LightLevel > 0.0 && 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 || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)) + Game1.player.CurrentTool.draw(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64))))) + { + for (int index = 0; index < Game1.rainDrops.Length; ++index) + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + } + Game1.spriteBatch.End(); + //base.Draw(gameTime); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + foreach (NPC actor in Game1.currentLocation.currentEvent.actors) + { + if (actor.isEmoting) + { + Vector2 localPosition = actor.getLocalPosition(Game1.viewport); + localPosition.Y -= 140f; + if (actor.Age == 2) + localPosition.Y += 32f; + else if (actor.Gender == 1) + localPosition.Y += 10f; + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); + } + } + } + Game1.spriteBatch.End(); + if (Game1.drawLighting) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); + if (Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)) + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); + Game1.spriteBatch.End(); + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + if (Game1.drawGrid) + { + int num2 = -Game1.viewport.X % 64; + float num3 = (float)(-Game1.viewport.Y % 64); + int num4 = num2; + while (true) + { + int num5 = num4; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + int width1 = viewport1.Width; + if (num5 < width1) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + int x = num4; + int y = (int)num3; + int width2 = 1; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + int height = viewport1.Height; + Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width2, height); + Color color = Color.Red * 0.5f; + spriteBatch.Draw(staminaRect, destinationRectangle, color); + num4 += 64; + } + else + break; + } + float num6 = num3; + while (true) + { + double num5 = (double)num6; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + double height1 = (double)viewport1.Height; + if (num5 < height1) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + int x = num2; + int y = (int)num6; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + int width = viewport1.Width; + int height2 = 1; + Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, height2); + Color color = Color.Red * 0.5f; + spriteBatch.Draw(staminaRect, destinationRectangle, color); + num6 += 64f; + } + else + break; + } + } + if ((uint)Game1.currentBillboard > 0U) + this.drawBillboard(); + if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode) && !Game1.HostPaused) + { + this.Events.Graphics_OnPreRenderHudEvent.Raise(); + this.drawHUD(); + this.Events.Graphics_OnPostRenderHudEvent.Raise(); + } + else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) + Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); + if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) + { + for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) + Game1.hudMessages[i].draw(Game1.spriteBatch, i); + } + } + if (Game1.farmEvent != null) + Game1.farmEvent.draw(Game1.spriteBatch); + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) + this.drawDialogueBox(); + if (Game1.progressBar) + { + SpriteBatch spriteBatch1 = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + int x1 = (viewport1.TitleSafeArea.Width - Game1.dialogueWidth) / 2; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle titleSafeArea = viewport1.TitleSafeArea; + int y1 = titleSafeArea.Bottom - 128; + int dialogueWidth = Game1.dialogueWidth; + int height1 = 32; + Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); + Color lightGray = Color.LightGray; + spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); + SpriteBatch spriteBatch2 = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + int x2 = (viewport1.TitleSafeArea.Width - Game1.dialogueWidth) / 2; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + titleSafeArea = viewport1.TitleSafeArea; + int y2 = titleSafeArea.Bottom - 128; + int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); + int height2 = 32; + Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); + Color dimGray = Color.DimGray; + spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + } + if (Game1.eventUp && (Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)) + Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); + if (Game1.isRaining && (Game1.currentLocation != null && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D staminaRect = Game1.staminaRect; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds; + Color color = Color.Blue * 0.2f; + spriteBatch.Draw(staminaRect, bounds, color); + } + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds; + Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } + else if ((double)Game1.flashAlpha > 0.0) + { + if (Game1.options.screenFlash) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D fadeToBlackRect = Game1.fadeToBlackRect; + viewport1 = Game1.graphics.GraphicsDevice.Viewport; + Microsoft.Xna.Framework.Rectangle bounds = viewport1.Bounds; + Color color = Color.White * Math.Min(1f, Game1.flashAlpha); + spriteBatch.Draw(fadeToBlackRect, bounds, color); + } + Game1.flashAlpha -= 0.1f; + } + if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) + this.drawDialogueBox(); + foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) + overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); + if (Game1.debugMode) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + SpriteFont smallFont = Game1.smallFont; + object[] objArray = new object[10]; + int index = 0; + string str; + if (!Game1.panMode) + str = "player: " + (object)(Game1.player.getStandingX() / 64) + ", " + (object)(Game1.player.getStandingY() / 64); + else + str = ((Game1.getOldMouseX() + Game1.viewport.X) / 64).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / 64); + objArray[index] = (object)str; + objArray[1] = (object)" mouseTransparency: "; + objArray[2] = (object)Game1.mouseCursorTransparency; + objArray[3] = (object)" mousePosition: "; + objArray[4] = (object)Game1.getMouseX(); + objArray[5] = (object)","; + objArray[6] = (object)Game1.getMouseY(); + objArray[7] = (object)Environment.NewLine; + objArray[8] = (object)"debugOutput: "; + objArray[9] = (object)Game1.debugOutput; + string text = string.Concat(objArray); + Viewport viewport2 = this.GraphicsDevice.Viewport; + double x = (double)viewport2.TitleSafeArea.X; + viewport2 = this.GraphicsDevice.Viewport; + double y = (double)viewport2.TitleSafeArea.Y; + Vector2 position = new Vector2((float)x, (float)y); + Color red = Color.Red; + double num2 = 0.0; + Vector2 zero = Vector2.Zero; + double num3 = 1.0; + int num4 = 0; + double num5 = 0.99999988079071; + spriteBatch.DrawString(smallFont, text, position, red, (float)num2, zero, (float)num3, (SpriteEffects)num4, (float)num5); + } + if (Game1.showKeyHelp) + 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, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + if (Game1.activeClickableMenu != null) + { + try + { + this.Events.Graphics_OnPreRenderGuiEvent.Raise(); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + this.Events.Graphics_OnPostRenderGuiEvent.Raise(); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} 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.HostPaused) + { + string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); + SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1); + } + this.RaisePostRender(); + Game1.spriteBatch.End(); + this.drawOverlays(Game1.spriteBatch); + this.renderScreenBuffer(); + } + } + } + } + } +#else private void DrawImpl(GameTime gameTime) { if (Game1.debugMode) @@ -1301,6 +1936,7 @@ namespace StardewModdingAPI.Framework } } } +#endif /**** ** Methods diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs index e78ac14b..7f0122cf 100644 --- a/src/SMAPI/Framework/WebApiClient.cs +++ b/src/SMAPI/Framework/WebApiClient.cs @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework { return this.Post<ModSearchModel, Dictionary<string, ModInfoModel>>( $"v{this.Version}/mods", - new ModSearchModel(modKeys) + new ModSearchModel(modKeys, allowInvalidVersions: true) ); } diff --git a/src/SMAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs index 512bfdab..42bf7d2e 100644 --- a/src/SMAPI/IPrivateField.cs +++ b/src/SMAPI/IPrivateField.cs @@ -1,3 +1,4 @@ +#if !STARDEW_VALLEY_1_3 using System; using System.Reflection; @@ -26,3 +27,4 @@ namespace StardewModdingAPI void SetValue(TValue value); } } +#endif diff --git a/src/SMAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs index b2fdaaeb..c24db602 100644 --- a/src/SMAPI/IPrivateMethod.cs +++ b/src/SMAPI/IPrivateMethod.cs @@ -1,3 +1,4 @@ +#if !STARDEW_VALLEY_1_3 using System; using System.Reflection; @@ -27,3 +28,4 @@ namespace StardewModdingAPI void Invoke(params object[] arguments); } } +#endif diff --git a/src/SMAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs index a24495dd..a1b21a69 100644 --- a/src/SMAPI/IPrivateProperty.cs +++ b/src/SMAPI/IPrivateProperty.cs @@ -1,3 +1,4 @@ +#if !STARDEW_VALLEY_1_3 using System; using System.Reflection; @@ -26,3 +27,4 @@ namespace StardewModdingAPI void SetValue(TValue value); } } +#endif diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs index fcebae42..60441471 100644 --- a/src/SMAPI/IReflectionHelper.cs +++ b/src/SMAPI/IReflectionHelper.cs @@ -48,6 +48,7 @@ namespace StardewModdingAPI /// <param name="required">Whether to throw an exception if the field is not found.</param> IReflectedMethod GetMethod(Type type, string name, bool required = true); +#if !STARDEW_VALLEY_1_3 /***** ** Obsolete *****/ @@ -114,5 +115,6 @@ namespace StardewModdingAPI /// <param name="required">Whether to throw an exception if the private field is not found.</param> [Obsolete("Use " + nameof(IReflectionHelper) + "." + nameof(IReflectionHelper.GetMethod) + " instead")] IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true); +#endif } } diff --git a/src/SMAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs index 5a98da4b..87629682 100644 --- a/src/SMAPI/Metadata/CoreAssets.cs +++ b/src/SMAPI/Metadata/CoreAssets.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Reflection; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; using StardewValley.Locations; +using StardewValley.Menus; using StardewValley.Objects; using StardewValley.Projectiles; using StardewValley.TerrainFeatures; @@ -23,7 +24,7 @@ namespace StardewModdingAPI.Metadata protected readonly Func<string, string> GetNormalisedPath; /// <summary>Setters which update static or singleton texture fields indexed by normalised asset key.</summary> - private readonly IDictionary<string, Action<SContentManager, string>> SingletonSetters; + private readonly IDictionary<string, Action<LocalizedContentManager, string>> SingletonSetters; /********* @@ -31,11 +32,12 @@ namespace StardewModdingAPI.Metadata *********/ /// <summary>Initialise the core asset data.</summary> /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param> - public CoreAssets(Func<string, string> getNormalisedPath) + /// <param name="reflection">Simplifies access to private code.</param> + public CoreAssets(Func<string, string> getNormalisedPath, Reflector reflection) { this.GetNormalisedPath = getNormalisedPath; this.SingletonSetters = - new Dictionary<string, Action<SContentManager, string>> + new Dictionary<string, Action<LocalizedContentManager, string>> { // from CraftingRecipe.InitShared ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key), @@ -82,6 +84,25 @@ namespace StardewModdingAPI.Metadata // from Game1.ResetToolSpriteSheet ["TileSheets\\tools"] = (content, key) => Game1.ResetToolSpriteSheet(), +#if STARDEW_VALLEY_1_3 + // from Bush + ["TileSheets\\bushes"] = (content, key) => reflection.GetField<Lazy<Texture2D>>(typeof(Bush), "texture").SetValue(new Lazy<Texture2D>(() => content.Load<Texture2D>(key))), + + // from Farm + ["Buildings\\houses"] = (content, key) => reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key)), + + // from Farmer + ["Characters\\Farmer\\farmer_base"] = (content, key) => + { + if (Game1.player != null && Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(key); + }, + ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + { + if (Game1.player != null && !Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(key); + }, +#else // from Bush ["TileSheets\\bushes"] = (content, key) => Bush.texture = content.Load<Texture2D>(key), @@ -107,6 +128,7 @@ namespace StardewModdingAPI.Metadata if (Game1.player != null && !Game1.player.isMale) Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key)); }, +#endif // from Flooring ["TerrainFeatures\\Flooring"] = (content, key) => Flooring.floorsTexture = content.Load<Texture2D>(key), @@ -119,6 +141,26 @@ namespace StardewModdingAPI.Metadata ["TerrainFeatures\\hoeDirtDark"] = (content, key) => HoeDirt.darkTexture = content.Load<Texture2D>(key), ["TerrainFeatures\\hoeDirtSnow"] = (content, key) => HoeDirt.snowTexture = content.Load<Texture2D>(key), + // from TitleMenu + ["Minigames\\Clouds"] = (content, key) => + { + if (Game1.activeClickableMenu is TitleMenu) + reflection.GetField<Texture2D>(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load<Texture2D>(key)); + }, + ["Minigames\\TitleButtons"] = (content, key) => + { + if (Game1.activeClickableMenu is TitleMenu titleMenu) + { + reflection.GetField<Texture2D>(titleMenu, "titleButtonsTexture").SetValue(content.Load<Texture2D>(key)); + foreach (TemporaryAnimatedSprite bird in reflection.GetField<List<TemporaryAnimatedSprite>>(titleMenu, "birds").GetValue()) +#if STARDEW_VALLEY_1_3 + bird.texture = content.Load<Texture2D>(key); +#else + bird.Texture = content.Load<Texture2D>(key); +#endif + } + }, + // from Wallpaper ["Maps\\walls_and_floors"] = (content, key) => Wallpaper.wallpaperTexture = content.Load<Texture2D>(key) } @@ -129,10 +171,10 @@ namespace StardewModdingAPI.Metadata /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="key">The asset key to reload.</param> /// <returns>Returns whether an asset was reloaded.</returns> - public bool ReloadForKey(SContentManager content, string key) + public bool ReloadForKey(LocalizedContentManager content, string key) { // static assets - if (this.SingletonSetters.TryGetValue(key, out Action<SContentManager, string> reload)) + if (this.SingletonSetters.TryGetValue(key, out Action<LocalizedContentManager, string> reload)) { reload(content, key); return true; @@ -144,9 +186,15 @@ namespace StardewModdingAPI.Metadata Building[] buildings = this.GetAllBuildings().Where(p => key == this.GetNormalisedPath($"Buildings\\{p.buildingType}")).ToArray(); if (buildings.Any()) { +#if STARDEW_VALLEY_1_3 + foreach (Building building in buildings) + building.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key)); +#else Texture2D texture = content.Load<Texture2D>(key); foreach (Building building in buildings) building.texture = texture; +#endif + return true; } return false; @@ -162,9 +210,11 @@ namespace StardewModdingAPI.Metadata /// <summary>Get all player-constructed buildings in the world.</summary> private IEnumerable<Building> GetAllBuildings() { - return Game1.locations - .OfType<BuildableGameLocation>() - .SelectMany(p => p.buildings); + foreach (BuildableGameLocation location in Game1.locations.OfType<BuildableGameLocation>()) + { + foreach (Building building in location.buildings) + yield return building; + } } } } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 5bb461c1..4960a458 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -6,6 +6,9 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; using StardewValley; +#if STARDEW_VALLEY_1_3 +using SObject = StardewValley.Object; +#endif namespace StardewModdingAPI.Metadata { @@ -13,6 +16,14 @@ namespace StardewModdingAPI.Metadata internal class InstructionMetadata { /********* + ** Properties + *********/ + /// <summary>The assembly names to which to heuristically detect broken references.</summary> + /// <remarks>The current implementation only works correctly with assemblies that should always be present.</remarks> + private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley" }; + + + /********* ** Public methods *********/ /// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary> @@ -21,12 +32,40 @@ namespace StardewModdingAPI.Metadata return new IInstructionHandler[] { /**** - ** throw exception for incompatible code + ** rewrite CIL to fix incompatible code + ****/ + // rewrite for crossplatform compatibility + new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), + +#if !STARDEW_VALLEY_1_3 + // rewrite for Stardew Valley 1.2 + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), + new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), + new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), + + // rewrite for SMAPI 1.9 + new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), +#endif + + // rewrite for SMAPI 2.0 + new VirtualEntryCallRemover(), + + // rewrite for Stardew Valley 1.3 +#if STARDEW_VALLEY_1_3 + new StaticFieldToConstantRewriter<int>(typeof(Game1), "tileSize", Game1.tileSize), +#endif + + /**** + ** detect incompatible code ****/ - // changes in Stardew Valley 1.2 (with no rewriters) + #if !STARDEW_VALLEY_1_3 + // detect changes in Stardew Valley 1.2 new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible), - // APIs removed in SMAPI 1.9 + // detect APIs removed in SMAPI 1.9 new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible), new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible), new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible), @@ -43,7 +82,7 @@ namespace StardewModdingAPI.Metadata new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), - // APIs removed in SMAPI 2.0 + // detect APIs removed in SMAPI 2.0 new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), @@ -65,6 +104,11 @@ namespace StardewModdingAPI.Metadata new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), + #endif + + // detect broken code + new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies), + new ReferenceToMemberWithUnexpectedTypeFinder(this.ValidateReferencesToAssemblies), /**** ** detect code which may impact game stability @@ -74,27 +118,7 @@ namespace StardewModdingAPI.Metadata new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), - new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick), - - /**** - ** rewrite CIL to fix incompatible code - ****/ - // crossplatform - new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), - - // Stardew Valley 1.2 - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), - new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), - new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), - - // SMAPI 1.9 - new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), - - // SMAPI 2.0 - new VirtualEntryCallRemover() // Mod.Entry changed from virtual to abstract in SMAPI 2.0, which breaks the few mods which called base.Entry() + new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick) }; } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 47db8e86..4bd40710 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI private SGame GameInstance; /// <summary>The underlying content manager.</summary> - private SContentManager ContentManager => this.GameInstance.SContentManager; + private ContentCore ContentCore => this.GameInstance.ContentCore; /// <summary>The SMAPI configuration settings.</summary> /// <remarks>This is initialised after the game starts.</remarks> @@ -177,7 +177,7 @@ namespace StardewModdingAPI } if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.", LogLevel.Error); this.PressAnyKeyToExit(); return; } @@ -190,6 +190,8 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // override game + SGame.MonitorDuringInitialisation = this.Monitor; + SGame.ReflectorDuringInitialisation = this.Reflection; this.GameInstance = new SGame(this.Monitor, this.Reflection, this.EventManager, this.InitialiseAfterGameStart); StardewValley.Program.gamePtr = this.GameInstance; @@ -247,6 +249,7 @@ namespace StardewModdingAPI try { this.IsGameRunning = true; + StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window this.GameInstance.Run(); } catch (Exception ex) @@ -285,10 +288,11 @@ namespace StardewModdingAPI // dispose core components this.IsGameRunning = false; - this.LogFile?.Dispose(); this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); this.CancellationTokenSource?.Dispose(); this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); } @@ -388,7 +392,7 @@ namespace StardewModdingAPI mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods - this.LoadMods(mods, this.JsonHelper, this.ContentManager); + this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase); // check for updates this.CheckForUpdatesAsync(mods); @@ -412,8 +416,8 @@ namespace StardewModdingAPI private void OnLocaleChanged() { // get locale - string locale = this.ContentManager.GetLocale(); - LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); + string locale = this.ContentCore.GetLocale(); + LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; // update mod translation helpers foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) @@ -604,29 +608,30 @@ namespace StardewModdingAPI foreach (var result in results) { IModMetadata mod = result.Mod; - ModInfoModel info = result.Info; + ModInfoModel remoteInfo = result.Info; // handle error - if (info.Error != null) + if (remoteInfo.Error != null) { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {remoteInfo.Error}", LogLevel.Trace); continue; } - // track update - ISemanticVersion localVersion = mod.DataRecord != null - ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) - : mod.Manifest.Version; - ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null - ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) - : info.Version - ); - bool isUpdate = latestVersion.IsNewerThan(localVersion); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "okay")}."); + // normalise versions + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + if (!SemanticVersion.TryParse(mod.DataRecord?.GetRemoteVersionForUpdateChecks(remoteInfo.Version) ?? remoteInfo.Version, out ISemanticVersion remoteVersion)) + { + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: Mod has invalid version {remoteInfo.Version}", LogLevel.Trace); + continue; + } + + // compare versions + bool isUpdate = remoteVersion.IsNewerThan(localVersion); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {remoteInfo.Version}{(!remoteVersion.Equals(new SemanticVersion(remoteInfo.Version)) ? $" [{remoteVersion}]" : "")}" : "okay")}."); if (isUpdate) { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) - updatesByMod[mod] = info; + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || remoteVersion.IsNewerThan(other.Version)) + updatesByMod[mod] = remoteInfo; } } @@ -669,13 +674,14 @@ namespace StardewModdingAPI /// <summary>Load and hook up the given mods.</summary> /// <param name="mods">The mods to load.</param> /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param> - /// <param name="contentManager">The content manager to use for mod content.</param> - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) + /// <param name="contentCore">The content manager to use for mod content.</param> + /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCore contentCore, ModDatabase modDatabase) { this.Monitor.Log("Loading mods...", LogLevel.Trace); - IDictionary<IModMetadata, string> skippedMods = new Dictionary<IModMetadata, string>(); - void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; + IDictionary<IModMetadata, string[]> skippedMods = new Dictionary<IModMetadata, string[]>(); + void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase }; // load content packs foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack)) @@ -694,7 +700,8 @@ namespace StardewModdingAPI // load mod as content pack IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); metadata.SetMod(contentPack, monitor); this.ModRegistry.Add(metadata); @@ -744,19 +751,21 @@ namespace StardewModdingAPI { modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); } - catch (IncompatibleInstructionException ex) + catch (IncompatibleInstructionException) // details already in trace logs { - TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); + string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray(); + + TrackSkip(metadata, $"it's outdated. Please check for a new version at {string.Join(" or ", updateUrls)}."); continue; } catch (SAssemblyLoadFailedException ex) { - TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}"); + TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}"); continue; } catch (Exception ex) { - TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); + TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}"); continue; } @@ -772,15 +781,17 @@ namespace StardewModdingAPI IModHelper modHelper; { ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + ContentManagerShim packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", packDirPath); + IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } @@ -816,12 +827,11 @@ namespace StardewModdingAPI foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; - string reason = pair.Value; + string[] reason = pair.Value; - if (mod.Manifest?.Version != null) - this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); - else - this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); + this.Monitor.Log($" {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error); + if (reason[1] != null) + this.Monitor.Log($" {reason[1]}", LogLevel.Trace); } this.Monitor.Newline(); } @@ -844,7 +854,7 @@ namespace StardewModdingAPI // log loaded content packs if (loadedContentPacks.Any()) { - string GetModDisplayName(string id) => loadedMods.First(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; + string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => id != null && id.Equals(p.Manifest?.UniqueID, StringComparison.InvariantCultureIgnoreCase))?.DisplayName; this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info); foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) @@ -877,8 +887,8 @@ namespace StardewModdingAPI helper.ObservableAssetLoaders.Add(loader); // ReSharper restore SuspiciousTypeConversion.Global - this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; + this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; + this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; } // call entry method @@ -923,7 +933,7 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]); + this.ContentCore.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => @@ -931,7 +941,7 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray()); + this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray()); } }; } @@ -943,7 +953,7 @@ namespace StardewModdingAPI if (editors.Any() || loaders.Any()) { this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(editors, loaders); + this.ContentCore.InvalidateCacheFor(editors, loaders); } // unlock mod integrations diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index c7527275..5b6bae12 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -97,15 +97,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "AccessChestAnywhere", "MapLocalVersions": { "1.1-1078": "1.1" }, "Default | UpdateKey": "Nexus:257", - "~1.1 | Status": "AssumeBroken", - "~1.1 | AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1 | Status": "AssumeBroken" }, "AdjustArtisanPrices": { - "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", + "ID": "ThatNorthernMonkey.AdjustArtisanPrices", + "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update + "MapRemoteVersions": { "0.01": "0.0.1" }, "Default | UpdateKey": "Chucklefish:3532", - "~0.1 | Status": "AssumeBroken", - "~0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.0.1 | Status": "AssumeBroken" }, "Adjust Monster": { @@ -127,8 +127,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AgingMod": { "ID": "skn.AgingMod", "Default | UpdateKey": "Nexus:1129", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "All Crops All Seasons": { @@ -170,16 +169,19 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "Ashley Mod": { + "FormerIDs": "{EntryDll: 'AshleyMod.dll'}", + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 + }, + "A Tapper's Dream": { "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", "Default | UpdateKey": "Nexus:260", - "~1.4 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.4 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Auto Animal Doors": { "ID": "AaronTaggart.AutoAnimalDoors", - "MapRemoteVersions": { "1.1.1": "1.1" }, // manifest not updated "Default | UpdateKey": "Nexus:1019" }, @@ -189,6 +191,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:643" }, + "AutoFish": { + "ID": "WhiteMind.AF", + "Default | UpdateKey": "Nexus:1895" + }, + "AutoGate": { "ID": "AutoGate", "Default | UpdateKey": "Nexus:820" @@ -202,8 +209,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Automated Doors": { "ID": "azah.automated-doors", "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1 - "MapLocalVersions": { "1.4.1-1": "1.4.1" }, - "Default | UpdateKey": "GitHub:azah/AutomatedDoors" + "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2 }, "AutoSpeed": { @@ -242,16 +248,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Kithio:BetterShippingBox", "MapLocalVersions": { "1.0.1": "1.0.2" }, "Default | UpdateKey": "Chucklefish:4302", - "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Better Sprinklers": { "ID": "Speeder.BetterSprinklers", "FormerIDs": "SPDSprinklersMod", // changed in 2.3 "Default | UpdateKey": "Nexus:41", - "~2.3.1-pathoschild-update | Status": "AssumeBroken", // broke in SDV 1.2 - "~2.3.1-pathoschild-update | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~2.3.1-pathoschild-update | Status": "AssumeBroken" // broke in SDV 1.2 }, "Billboard Anywhere": { @@ -264,8 +268,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "KathrynHazuka.BirthdayMail", "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update "Default | UpdateKey": "Nexus:276", - "~1.2.2 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.2.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Breed Like Rabbits": { @@ -326,8 +329,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Speeder.ChestLabel", "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update "Default | UpdateKey": "Nexus:242", - "~1.6 | Status": "AssumeBroken", // broke in SDV 1.1 - "~1.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.6 | Status": "AssumeBroken" // broke in SDV 1.1 }, "Chest Pooling": { @@ -345,10 +347,9 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Choose Baby Gender": { - "ID": "{EntryDll: 'ChooseBabyGender.dll'}", + "FormerIDs": "{EntryDll: 'ChooseBabyGender.dll'}", "Default | UpdateKey": "Nexus:590", - "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "CJB Automation": { @@ -390,11 +391,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "Coal Regen": { + "ID": "Blucifer.CoalRegen", + "Default | UpdateKey": "Nexus:1664" + }, + "Cold Weather Haley": { "ID": "LordXamon.ColdWeatherHaleyPRO", "Default | UpdateKey": "Nexus:1169", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Colored Chests": { @@ -406,8 +411,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Combat with Farm Implements": { "ID": "SPDFarmingImplementsInCombat", "Default | UpdateKey": "Nexus:313", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Community Bundle Item Tooltip": { @@ -429,8 +433,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Configurable Shipping Dates": { "ID": "ConfigurableShippingDates", "Default | UpdateKey": "Nexus:675", - "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Cooking Skill": { @@ -447,11 +450,21 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "Crafting Counter": { + "ID": "lolpcgaming.CraftingCounter", + "Default | UpdateKey": "Nexus:1585" + }, + "Current Location": { "ID": "CurrentLocation102120161203", "Default | UpdateKey": "Nexus:638" }, + "Custom Asset Modifier": { + "ID": "Omegasis.CustomAssetModifier", + "Default | UpdateKey": "1836" + }, + "Custom Critters": { "ID": "spacechase0.CustomCritters", "Default | UpdateKey": "Nexus:1255" @@ -495,7 +508,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, - "Customizable Cart Redux": { "ID": "KoihimeNakamura.CCR", "MapLocalVersions": { "1.1-20170917": "1.1" }, @@ -505,8 +517,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Customizable Traveling Cart Days": { "ID": "TravelingCartYyeahdude", "Default | UpdateKey": "Nexus:567", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Custom Linens": { @@ -553,11 +564,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:679" }, + "Did You Water Your Crops?": { + "ID": "Nishtra.DidYouWaterYourCrops", + "Default | UpdateKey": "Nexus:1583" + }, + "Dynamic Checklist": { "ID": "gunnargolf.DynamicChecklist", "Default | UpdateKey": "Nexus:1145", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Dynamic Horses": { @@ -570,8 +585,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "DynamicMachines", "MapLocalVersions": { "1.1": "1.1.1" }, "Default | UpdateKey": "Nexus:374", - "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Dynamic NPC Sprites": { @@ -587,16 +601,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Empty Hands": { "ID": "QuicksilverFox.EmptyHands", "Default | UpdateKey": "Nexus:1176", // added in 1.0.1-pathoschild-update - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Enemy Health Bars": { "ID": "Speeder.HealthBars", "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update "Default | UpdateKey": "Nexus:193", - "~1.7 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.7 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Entoarox Framework": { @@ -626,15 +638,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Crystalmir.ExtendedFridge", "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1 "Default | UpdateKey": "Nexus:485", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Extended Greenhouse": { "ID": "ExtendedGreenhouse", "Default | UpdateKey": "Chucklefish:4303", - "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Extended Minecart": { @@ -656,36 +666,31 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Farm Automation: Barn Door Automation": { - "ID": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'FarmAutomation.BarnDoorAutomation.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Farm Automation: Item Collector": { - "ID": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'FarmAutomation.ItemCollector.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Farm Automation Unofficial: Item Collector": { "ID": "Maddy99.FarmAutomation.ItemCollector", - "~0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Farm Expansion": { "ID": "Advize.FarmExpansion", "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0 "Default | UpdateKey": "Nexus:130", - "~2.0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~2.0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~2.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Farm Resource Generator": { - "ID": "{EntryDll: 'FarmResourceGenerator.dll'}", + "FormerIDs": "{EntryDll: 'FarmResourceGenerator.dll'}", "Default | UpdateKey": "Nexus:647", - "~1.0.4 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.4 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.4 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Fast Animations": { @@ -693,6 +698,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1089" }, + "Faster Grass": { + "ID": "IceGladiador.FasterGrass", + "Default | UpdateKey": "Nexus:1772" + }, + "Faster Paths": { "ID": "Entoarox.FasterPaths", "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.2 and 1.3; disambiguate from Shop Expander @@ -703,8 +713,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "KathrynHazuka.FasterRun", "FormerIDs": "{EntryDll: 'FasterRun.dll'}", // changed in 1.1.1-pathoschild-update "Default | UpdateKey": "Nexus:733", // added in 1.1.1-pathoschild-update - "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Fishing Adjust": { @@ -717,12 +726,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Chucklefish:4578" }, + "Fixed Secret Woods Debris": { + "ID": "f4iTh.WoodsDebrisFix", + "Default | UpdateKey": "Nexus:1941" + }, + "FlorenceMod": { - "ID": "{EntryDll: 'FlorenceMod.dll'}", + "FormerIDs": "{EntryDll: 'FlorenceMod.dll'}", "MapLocalVersions": { "1.0.1": "1.1" }, "Default | UpdateKey": "Nexus:591", - "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Flower Color Picker": { @@ -731,10 +744,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Forage at the Farm": { - "ID": "ForageAtTheFarm", + "ID": "Nishtra.ForageAtTheFarm", + "FormerIDs": "ForageAtTheFarm", // changed in <=1.6 "Default | UpdateKey": "Nexus:673", - "~1.5.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.5.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Furniture Anywhere": { @@ -792,8 +805,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Happy Animals": { "ID": "HappyAnimals", - "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Happy Birthday (Omegasis)": { @@ -804,8 +816,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Happy Birthday (Oxyligen fork)": { - "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "Default | UpdateKey": "Nexus:1064" + "FormerIDs": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork + "Default | UpdateKey": "Nexus:1064" // missing key reported: https://www.nexusmods.com/stardewvalley/mods/1064?tab=bugs + }, + + "Hardcore Mines": { + "ID": "kibbe.hardcore_mines", + "Default | UpdateKey": "Nexus:1674" }, "Harp of Yoba Redux": { @@ -821,8 +838,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Harvest With Scythe": { "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", "Default | UpdateKey": "Nexus:236", - "~1.0.6 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Horse Whistle (icepuente)": { @@ -838,8 +854,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Hunger for Food (Tigerle)": { "ID": "HungerForFoodByTigerle", "Default | UpdateKey": "Nexus:810", - "~0.1.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~0.1.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Hunger Mod (skn)": { @@ -862,8 +877,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Instant Geode": { "ID": "InstantGeode", - "~1.12 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Instant Grow Trees": { @@ -875,8 +889,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Interaction Helper": { "ID": "HammurabiInteractionHelper", "Default | UpdateKey": "Chucklefish:4640", // added in 1.0.4-pathoschild-update - "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Item Auto Stacker": { @@ -906,8 +919,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "BALANCEMOD_AntiExhaustion", "MapLocalVersions": { "0.0": "1.1" }, "Default | UpdateKey": "Nexus:637", - "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Level Extender": { @@ -918,6 +930,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Level Up Notifications": { "ID": "Level Up Notifications", + "MapRemoteVersions": { "0.0.1a": "0.0.1" }, "Default | UpdateKey": "Nexus:855" }, @@ -928,6 +941,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Longevity": { "ID": "RTGOAT.Longevity", + "MapRemoteVersions": { "1.6.8h": "1.6.8" }, "Default | UpdateKey": "Nexus:649" }, @@ -985,17 +999,18 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Message Box [API]? (ChatMod)": { "ID": "Kithio:ChatMod", "Default | UpdateKey": "Chucklefish:4296", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Mining at the Farm": { - "ID": "MiningAtTheFarm", + "ID": "Nishtra.MiningAtTheFarm", + "FormerIDs": "MiningAtTheFarm", // changed in <=1.7 "Default | UpdateKey": "Nexus:674" }, "Mining With Explosives": { - "ID": "MiningWithExplosives", + "ID": "Nishtra.MiningWithExplosives", + "FormerIDs": "MiningWithExplosives", // changed in 1.1 "Default | UpdateKey": "Nexus:770" }, @@ -1005,6 +1020,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~ | StatusReasonPhrase": "it's no longer maintained or used." }, + "Monster Level Tip": { + "ID": "WhiteMind.MonsterLT", + "Default | UpdateKey": "Nexus:1896" + }, + "More Animals": { "ID": "Entoarox.MoreAnimals", "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0 @@ -1015,8 +1035,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "More Artifact Spots": { "ID": "451", "Default | UpdateKey": "Nexus:451", - "~1.0.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "More Map Layers": { @@ -1045,8 +1064,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "FileLoading", "MapLocalVersions": { "1.1": "1.12" }, "Default | UpdateKey": "Nexus:1094", - "~1.12 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.12 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Museum Rearranger": { @@ -1056,11 +1074,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "Mushroom Level Tip": { + "ID": "WhiteMind.MLT", + "Default | UpdateKey": "Nexus:1894" + }, + "New Machines": { "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", "Default | UpdateKey": "Chucklefish:3683", - "~4.2.1343 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~4.2.1343 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Night Owl": { @@ -1071,6 +1093,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, + "No Crows": { + "ID": "cat.nocrows", + "Default | UpdateKey": "Nexus:1682" + }, + "No Kids Ever": { "ID": "Hangy.NoKidsEver", "Default | UpdateKey": "Nexus:1464" @@ -1093,11 +1120,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:506" // added in 1.4.1 }, - "NoSoilDecay": { + "No Rumble Horse": { + "ID": "Xangria.NoRumbleHorse", + "Default | UpdateKey": "Nexus:1779" + }, + + "No Soil Decay": { "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", "Default | UpdateKey": "Nexus:237", - "~0.5 | Status": "AssumeBroken", // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location - "~0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location }, "No Soil Decay Redux": { @@ -1113,10 +1144,9 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "NPC Speak": { - "ID": "{EntryDll: 'NpcEcho.dll'}", + "FormerIDs": "{EntryDll: 'NpcEcho.dll'}", "Default | UpdateKey": "Nexus:694", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Object Time Left": { @@ -1153,8 +1183,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "PelicanTTS": { "ID": "Platonymous.PelicanTTS", "Default | UpdateKey": "Nexus:1079", // added in 1.6.1 - "~1.6 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.6 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.6 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Persia the Mermaid - Standalone Custom NPC": { @@ -1162,11 +1191,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1419" }, + "Persistent Game Options": { + "ID": "Xangria.PersistentGameOptions", + "Default | UpdateKey": "Nexus:1778" + }, + "Persival's BundleMod": { - "ID": "{EntryDll: 'BundleMod.dll'}", + "FormerIDs": "{EntryDll: 'BundleMod.dll'}", "Default | UpdateKey": "Nexus:438", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 }, "Plant on Grass": { @@ -1200,8 +1233,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Mucchan.PrairieKingMadeEasy", "FormerIDs": "{EntryDll: 'PrairieKingMadeEasy.dll'}", // changed in 1.0.1 "Default | UpdateKey": "Chucklefish:3594", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 + }, + + "Purchasable Recipes": { + "ID": "Paracosm.PurchasableRecipes", + "Default | UpdateKey": "Nexus:1722" }, "Quest Delay": { @@ -1210,9 +1247,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Rain Randomizer": { - "ID": "{EntryDll: 'RainRandomizer.dll'}", - "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'RainRandomizer.dll'}", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Recatch Legendary Fish": { @@ -1234,16 +1270,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "RelationshipsEnhanced": { "ID": "relationshipsenhanced", "Default | UpdateKey": "Chucklefish:4435", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Relationship Status": { "ID": "relationshipstatus", "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest "Default | UpdateKey": "Nexus:751", - "~1.0.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Rented Tools": { @@ -1272,8 +1306,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Reusable Wallpapers and Floors (Wallpaper Retain)": { "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", "Default | UpdateKey": "Nexus:356", - "~1.5 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.5 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Ring of Fire": { @@ -1281,6 +1314,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1166" // added in 1.0.1 }, + "Rise and Shine": { + "ID": "Yoshify.RiseAndShine", + "FormerIDs": "{EntryDll: 'RiseAndShine.dll'}", // changed in 1.1.1-whisk-update + "Default | UpdateKey": "Nexus:3" + }, + "Rope Bridge": { "ID": "RopeBridge", "Default | UpdateKey": "Nexus:824" @@ -1323,6 +1362,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1106" }, + "SDV Twitch": { + "ID": "MTD.SDVTwitch", + "Default | UpdateKey": "Nexus:1760" + }, + "Seasonal Immersion": { "ID": "Entoarox.SeasonalImmersion", "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 @@ -1335,6 +1379,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1133" // added in 1.1.2 }, + "Seed Catalogue": { + "ID": "spacechase0.SeedCatalogue", + "Default | UpdateKey": "Nexus:1640" + }, + "Self Service": { "ID": "JarvieK.SelfService", "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated @@ -1350,16 +1399,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Shed Notifications (BuildingsNotifications)": { "ID": "TheCroak.BuildingsNotifications", "Default | UpdateKey": "Nexus:620", - "~0.4.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~0.4.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Shenandoah Project": { - "ID": "Shenandoah Project", + "ID": "Nishtra.ShenandoahProject", + "FormerIDs": "Shenandoah Project", // changed in 1.2 "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest "Default | UpdateKey": "Nexus:756", - "~1.1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Ship Anywhere": { @@ -1370,14 +1418,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Shipment Tracker": { "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", "Default | UpdateKey": "Nexus:321", - "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Shop Expander": { "ID": "Entoarox.ShopExpander", "FormerIDs": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - "~1.5.3 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.5.3 by request (has its own update-check feature) + "MapRemoteVersions": { "1.6.0b": "1.6.0" }, + "~1.6 | UpdateKey": "Chucklefish:4381", // only enable update checks up to 1.6 by request (has its own update-check feature) "~1.5.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, @@ -1385,8 +1433,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Igorious.Showcase", "MapLocalVersions": { "0.9-500": "0.9" }, "Default | UpdateKey": "Chucklefish:4487", - "~0.9 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~0.9 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Shroom Spotter": { @@ -1416,8 +1463,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "6266959802", "MapLocalVersions": { "0.0": "1.4" }, "Default | UpdateKey": "Nexus:366", - "~1.2.2 | Status": "AssumeBroken", // broke in SMAPI 1.9 (has multiple Mod instances) - "~1.2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.2.2 | Status": "AssumeBroken" // broke in SMAPI 1.9 (has multiple Mod instances) }, "Skill Prestige": { @@ -1461,14 +1507,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Speeder.SlowerFenceDecay", "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update "Default | UpdateKey": "Nexus:252", - "~0.5.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~0.5.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Smart Mod": { "ID": "KuroBear.SmartMod", - "~2.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~2.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Solar Eclipse Event": { @@ -1490,22 +1534,19 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Sprinkler Range": { "ID": "cat.sprinklerrange", - "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated "Default | UpdateKey": "Nexus:1179" }, "Sprinkles": { "ID": "Platonymous.Sprinkles", "Default | UpdateKey": "Chucklefish:4592", - "~1.1.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Sprint and Dash": { "ID": "SPDSprintAndDash", "Default | UpdateKey": "Chucklefish:3531", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Sprint and Dash Redux": { @@ -1515,11 +1556,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Sprinting Mod": { - "ID": "a10d3097-b073-4185-98ba-76b586cba00c", + "FormerIDs": "{EntryDll: 'SprintingMod.dll'}", "MapLocalVersions": { "1.0": "2.1" }, // not updated in manifest "Default | UpdateKey": "GitHub:oliverpl/SprintingMod", - "~2.1 | Status": "AssumeBroken", // broke in SDV 1.2 - "~2.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~2.1 | Status": "AssumeBroken" // broke in SDV 1.2 }, "StackSplitX": { @@ -1530,9 +1570,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "StaminaRegen": { - "ID": "{EntryDll: 'StaminaRegen.dll'}", - "~1.0.3 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.3 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'StaminaRegen.dll'}", + "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Stardew Config Menu": { @@ -1554,8 +1593,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Stardew Notification": { "ID": "stardewnotification", "Default | UpdateKey": "GitHub:monopandora/StardewNotification", - "~1.7 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.7 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.7 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Stardew Symphony": { @@ -1580,8 +1618,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "StashItemsToChest": { "ID": "BlueMod_StashItemsToChest", "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest", - "~1.0.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Stephan's Lots of Crops": { @@ -1591,7 +1628,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Stone Bridge Over Pond (PondWithBridge)": { - "ID": "{EntryDll: 'PondWithBridge.dll'}", + "FormerIDs": "{EntryDll: 'PondWithBridge.dll'}", "MapLocalVersions": { "0.0": "1.0" }, "Default | UpdateKey": "Nexus:316", "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 @@ -1605,8 +1642,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Super Greenhouse Warp Modifier": { "ID": "SuperGreenhouse", "Default | UpdateKey": "Chucklefish:4334", - "~1.0 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Swim Almost Anywhere / Swim Suit": { @@ -1615,9 +1651,9 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Tainted Cellar": { - "ID": "{EntryDll: 'TaintedCellar.dll'}", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "ID": "TaintedCellar", + "FormerIDs": "{EntryDll: 'TaintedCellar.dll'}", // changed in 1.1 + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 }, "Tapper Ready": { @@ -1633,8 +1669,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Teleporter": { "ID": "Teleporter", "Default | UpdateKey": "Chucklefish:4374", - "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, "The Long Night": { @@ -1667,6 +1702,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~2.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, + "To Do List": { + "ID": "eleanor.todolist", + "Default | UpdateKey": "Nexus:1630" + }, + + "Tool Charging": { + "ID": "mralbobo.ToolCharging", + "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging" + }, + "TractorMod": { "ID": "Pathoschild.TractorMod", "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 @@ -1693,8 +1738,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "Demiacle.UiModSuite", "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest "Default | UpdateKey": "Nexus:1023", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2 }, "Variable Grass": { @@ -1708,16 +1752,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "WakeUp": { - "ID": "{EntryDll: 'WakeUp.dll'}", - "~1.0.2 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'WakeUp.dll'}", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "Wallpaper Fix": { - "ID": "{EntryDll: 'WallpaperFix.dll'}", + "FormerIDs": "{EntryDll: 'WallpaperFix.dll'}", "Default | UpdateKey": "Chucklefish:4211", - "~1.1 | Status": "AssumeBroken", // broke in SMAPI 2.0 - "~1.1 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0 }, "WarpAnimals": { @@ -1726,9 +1768,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, "Weather Controller": { - "ID": "{EntryDll: 'WeatherController.dll'}", - "~1.0.2 | Status": "AssumeBroken", // broke in SDV 1.2 - "~1.0.2 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'WeatherController.dll'}", + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2 }, "What Farm Cave / WhatAMush": { @@ -1741,14 +1782,18 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Default | UpdateKey": "Nexus:1082" }, + "Winter Grass": { + "ID": "cat.wintergrass", + "Default | UpdateKey": "Nexus:1601" + }, + "Wonderful Farm Life": { - "ID": "{EntryDll: 'WonderfulFarmLife.dll'}", - "~1.0 | Status": "AssumeBroken", // broke in SDV 1.1 or 1.11 - "~1.0 | AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0" + "FormerIDs": "{EntryDll: 'WonderfulFarmLife.dll'}", + "~1.0 | Status": "AssumeBroken" // broke in SDV 1.1 or 1.11 }, "XmlSerializerRetool": { - "ID": "{EntryDll: 'XmlSerializerRetool.dll'}", + "FormerIDs": "{EntryDll: 'XmlSerializerRetool.dll'}", "~ | Status": "Obsolete", "~ | StatusReasonPhrase": "it's no longer maintained or used." }, diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 8ef3022f..bffb96e2 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -103,6 +103,7 @@ <Compile Include="Framework\ModLoading\Finders\FieldFinder.cs" /> <Compile Include="Framework\ModLoading\Finders\MethodFinder.cs" /> <Compile Include="Framework\ModLoading\Finders\PropertyFinder.cs" /> + <Compile Include="Framework\ModLoading\Finders\ReferenceToMissingMemberFinder.cs" /> <Compile Include="Framework\ModLoading\Finders\TypeFinder.cs" /> <Compile Include="Framework\ModLoading\IInstructionHandler.cs" /> <Compile Include="Framework\ModLoading\IncompatibleInstructionException.cs" /> @@ -111,7 +112,9 @@ <Compile Include="Framework\ModLoading\PlatformAssemblyMap.cs" /> <Compile Include="Framework\ModLoading\RewriteHelper.cs" /> <Compile Include="Framework\ModLoading\Rewriters\FieldReplaceRewriter.cs" /> + <Compile Include="Framework\ModLoading\Rewriters\StaticFieldToConstantRewriter.cs" /> <Compile Include="Framework\ModLoading\Rewriters\FieldToPropertyRewriter.cs" /> + <Compile Include="Framework\ModLoading\Finders\ReferenceToMemberWithUnexpectedTypeFinder.cs" /> <Compile Include="Framework\ModLoading\Rewriters\VirtualEntryCallRemover.cs" /> <Compile Include="Framework\ModLoading\Rewriters\MethodParentRewriter.cs" /> <Compile Include="Framework\ModLoading\Rewriters\TypeReferenceRewriter.cs" /> @@ -193,7 +196,7 @@ <Compile Include="Framework\ModLoading\ModMetadata.cs" /> <Compile Include="Framework\Reflection\ReflectedProperty.cs" /> <Compile Include="Framework\RequestExitDelegate.cs" /> - <Compile Include="Framework\SContentManager.cs" /> + <Compile Include="Framework\ContentCore.cs" /> <Compile Include="Framework\Exceptions\SParseException.cs" /> <Compile Include="Framework\Serialisation\JsonHelper.cs" /> <Compile Include="Framework\Serialisation\SmapiConverters\StringEnumConverter.cs" /> |