From d8bbf3c4412514e20abea6181adaa567a82db5a2 Mon Sep 17 00:00:00 2001 From: ChulkyBow <83290351+ChulkyBow@users.noreply.github.com> Date: Tue, 18 Jan 2022 16:25:36 +0200 Subject: Update Ukrainian translation for SMAPI --- src/SMAPI/i18n/uk.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI/i18n/uk.json b/src/SMAPI/i18n/uk.json index d84aabcf..a4286363 100644 --- a/src/SMAPI/i18n/uk.json +++ b/src/SMAPI/i18n/uk.json @@ -1,6 +1,6 @@ { // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) - "generic.date": "{{season}} День {{day}}", - "generic.date-with-year": "{{season}} День {{day}}, Рік {{year}}" + "generic.date": "{{season}}, {{day}}-й день", + "generic.date-with-year": "{{season}}, {{day}}-й день, {{year}} рік" } -- cgit From 53ed5f4faaffad212a753b33db2106470f48b6b5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Jan 2022 21:55:30 -0500 Subject: update release notes --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 05ab58a3..f45df4bd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,9 @@ ← [README](README.md) # Release notes +## Upcoming release +* Improved translations. Thanks to ChulkyBow (updated Ukrainian)! + ## 3.13.4 Released 16 January 2022 for Stardew Valley 1.5.6 or later. -- cgit From bca9e599cc02edcc6e9bcfba2138d916ae6a3a79 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Jan 2022 17:27:42 -0500 Subject: remove unneeded dictionary patch The dictionary errors were improved in .NET 5, so they include the key automatically. --- src/SMAPI.Mods.ErrorHandler/ModEntry.cs | 1 - .../Patches/DictionaryPatcher.cs | 98 ---------------------- 2 files changed, 99 deletions(-) delete mode 100644 src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs index 067f6a8d..7286e316 100644 --- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs +++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs @@ -30,7 +30,6 @@ namespace StardewModdingAPI.Mods.ErrorHandler // apply patches HarmonyPatcher.Apply(this.ModManifest.UniqueID, this.Monitor, new DialoguePatcher(monitorForGame, this.Helper.Reflection), - new DictionaryPatcher(this.Helper.Reflection), new EventPatcher(monitorForGame), new GameLocationPatcher(monitorForGame), new IClickableMenuPatcher(), diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs deleted file mode 100644 index 8ceafcc5..00000000 --- a/src/SMAPI.Mods.ErrorHandler/Patches/DictionaryPatcher.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Internal.Patching; -using StardewValley.GameData; -using StardewValley.GameData.HomeRenovations; -using StardewValley.GameData.Movies; - -namespace StardewModdingAPI.Mods.ErrorHandler.Patches -{ - /// Harmony patches for which add the accessed key to exceptions. - /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] - internal class DictionaryPatcher : BasePatcher - { - /********* - ** Fields - *********/ - /// Simplifies access to private code. - private static IReflectionHelper Reflection; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Simplifies access to private code. - public DictionaryPatcher(IReflectionHelper reflector) - { - DictionaryPatcher.Reflection = reflector; - } - - /// - public override void Apply(Harmony harmony, IMonitor monitor) - { - Type[] keyTypes = { typeof(int), typeof(string) }; - Type[] valueTypes = { typeof(int), typeof(string), typeof(HomeRenovation), typeof(MovieData), typeof(SpecialOrderData) }; - - foreach (Type keyType in keyTypes) - { - foreach (Type valueType in valueTypes) - { - Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); - - harmony.Patch( - original: AccessTools.Method(dictionaryType, "get_Item") ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(dictionaryType, "get_Item")} to patch."), - finalizer: this.GetHarmonyMethod(nameof(DictionaryPatcher.Finalize_GetItem)) - ); - - harmony.Patch( - original: AccessTools.Method(dictionaryType, "Add") ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(dictionaryType, "Add")} to patch."), - finalizer: this.GetHarmonyMethod(nameof(DictionaryPatcher.Finalize_Add)) - ); - } - } - } - - - /********* - ** Private methods - *********/ - /// The method to call after the dictionary indexer throws an exception. - /// The dictionary key being fetched. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception Finalize_GetItem(object key, Exception __exception) - { - if (__exception is KeyNotFoundException) - DictionaryPatcher.AddKey(__exception, key); - - return __exception; - } - - /// The method to call after a dictionary insert throws an exception. - /// The dictionary key being inserted. - /// The exception thrown by the wrapped method, if any. - /// Returns the exception to throw, if any. - private static Exception Finalize_Add(object key, Exception __exception) - { - if (__exception is ArgumentException) - DictionaryPatcher.AddKey(__exception, key); - - return __exception; - } - - /// Add the dictionary key to an exception message. - /// The exception whose message to edit. - /// The dictionary key. - private static void AddKey(Exception exception, object key) - { - DictionaryPatcher.Reflection - .GetField(exception, "_message") - .SetValue($"{exception.Message}\nkey: '{key}'"); - } - } -} -- cgit From 6b9c9be2b6f08f46077f8d81ef05e9d249d72935 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Jan 2022 17:33:12 -0500 Subject: move item repo secret note + flavored object logic into methods --- .../Framework/ItemRepository.cs | 248 +++++++++++---------- 1 file changed, 133 insertions(+), 115 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 0357fe6b..4a57ba5a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -139,15 +139,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework { if (ShouldGet(ItemType.Object)) { - foreach (int secretNoteId in this.TryLoad("Data\\SecretNotes").Keys) - { - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ => - { - SObject note = new SObject(79, 1); - note.name = $"{note.name} #{secretNoteId}"; - return note; - }); - } + foreach (SearchableItem secretNote in this.GetSecretNotes()) + yield return secretNote; } } @@ -176,112 +169,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // flavored items if (includeVariants) { - switch (item.Category) - { - // fruit products - case SObject.FruitsCategory: - // wine - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1) - { - Name = $"{item.Name} Wine", - Price = item.Price * 3, - preserve = { SObject.PreserveType.Wine }, - preservedParentSheetIndex = { item.ParentSheetIndex } - }); - - // jelly - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + item.ParentSheetIndex, _ => new SObject(344, 1) - { - Name = $"{item.Name} Jelly", - Price = 50 + item.Price * 2, - preserve = { SObject.PreserveType.Jelly }, - preservedParentSheetIndex = { item.ParentSheetIndex } - }); - break; - - // vegetable products - case SObject.VegetableCategory: - // juice - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + item.ParentSheetIndex, _ => new SObject(350, 1) - { - Name = $"{item.Name} Juice", - Price = (int)(item.Price * 2.25d), - preserve = { SObject.PreserveType.Juice }, - preservedParentSheetIndex = { item.ParentSheetIndex } - }); - - // pickled - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ => new SObject(342, 1) - { - Name = $"Pickled {item.Name}", - Price = 50 + item.Price * 2, - preserve = { SObject.PreserveType.Pickle }, - preservedParentSheetIndex = { item.ParentSheetIndex } - }); - break; - - // flower honey - case SObject.flowersCategory: - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ => - { - SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false) - { - Name = $"{item.Name} Honey", - preservedParentSheetIndex = { item.ParentSheetIndex } - }; - honey.Price += item.Price * 2; - return honey; - }); - break; - - // roe and aged roe (derived from FishPond.GetFishProduce) - case SObject.sellAtFishShopCategory when item.ParentSheetIndex == 812: - { - this.GetRoeContextTagLookups(out HashSet simpleTags, out List> complexTags); - - foreach (var pair in Game1.objectInformation) - { - // get input - SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject; - var inputTags = input?.GetContextTags(); - if (inputTags?.Any() != true) - continue; - - // check if roe-producing fish - if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag)))) - continue; - - // yield roe - SObject roe = null; - Color color = this.GetRoeColor(input); - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ => - { - roe = new ColoredObject(812, 1, color) - { - name = $"{input.Name} Roe", - preserve = { Value = SObject.PreserveType.Roe }, - preservedParentSheetIndex = { Value = input.ParentSheetIndex } - }; - roe.Price += input.Price / 2; - return roe; - }); - - // aged roe - if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item - { - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ => new ColoredObject(447, 1, color) - { - name = $"Aged {input.Name} Roe", - Category = -27, - preserve = { Value = SObject.PreserveType.AgedRoe }, - preservedParentSheetIndex = { Value = input.ParentSheetIndex }, - Price = roe.Price * 2 - }); - } - } - } - break; - } + foreach (SearchableItem variant in this.GetFlavoredObjectVariants(item)) + yield return variant; } } } @@ -295,6 +184,135 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework /********* ** Private methods *********/ + /// Get the individual secret note items, if any. + /// Derived from . + private IEnumerable GetSecretNotes() + { + foreach (int secretNoteId in this.TryLoad("Data\\SecretNotes").Keys) + { + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ => + { + SObject note = new(79, 1); + note.name = $"{note.name} #{secretNoteId}"; + return note; + }); + } + } + + /// Get flavored variants of a base item (like Blueberry Wine for Blueberry), if any. + /// A sample of the base item. + private IEnumerable GetFlavoredObjectVariants(SObject item) + { + int id = item.ParentSheetIndex; + + switch (item.Category) + { + // fruit products + case SObject.FruitsCategory: + // wine + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + id, _ => new SObject(348, 1) + { + Name = $"{item.Name} Wine", + Price = item.Price * 3, + preserve = { SObject.PreserveType.Wine }, + preservedParentSheetIndex = { id } + }); + + // jelly + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + id, _ => new SObject(344, 1) + { + Name = $"{item.Name} Jelly", + Price = 50 + item.Price * 2, + preserve = { SObject.PreserveType.Jelly }, + preservedParentSheetIndex = { id } + }); + break; + + // vegetable products + case SObject.VegetableCategory: + // juice + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + id, _ => new SObject(350, 1) + { + Name = $"{item.Name} Juice", + Price = (int)(item.Price * 2.25d), + preserve = { SObject.PreserveType.Juice }, + preservedParentSheetIndex = { id } + }); + + // pickled + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => new SObject(342, 1) + { + Name = $"Pickled {item.Name}", + Price = 50 + item.Price * 2, + preserve = { SObject.PreserveType.Pickle }, + preservedParentSheetIndex = { id } + }); + break; + + // flower honey + case SObject.flowersCategory: + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => + { + SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false) + { + Name = $"{item.Name} Honey", + preservedParentSheetIndex = { id } + }; + honey.Price += item.Price * 2; + return honey; + }); + break; + + // roe and aged roe (derived from FishPond.GetFishProduce) + case SObject.sellAtFishShopCategory when id == 812: + { + this.GetRoeContextTagLookups(out HashSet simpleTags, out List> complexTags); + + foreach (var pair in Game1.objectInformation) + { + // get input + SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject; + var inputTags = input?.GetContextTags(); + if (inputTags?.Any() != true) + continue; + + // check if roe-producing fish + if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag)))) + continue; + + // yield roe + SObject roe = null; + Color color = this.GetRoeColor(input); + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ => + { + roe = new ColoredObject(812, 1, color) + { + name = $"{input.Name} Roe", + preserve = { Value = SObject.PreserveType.Roe }, + preservedParentSheetIndex = { Value = input.ParentSheetIndex } + }; + roe.Price += input.Price / 2; + return roe; + }); + + // aged roe + if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item + { + yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ => new ColoredObject(447, 1, color) + { + name = $"Aged {input.Name} Roe", + Category = -27, + preserve = { Value = SObject.PreserveType.AgedRoe }, + preservedParentSheetIndex = { Value = input.ParentSheetIndex }, + Price = roe.Price * 2 + }); + } + } + } + break; + } + } + /// Get optimized lookups to match items which produce roe in a fish pond. /// A lookup of simple singular tags which match a roe-producing fish. /// A list of tag sets which match roe-producing fish. -- cgit From 6dd4a8a12b25d349b18609132dade14da6ec3cf9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Jan 2022 17:46:45 -0500 Subject: fix item repo's handling of Journal Scraps and Secret Notes --- docs/release-notes.md | 6 ++- .../Framework/ItemRepository.cs | 62 ++++++++++++++++------ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f45df4bd..f1911716 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,7 +2,11 @@ # Release notes ## Upcoming release -* Improved translations. Thanks to ChulkyBow (updated Ukrainian)! +* For players: + * Improved translations. Thanks to ChulkyBow (updated Ukrainian)! + +* For console commands: + * Fixed `player_add` with Journal Scraps and Secret Notes. ## 3.13.4 Released 16 January 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 4a57ba5a..8704a403 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -134,24 +134,34 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework { string[] fields = Game1.objectInformation[id]?.Split('/'); - // secret notes - if (id == 79) + // ring + if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring + { + if (ShouldGet(ItemType.Ring)) + yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID)); + } + + // journal scrap + else if (id == 842) { if (ShouldGet(ItemType.Object)) { - foreach (SearchableItem secretNote in this.GetSecretNotes()) - yield return secretNote; + foreach (SearchableItem journalScrap in this.GetSecretNotes(isJournalScrap: true)) + yield return journalScrap; } } - // ring - else if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring + // secret notes + else if (id == 79) { - if (ShouldGet(ItemType.Ring)) - yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID)); + if (ShouldGet(ItemType.Object)) + { + foreach (SearchableItem secretNote in this.GetSecretNotes(isJournalScrap: false)) + yield return secretNote; + } } - // item + // object else if (ShouldGet(ItemType.Object)) { // spawn main item @@ -184,16 +194,38 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework /********* ** Private methods *********/ - /// Get the individual secret note items, if any. + /// Get the individual secret note or journal scrap items. + /// Whether to get journal scraps. /// Derived from . - private IEnumerable GetSecretNotes() + private IEnumerable GetSecretNotes(bool isJournalScrap) { - foreach (int secretNoteId in this.TryLoad("Data\\SecretNotes").Keys) + // get base item ID + int baseId = isJournalScrap ? 842 : 79; + + // get secret note IDs + var ids = this + .TryLoad("Data\\SecretNotes") + .Keys + .Where(isJournalScrap + ? id => (id >= GameLocation.JOURNAL_INDEX) + : id => (id < GameLocation.JOURNAL_INDEX) + ) + .Select(isJournalScrap + ? id => (id - GameLocation.JOURNAL_INDEX) + : id => id + ); + + // build items + foreach (int id in ids) { - yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ => + int fakeId = this.CustomIDOffset * 8 + id; + if (isJournalScrap) + fakeId += GameLocation.JOURNAL_INDEX; + + yield return this.TryCreate(ItemType.Object, fakeId, _ => { - SObject note = new(79, 1); - note.name = $"{note.name} #{secretNoteId}"; + SObject note = new(baseId, 1); + note.Name = $"{note.Name} #{id}"; return note; }); } -- cgit From 3431f486a2ef93e86d8923c1a4651644110df81b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Jan 2022 18:15:42 -0500 Subject: normalize season names in SDate constructor --- docs/release-notes.md | 3 +++ src/SMAPI.Tests/Utilities/SDateTests.cs | 10 ++++++---- src/SMAPI/Utilities/SDate.cs | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f1911716..4ca11f01 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,9 @@ * For players: * Improved translations. Thanks to ChulkyBow (updated Ukrainian)! +* For mod authors: + * The `SDate` constructor is no longer case-sensitive for season names. + * For console commands: * Fixed `player_add` with Journal Scraps and Secret Notes. diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index 0461952e..374f4921 100644 --- a/src/SMAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -16,9 +16,12 @@ namespace SMAPI.Tests.Utilities /********* ** Fields *********/ - /// All valid seasons. + /// The valid seasons. private static readonly string[] ValidSeasons = { "spring", "summer", "fall", "winter" }; + /// Sample user inputs for season names. + private static readonly string[] SampleSeasonValues = SDateTests.ValidSeasons.Concat(new[] { " WIntEr " }).ToArray(); + /// All valid days of a month. private static readonly int[] ValidDays = Enumerable.Range(1, 28).ToArray(); @@ -55,19 +58,18 @@ namespace SMAPI.Tests.Utilities ** Constructor ****/ [Test(Description = "Assert that the constructor sets the expected values for all valid dates.")] - public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.ValidSeasons))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year) + public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.SampleSeasonValues))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year) { // act SDate date = new SDate(day, season, year); // assert Assert.AreEqual(day, date.Day); - Assert.AreEqual(season, date.Season); + Assert.AreEqual(season.Trim().ToLowerInvariant(), date.Season); Assert.AreEqual(year, date.Year); } [Test(Description = "Assert that the constructor throws an exception if the values are invalid.")] - [TestCase(01, "Spring", 1)] // seasons are case-sensitive [TestCase(01, "springs", 1)] // invalid season name [TestCase(-1, "spring", 1)] // day < 0 [TestCase(0, "spring", 1)] // day zero diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index cd075dcc..e10a59f8 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -250,6 +250,8 @@ namespace StardewModdingAPI.Utilities /// One of the arguments has an invalid value (like day 35). private SDate(int day, string season, int year, bool allowDayZero) { + season = season?.Trim().ToLowerInvariant(); + // validate if (season == null) throw new ArgumentNullException(nameof(season)); @@ -277,7 +279,7 @@ namespace StardewModdingAPI.Utilities /// The year. private bool IsDayZero(int day, string season, int year) { - return day == 0 && season == "spring" && year == 1; + return day == 0 && season?.Trim().ToLower() == "spring" && year == 1; } /// Get the day of week for a given date. -- cgit From 25a9f54ecfdaf6c4ad67dfb46c3f8c556d15d949 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 29 Jan 2022 20:43:45 -0500 Subject: fix manifest JSON schema's update key pattern --- docs/release-notes.md | 3 +++ src/SMAPI.Web/wwwroot/schemas/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4ca11f01..585644ef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,9 @@ * For console commands: * Fixed `player_add` with Journal Scraps and Secret Notes. +* For the web UI: + * Fixed JSON validator warning for update keys without a subkey. + ## 3.13.4 Released 16 January 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index b6722347..7457b993 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -103,7 +103,7 @@ "type": "array", "items": { "type": "string", - "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)(?: *@ *[a-zA-Z0-9_]+ *)$", + "pattern": "^(?i)(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_\\-]+/[A-Za-z0-9_\\-]+|ModDrop:\\d+)(?: *@ *[a-zA-Z0-9_]+ *)?$", "@errorMessages": { "pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info." } -- cgit From 0ff82c38e7a5b630256d2cd23a63ac1088d13e39 Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 20:02:13 +0100 Subject: allow default interface method implementations in API proxies --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 70ef81f8..164cac0b 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -55,10 +55,39 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ret); } + var allTargetMethods = targetType.GetMethods().ToList(); + foreach (Type targetInterface in targetType.GetInterfaces()) + { + foreach (MethodInfo targetMethod in targetInterface.GetMethods()) + { + if (!targetMethod.IsAbstract) + allTargetMethods.Add(targetMethod); + } + } + // proxy methods foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) { - var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); + var proxyMethodParameters = proxyMethod.GetParameters(); + var targetMethod = allTargetMethods.Where(m => + { + if (m.Name != proxyMethod.Name) + return false; + if (m.ReturnType != proxyMethod.ReturnType) + return false; + + var mParameters = m.GetParameters(); + if (m.GetParameters().Length != proxyMethodParameters.Length) + return false; + for (int i = 0; i < mParameters.Length; i++) + { + // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) + // TODO: test if this actually works + if (!mParameters[i].ParameterType.IsAssignableFrom(proxyMethodParameters[i].ParameterType)) + return false; + } + return true; + }).FirstOrDefault(); if (targetMethod == null) throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); -- cgit From 3135925982b01d8f316adc171c7c1235ea41c1d3 Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 21:54:53 +0100 Subject: allow generic methods and any assignable types in API proxies --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 53 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 164cac0b..35faa852 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -65,15 +65,30 @@ namespace StardewModdingAPI.Framework.Reflection } } + bool AreTypesMatching(Type targetType, Type proxyType, MethodTypeMatchingPart part) + { + var typeA = part == MethodTypeMatchingPart.Parameter ? targetType : proxyType; + var typeB = part == MethodTypeMatchingPart.Parameter ? proxyType : targetType; + + if (typeA.IsGenericMethodParameter != typeB.IsGenericMethodParameter) + return false; + // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) + return typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB); + } + // proxy methods foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) { var proxyMethodParameters = proxyMethod.GetParameters(); + var proxyMethodGenericArguments = proxyMethod.GetGenericArguments(); var targetMethod = allTargetMethods.Where(m => { if (m.Name != proxyMethod.Name) return false; - if (m.ReturnType != proxyMethod.ReturnType) + + if (m.GetGenericArguments().Length != proxyMethodGenericArguments.Length) + return false; + if (!AreTypesMatching(m.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType)) return false; var mParameters = m.GetParameters(); @@ -81,9 +96,7 @@ namespace StardewModdingAPI.Framework.Reflection return false; for (int i = 0; i < mParameters.Length; i++) { - // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) - // TODO: test if this actually works - if (!mParameters[i].ParameterType.IsAssignableFrom(proxyMethodParameters[i].ParameterType)) + if (!AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter)) return false; } return true; @@ -91,7 +104,7 @@ namespace StardewModdingAPI.Framework.Reflection if (targetMethod == null) throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); - this.ProxyMethod(proxyBuilder, targetMethod, targetField); + this.ProxyMethod(proxyBuilder, proxyMethod, targetMethod, targetField); } // save info @@ -115,16 +128,30 @@ namespace StardewModdingAPI.Framework.Reflection *********/ /// Define a method which proxies access to a method on the target. /// The proxy type being generated. + /// The proxy method. /// The target method. /// The proxy field containing the API instance. - private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) + private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField) { - Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray(); - - // create method MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); + + // set up generic arguments + Type[] proxyGenericArguments = proxy.GetGenericArguments(); + string[] genericArgNames = proxyGenericArguments.Select(a => a.Name).ToArray(); + GenericTypeParameterBuilder[] genericTypeParameterBuilders = proxyGenericArguments.Length == 0 ? null : methodBuilder.DefineGenericParameters(genericArgNames); + for (int i = 0; i < proxyGenericArguments.Length; i++) + genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes); + + // set up return type + // TODO: keep if it's decided to use isAssignableFrom + methodBuilder.SetReturnType(proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType); + + // set up parameters + Type[] argTypes = proxy.GetParameters() + .Select(a => a.ParameterType) + .Select(t => t.IsGenericMethodParameter ? genericTypeParameterBuilders[t.GenericParameterPosition] : t) + .ToArray(); methodBuilder.SetParameters(argTypes); - methodBuilder.SetReturnType(target.ReturnType); // create method body { @@ -143,5 +170,11 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ret); } } + + /// The part of a method that is being matched. + private enum MethodTypeMatchingPart + { + ReturnType, Parameter + } } } -- cgit From 5b5304403b79ce09bd06d1c4b52b67427069b772 Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 22:19:16 +0100 Subject: oops old code --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 35faa852..4c154679 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -143,7 +143,6 @@ namespace StardewModdingAPI.Framework.Reflection genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes); // set up return type - // TODO: keep if it's decided to use isAssignableFrom methodBuilder.SetReturnType(proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType); // set up parameters -- cgit From 5a92b0cd357776eebb88e001384f9ca1ccdb7d5c Mon Sep 17 00:00:00 2001 From: Shockah Date: Tue, 8 Feb 2022 22:36:34 +0100 Subject: uses `proxy.Name` instead of `target.Name` (which makes more sense in this context) --- src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 4c154679..5ae96dff 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -133,7 +133,7 @@ namespace StardewModdingAPI.Framework.Reflection /// The proxy field containing the API instance. private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField) { - MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); + MethodBuilder methodBuilder = proxyBuilder.DefineMethod(proxy.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); // set up generic arguments Type[] proxyGenericArguments = proxy.GetGenericArguments(); -- cgit From 43ad219b75740ef71ad9bad496b00c076182619b Mon Sep 17 00:00:00 2001 From: Shockah Date: Wed, 9 Feb 2022 20:07:01 +0100 Subject: support proxying return values in API proxies --- .../Framework/Reflection/InterfaceProxyBuilder.cs | 183 ++++++++++++++++----- .../Framework/Reflection/InterfaceProxyFactory.cs | 30 +++- .../Framework/Reflection/InterfaceProxyGlue.cs | 18 ++ 3 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 src/SMAPI/Framework/Reflection/InterfaceProxyGlue.cs diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs index 5ae96dff..d8b066bd 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -1,13 +1,22 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using HarmonyLib; namespace StardewModdingAPI.Framework.Reflection { /// Generates a proxy class to access a mod API through an arbitrary interface. internal class InterfaceProxyBuilder { + /********* + ** Consts + *********/ + private static readonly string TargetFieldName = "__Target"; + private static readonly string GlueFieldName = "__Glue"; + private static readonly MethodInfo CreateInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.CreateInstanceForProxyTypeName), new Type[] { typeof(string), typeof(object) }); + /********* ** Fields *********/ @@ -22,11 +31,14 @@ namespace StardewModdingAPI.Framework.Reflection ** Public methods *********/ /// Construct an instance. + /// The that requested to build a proxy. /// The type name to generate. /// The CLR module in which to create proxy classes. /// The interface type to implement. /// The target type. - public InterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) + /// The unique ID of the mod consuming the API. + /// The unique ID of the mod providing the API. + public InterfaceProxyBuilder(InterfaceProxyFactory factory, string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType, string sourceModID, string targetModID) { // validate if (name == null) @@ -38,12 +50,13 @@ namespace StardewModdingAPI.Framework.Reflection TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); proxyBuilder.AddInterfaceImplementation(interfaceType); - // create field to store target instance - FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); + // create fields to store target instance and proxy factory + FieldBuilder targetField = proxyBuilder.DefineField(TargetFieldName, targetType, FieldAttributes.Private); + FieldBuilder glueField = proxyBuilder.DefineField(GlueFieldName, typeof(InterfaceProxyGlue), FieldAttributes.Private); - // create constructor which accepts target instance and sets field + // create constructor which accepts target instance + factory, and sets fields { - ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); + ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType, typeof(InterfaceProxyGlue) }); ILGenerator il = constructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // this @@ -52,6 +65,9 @@ namespace StardewModdingAPI.Framework.Reflection il.Emit(OpCodes.Ldarg_0); // this il.Emit(OpCodes.Ldarg_1); // load argument il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldarg_2); // load argument + il.Emit(OpCodes.Stfld, glueField); // set field to loaded argument il.Emit(OpCodes.Ret); } @@ -65,15 +81,20 @@ namespace StardewModdingAPI.Framework.Reflection } } - bool AreTypesMatching(Type targetType, Type proxyType, MethodTypeMatchingPart part) + MatchingTypesResult AreTypesMatching(Type targetType, Type proxyType, MethodTypeMatchingPart part) { var typeA = part == MethodTypeMatchingPart.Parameter ? targetType : proxyType; var typeB = part == MethodTypeMatchingPart.Parameter ? proxyType : targetType; if (typeA.IsGenericMethodParameter != typeB.IsGenericMethodParameter) - return false; + return MatchingTypesResult.False; // TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality) - return typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB); + if (typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB)) + return MatchingTypesResult.True; + + if (!proxyType.IsGenericMethodParameter && proxyType.IsInterface && proxyType.Assembly == interfaceType.Assembly) + return MatchingTypesResult.IfProxied; + return MatchingTypesResult.False; } // proxy methods @@ -81,30 +102,61 @@ namespace StardewModdingAPI.Framework.Reflection { var proxyMethodParameters = proxyMethod.GetParameters(); var proxyMethodGenericArguments = proxyMethod.GetGenericArguments(); - var targetMethod = allTargetMethods.Where(m => + + foreach (MethodInfo targetMethod in allTargetMethods) { - if (m.Name != proxyMethod.Name) - return false; + // checking if `targetMethod` matches `proxyMethod` - if (m.GetGenericArguments().Length != proxyMethodGenericArguments.Length) - return false; - if (!AreTypesMatching(m.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType)) - return false; + if (targetMethod.Name != proxyMethod.Name) + continue; + if (targetMethod.GetGenericArguments().Length != proxyMethodGenericArguments.Length) + continue; + var positionsToProxy = new HashSet(); // null = return type; anything else = parameter position + + switch (AreTypesMatching(targetMethod.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType)) + { + case MatchingTypesResult.False: + continue; + case MatchingTypesResult.True: + break; + case MatchingTypesResult.IfProxied: + positionsToProxy.Add(null); + break; + } - var mParameters = m.GetParameters(); - if (m.GetParameters().Length != proxyMethodParameters.Length) - return false; + var mParameters = targetMethod.GetParameters(); + if (mParameters.Length != proxyMethodParameters.Length) + continue; for (int i = 0; i < mParameters.Length; i++) { - if (!AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter)) - return false; + switch (AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter)) + { + case MatchingTypesResult.False: + goto targetMethodLoopContinue; + case MatchingTypesResult.True: + break; + case MatchingTypesResult.IfProxied: + if (proxyMethodParameters[i].IsOut) + { + positionsToProxy.Add(i); + break; + } + else + { + goto targetMethodLoopContinue; + } + } } - return true; - }).FirstOrDefault(); - if (targetMethod == null) - throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); - this.ProxyMethod(proxyBuilder, proxyMethod, targetMethod, targetField); + // method matched; proxying + + this.ProxyMethod(factory, proxyBuilder, proxyMethod, targetMethod, targetField, glueField, positionsToProxy, sourceModID, targetModID); + goto proxyMethodLoopContinue; + targetMethodLoopContinue:; + } + + throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); + proxyMethodLoopContinue:; } // save info @@ -114,12 +166,13 @@ namespace StardewModdingAPI.Framework.Reflection /// Create an instance of the proxy for a target instance. /// The target instance. - public object CreateInstance(object targetInstance) + /// The that requested to build a proxy. + public object CreateInstance(object targetInstance, InterfaceProxyFactory factory) { - ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); + ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType, typeof(InterfaceProxyGlue) }); if (constructor == null) throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen - return constructor.Invoke(new[] { targetInstance }); + return constructor.Invoke(new[] { targetInstance, new InterfaceProxyGlue(factory) }); } @@ -127,11 +180,16 @@ namespace StardewModdingAPI.Framework.Reflection ** Private methods *********/ /// Define a method which proxies access to a method on the target. + /// The that requested to build a proxy. /// The proxy type being generated. /// The proxy method. /// The target method. /// The proxy field containing the API instance. - private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField) + /// The proxy field containing an . + /// Parameter type positions (or null for the return type) for which types should also be proxied. + /// The unique ID of the mod consuming the API. + /// The unique ID of the mod providing the API. + private void ProxyMethod(InterfaceProxyFactory factory, TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField, FieldBuilder glueField, ISet positionsToProxy, string sourceModID, string targetModID) { MethodBuilder methodBuilder = proxyBuilder.DefineMethod(proxy.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); @@ -143,7 +201,8 @@ namespace StardewModdingAPI.Framework.Reflection genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes); // set up return type - methodBuilder.SetReturnType(proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType); + Type returnType = proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType; + methodBuilder.SetReturnType(returnType); // set up parameters Type[] argTypes = proxy.GetParameters() @@ -152,18 +211,62 @@ namespace StardewModdingAPI.Framework.Reflection .ToArray(); methodBuilder.SetParameters(argTypes); + // proxy additional types + string returnValueProxyTypeName = null; + string[] parameterProxyTypeNames = new string[argTypes.Length]; + if (positionsToProxy.Count > 0) + { + var targetParameters = target.GetParameters(); + foreach (int? position in positionsToProxy) + { + // we don't check for generics here, because earlier code does and generic positions won't end up here + if (position == null) // it's the return type + { + var builder = factory.ObtainBuilder(target.ReturnType, proxy.ReturnType, sourceModID, targetModID); + returnType = proxy.ReturnType; + returnValueProxyTypeName = builder.ProxyType.FullName; + } + else // it's one of the parameters + { + var builder = factory.ObtainBuilder(targetParameters[position.Value].ParameterType, argTypes[position.Value], sourceModID, targetModID); + argTypes[position.Value] = proxy.ReturnType; + parameterProxyTypeNames[position.Value] = builder.ProxyType.FullName; + } + } + + methodBuilder.SetReturnType(returnType); + methodBuilder.SetParameters(argTypes); + } + // create method body { ILGenerator il = methodBuilder.GetILGenerator(); - // load target instance - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, instanceField); + void EmitCallInstance() + { + // load target instance + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, instanceField); + + // invoke target method on instance + for (int i = 0; i < argTypes.Length; i++) + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Callvirt, target); + } - // invoke target method on instance - for (int i = 0; i < argTypes.Length; i++) - il.Emit(OpCodes.Ldarg, i + 1); - il.Emit(OpCodes.Call, target); + if (returnValueProxyTypeName == null) + { + EmitCallInstance(); + } + else + { + // this.Glue.CreateInstanceForProxyTypeName(proxyTypeName, this.Instance.Call(args)) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, glueField); + il.Emit(OpCodes.Ldstr, returnValueProxyTypeName); + EmitCallInstance(); + il.Emit(OpCodes.Call, CreateInstanceForProxyTypeNameMethod); + } // return result il.Emit(OpCodes.Ret); @@ -175,5 +278,11 @@ namespace StardewModdingAPI.Framework.Reflection { ReturnType, Parameter } + + /// The result of matching a target and a proxy type. + private enum MatchingTypesResult + { + False, IfProxied, True + } } } diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs index 5acba569..8ce187bf 100644 --- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs +++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -35,26 +35,40 @@ namespace StardewModdingAPI.Framework.Reflection /// The unique ID of the mod providing the API. public TInterface CreateProxy(object instance, string sourceModID, string targetModID) where TInterface : cla