From a4713ea88238e6a6d62447aef97b35321e63c010 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 12 Jun 2017 18:44:36 -0400 Subject: add separate list of obsolete mods --- .../Framework/ModLoading/ModResolver.cs | 22 +++++++++++++++++----- .../Framework/Models/DisabledMod.cs | 22 ++++++++++++++++++++++ src/StardewModdingAPI/Framework/Models/SConfig.cs | 3 +++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/DisabledMod.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index f5139ce5..e8308f3e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -17,10 +17,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The root path to search for mods. /// The JSON helper with which to read manifests. /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + /// Metadata about mods that SMAPI should consider obsolete and not load. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords, IEnumerable disabledMods) { compatibilityRecords = compatibilityRecords.ToArray(); + disabledMods = disabledMods.ToArray(); + foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file @@ -47,20 +50,29 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get compatibility record + // validate metadata ModCompatibility compatibility = null; if (manifest != null) { + // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + + // check if mod should be disabled + DisabledMod disabledMod = disabledMods.FirstOrDefault(mod => mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)); + if (disabledMod != null) + error = $"it's obsolete: {disabledMod.ReasonPhrase}"; + + // get compatibility record compatibility = ( from mod in compatibilityRecords where - mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) - && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) - && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) + && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) + && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) select mod ).FirstOrDefault(); } + // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name diff --git a/src/StardewModdingAPI/Framework/Models/DisabledMod.cs b/src/StardewModdingAPI/Framework/Models/DisabledMod.cs new file mode 100644 index 00000000..170fa760 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/DisabledMod.cs @@ -0,0 +1,22 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata about for a mod that should never be loaded. + internal class DisabledMod + { + /********* + ** Accessors + *********/ + /**** + ** From config + ****/ + /// The unique mod IDs. + public string[] ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The reason phrase to show in the warning, or null to use the default value. + /// "this mod is no longer supported or used" + public string ReasonPhrase { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index c3f0816e..b2ca4113 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -17,5 +17,8 @@ /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. public ModCompatibility[] ModCompatibility { get; set; } + + /// A list of mods which should be considered obsolete and not loaded. + public DisabledMod[] DisabledMods { get; set; } } } -- cgit From 3c3953a7fdca6e79f50a4a5474be69ca6aab6446 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 18:18:04 -0400 Subject: add support for minimum dependency versions (#286) --- release-notes.md | 1 + src/StardewModdingAPI.Tests/ModResolverTests.cs | 136 +++++++++++++++------ .../Framework/ModLoading/ModResolver.cs | 44 ++++--- .../Framework/Models/ManifestDependency.cs | 9 +- .../Serialisation/ManifestFieldConverter.cs | 3 +- src/StardewModdingAPI/IManifestDependency.cs | 3 + 6 files changed, 139 insertions(+), 57 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index c75f0c19..8a8aa46e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,6 +17,7 @@ For players: * SMAPI will no longer load mods known to be obsolete or unneeded. For modders: +* You can now specify minimum dependency versions in `manifest.json`. * Added `System.ValueTuple.dll` to the SMAPI install package so mods can use [C# 7 value tuples](https://docs.microsoft.com/en-us/dotnet/csharp/tuples). ## 1.14 diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index a9df2056..4afba162 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Tests Mock mock = new Mock(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mock.Setup(p => p.Compatibility).Returns(() => null); - mock.Setup(p => p.Manifest).Returns(this.GetRandomManifest(m => m.MinimumApiVersion = "1.1")); + mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = "1.1")); mock.Setup(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny())).Returns(() => mock.Object); // act @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Tests Mock mock = new Mock(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mock.Setup(p => p.Compatibility).Returns(() => null); - mock.Setup(p => p.Manifest).Returns(this.GetRandomManifest()); + mock.Setup(p => p.Manifest).Returns(this.GetManifest()); mock.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); mock.Setup(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny())).Returns(() => mock.Object); @@ -192,7 +192,7 @@ namespace StardewModdingAPI.Tests public void ValidateManifests_Valid_Passes() { // set up manifest - IManifest manifest = this.GetRandomManifest(); + IManifest manifest = this.GetManifest(); // create DLL string modFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); @@ -231,9 +231,9 @@ namespace StardewModdingAPI.Tests { // arrange // A B C - Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B"); - Mock modC = this.GetMetadataForDependencyTest("Mod C"); + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B"); + Mock modC = this.GetMetadata("Mod C"); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object, modC.Object }).ToArray(); @@ -267,9 +267,9 @@ namespace StardewModdingAPI.Tests // ▲ ▲ // │ │ // └─ C ─┘ - Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod A", "Mod B" }); + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod A", "Mod B" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object }).ToArray(); @@ -286,10 +286,10 @@ namespace StardewModdingAPI.Tests { // arrange // A ◀── B ◀── C ◀── D - Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B" }); - Mock modD = this.GetMetadataForDependencyTest("Mod D", dependencies: new[] { "Mod C" }); + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); + Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); @@ -310,12 +310,12 @@ namespace StardewModdingAPI.Tests // ▲ ▲ // │ │ // E ◀── F - Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B" }); - Mock modD = this.GetMetadataForDependencyTest("Mod D", dependencies: new[] { "Mod C" }); - Mock modE = this.GetMetadataForDependencyTest("Mod E", dependencies: new[] { "Mod B" }); - Mock modF = this.GetMetadataForDependencyTest("Mod F", dependencies: new[] { "Mod C", "Mod E" }); + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }); + Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod C" }); + Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod B" }); + Mock modF = this.GetMetadata("Mod F", dependencies: new[] { "Mod C", "Mod E" }); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modF.Object, modE.Object }).ToArray(); @@ -338,11 +338,11 @@ namespace StardewModdingAPI.Tests // ▲ │ // │ ▼ // └──── E - Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B", "Mod D" }, allowStatusChange: true); - Mock modD = this.GetMetadataForDependencyTest("Mod D", dependencies: new[] { "Mod E" }, allowStatusChange: true); - Mock modE = this.GetMetadataForDependencyTest("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B", "Mod D" }, allowStatusChange: true); + Mock modD = this.GetMetadata("Mod D", dependencies: new[] { "Mod E" }, allowStatusChange: true); + Mock modE = this.GetMetadata("Mod E", dependencies: new[] { "Mod C" }, allowStatusChange: true); // act IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object, modE.Object }).ToArray(); @@ -361,9 +361,9 @@ namespace StardewModdingAPI.Tests { // arrange // A ◀── B ◀── C D (failed) - Mock modA = this.GetMetadataForDependencyTest("Mod A"); - Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); - Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); + Mock modA = this.GetMetadata("Mod A"); + Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); Mock modD = new Mock(MockBehavior.Strict); modD.Setup(p => p.Manifest).Returns(null); modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); @@ -378,13 +378,47 @@ namespace StardewModdingAPI.Tests Assert.AreSame(modB.Object, mods[2], "The load order is incorrect: mod B should be third since it needs mod A, and is needed by mod C."); Assert.AreSame(modC.Object, mods[3], "The load order is incorrect: mod C should be fourth since it needs mod B, and is needed by mod D."); } + + [Test(Description = "Assert that dependencies are failed if they don't meet the minimum version.")] + public void ProcessDependencies_WithMinVersions_FailsIfNotMet() + { + // arrange + // A 1.0 ◀── B (need A 1.1) + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.1")), allowStatusChange: true); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); + } + + [Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")] + public void ProcessDependencies_WithMinVersions_SucceedsIfMet() + { + // arrange + // A 1.0 ◀── B (need A 1.0-beta) + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0-beta")), allowStatusChange: false); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modA.Object, modB.Object }).ToArray(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + } + /********* ** Private methods *********/ /// Get a randomised basic manifest. /// Adjust the generated manifest. - private Manifest GetRandomManifest(Action adjust = null) + private Manifest GetManifest(Action adjust = null) { Manifest manifest = new Manifest { @@ -401,26 +435,50 @@ namespace StardewModdingAPI.Tests /// Get a randomised basic manifest. /// The mod's name and unique ID. + /// The mod version. /// The dependencies this mod requires. + private IManifest GetManifest(string uniqueID, string version, params IManifestDependency[] dependencies) + { + return this.GetManifest(manifest => + { + manifest.Name = uniqueID; + manifest.UniqueID = uniqueID; + manifest.Version = new SemanticVersion(version); + manifest.Dependencies = dependencies; + }); + } + + /// Get a randomised basic manifest. + /// The mod's name and unique ID. + private Mock GetMetadata(string uniqueID) + { + return this.GetMetadata(this.GetManifest(uniqueID, "1.0")); + } + + /// Get a randomised basic manifest. + /// The mod's name and unique ID. + /// The dependencies this mod requires. + /// Whether the code being tested is allowed to change the mod status. + private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) + { + IManifest manifest = this.GetManifest(uniqueID, "1.0", dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); + return this.GetMetadata(manifest, allowStatusChange); + } + + /// Get a randomised basic manifest. + /// The mod manifest. /// Whether the code being tested is allowed to change the mod status. - private Mock GetMetadataForDependencyTest(string uniqueID, string[] dependencies = null, bool allowStatusChange = false) + private Mock GetMetadata(IManifest manifest, bool allowStatusChange = false) { Mock mod = new Mock(MockBehavior.Strict); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.DisplayName).Returns(uniqueID); - mod.Setup(p => p.Manifest).Returns( - this.GetRandomManifest(manifest => - { - manifest.Name = uniqueID; - manifest.UniqueID = uniqueID; - manifest.Dependencies = dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID)).ToArray(); - }) - ); + mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); + mod.Setup(p => p.Manifest).Returns(manifest); if (allowStatusChange) { mod .Setup(p => p.SetStatus(It.IsAny(), It.IsAny())) - .Callback((status, message) => Console.WriteLine($"<{uniqueID} changed status: [{status}] {message}")) + .Callback((status, message) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}")) .Returns(mod.Object); } return mod; diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index e8308f3e..dc140483 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -205,20 +205,40 @@ namespace StardewModdingAPI.Framework.ModLoading return states[mod] = ModDependencyStatus.Sorted; } + // get dependencies + var dependencies = + ( + from entry in mod.Manifest.Dependencies + let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + orderby entry.UniqueID + select (ID: entry.UniqueID, MinVersion: entry.MinimumVersion, Mod: dependencyMod) + ) + .ToArray(); + // missing required dependencies, mark failed { - string[] missingModIDs = + string[] failedIDs = (from entry in dependencies where entry.Mod == null select entry.ID).ToArray(); + if (failedIDs.Any()) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + return states[mod] = ModDependencyStatus.Failed; + } + } + + // dependency min version not met, mark failed + { + string[] failedLabels = ( - from dependency in mod.Manifest.Dependencies - where mods.All(m => m.Manifest?.UniqueID != dependency.UniqueID) - orderby dependency.UniqueID - select dependency.UniqueID + from entry in dependencies + where entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) + select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" ) .ToArray(); - if (missingModIDs.Any()) + if (failedLabels.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", missingModIDs)})."); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return states[mod] = ModDependencyStatus.Failed; } } @@ -227,16 +247,8 @@ namespace StardewModdingAPI.Framework.ModLoading { states[mod] = ModDependencyStatus.Checking; - // get mods to load first - IModMetadata[] modsToLoadFirst = - ( - from other in mods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest?.UniqueID) - select other - ) - .ToArray(); - // recursively sort dependencies + IModMetadata[] modsToLoadFirst = dependencies.Select(p => p.Mod).ToArray(); foreach (IModMetadata requiredMod in modsToLoadFirst) { var subchain = new List(currentChain) { mod }; diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs index 2f580c1d..a0ff0c90 100644 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs @@ -9,15 +9,22 @@ /// The unique mod ID to require. public string UniqueID { get; set; } + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + /********* ** Public methods *********/ /// Construct an instance. /// The unique mod ID to require. - public ManifestDependency(string uniqueID) + /// The minimum required version (if any). + public ManifestDependency(string uniqueID, string minimumVersion) { this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 6b5a6aaa..7acb5fd0 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -51,7 +51,8 @@ namespace StardewModdingAPI.Framework.Serialisation foreach (JObject obj in JArray.Load(reader).Children()) { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); - result.Add(new ManifestDependency(uniqueID)); + string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); + result.Add(new ManifestDependency(uniqueID, minVersion)); } return result.ToArray(); } diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs index 7bd2e8b6..ebb1140e 100644 --- a/src/StardewModdingAPI/IManifestDependency.cs +++ b/src/StardewModdingAPI/IManifestDependency.cs @@ -8,5 +8,8 @@ *********/ /// The unique mod ID to require. string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } } } -- cgit From b46776a4fbabe765b81751f8c4984cdd8a207419 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 22:08:56 -0400 Subject: enable string versions in manifest.json (#308) --- release-notes.md | 1 + .../Serialisation/ManifestFieldConverter.cs | 25 ++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 8a8aa46e..851e6abe 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,7 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...2.0). For mod developers: +* The manifest.json version can now be specified as a string. * Added `ContentEvents.AssetLoading` event with a helper which lets you intercept the XNB content load, and dynamically adjust or replace the content being loaded (including support for patching images). diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 7acb5fd0..7a59f134 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -36,12 +36,25 @@ namespace StardewModdingAPI.Framework.Serialisation // semantic version if (objectType == typeof(ISemanticVersion)) { - JObject obj = JObject.Load(reader); - int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); - string build = obj.Value(nameof(ISemanticVersion.Build)); - return new SemanticVersion(major, minor, patch, build); + JToken token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.Object: + { + JObject obj = (JObject)token; + int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); + string build = obj.Value(nameof(ISemanticVersion.Build)); + return new SemanticVersion(major, minor, patch, build); + } + + case JTokenType.String: + return new SemanticVersion(token.Value()); + + default: + throw new FormatException($"Can't parse {token.Type} token as a semantic version, must be an object or string."); + } } // manifest dependency -- cgit From fb8fefea00aacd603e68fbdbaecd27e4c451cc82 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 22:11:48 -0400 Subject: show friendly error when parsing a manifest version fails (#308) --- .../Framework/Exceptions/SParseException.cs | 17 +++++++++++++++++ .../Framework/ModLoading/ModResolver.cs | 5 +++++ .../Framework/Serialisation/ManifestFieldConverter.cs | 12 ++++++++++-- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SParseException.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs new file mode 100644 index 00000000..f7133ee7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index dc140483..045b175c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; @@ -45,6 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) error = "its manifest doesn't set an entry DLL."; } + catch (SParseException ex) + { + error = $"parsing its manifest failed: {ex.Message}"; + } catch (Exception ex) { error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 7a59f134..e6d62d50 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.Serialisation @@ -50,10 +51,17 @@ namespace StardewModdingAPI.Framework.Serialisation } case JTokenType.String: - return new SemanticVersion(token.Value()); + { + string str = token.Value(); + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta."); + return version; + } default: - throw new FormatException($"Can't parse {token.Type} token as a semantic version, must be an object or string."); + throw new SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 465a5ea7..77d3b12b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -151,6 +151,7 @@ + -- cgit From 8d7b5b372657c0f96196cb2a902b2bdcce184fe4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Jun 2017 11:01:47 -0400 Subject: improve logging when SMAPI loads mods --- release-notes.md | 2 ++ .../Framework/ModLoading/AssemblyLoader.cs | 27 +++++++++++++--------- src/StardewModdingAPI/Program.cs | 11 +++++++-- 3 files changed, 27 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index f0e08e3a..d1f02588 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,10 +17,12 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.14...1.15). For players: * SMAPI will no longer load mods known to be obsolete or unneeded. * When the `ObjectInformation.xnb` is broken, SMAPI will now print one error to the console instead of a warning flood. (The individual issues are still listed in the log file if needed.) +* Mods are now listed in alphabetical order in the log. For modders: * You can now specify minimum dependency versions in `manifest.json`. * Added `System.ValueTuple.dll` to the SMAPI install package so mods can use [C# 7 value tuples](https://docs.microsoft.com/en-us/dotnet/csharp/tuples). +* Improved trace logging when SMAPI loads mods. * Fixed `SemanticVersion` parsing some invalid versions into close approximations (like `1.apple` → `1.0-apple`). * Fixed `SemanticVersion` not treating hyphens as separators when comparing prerelease tags. _(While that was technically correct, it leads to unintuitive behaviour like sorting `-alpha-2` _after_ `-alpha-10`, even though `-alpha.2` sorts before `-alpha.10`.)_ diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 42bd7bfb..406d49e1 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -71,13 +71,15 @@ namespace StardewModdingAPI.Framework.ModLoading } // rewrite & load assemblies in leaf-to-root order + bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; foreach (AssemblyParseResult assembly in assemblies) { - bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); if (changed) { - this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}.dll (rewritten in memory)...", LogLevel.Trace); using (MemoryStream outStream = new MemoryStream()) { assembly.Definition.Write(outStream); @@ -87,7 +89,8 @@ namespace StardewModdingAPI.Framework.ModLoading } else { - this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}.dll...", LogLevel.Trace); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } } @@ -161,12 +164,14 @@ namespace StardewModdingAPI.Framework.ModLoading /// Rewrite the types referenced by an assembly. /// The assembly to rewrite. /// Assume the mod is compatible, even if incompatible code is detected. + /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) + private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) { ModuleDefinition module = assembly.MainModule; HashSet loggedMessages = new HashSet(); + string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; @@ -175,7 +180,7 @@ namespace StardewModdingAPI.Framework.ModLoading // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; @@ -205,15 +210,15 @@ namespace StardewModdingAPI.Framework.ModLoading { if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; } } catch (IncompatibleInstructionException) { if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); } } @@ -227,15 +232,15 @@ namespace StardewModdingAPI.Framework.ModLoading { if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; } } catch (IncompatibleInstructionException) { if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index da0c5bca..0805b6c5 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -483,7 +483,7 @@ namespace StardewModdingAPI /// Returns whether all integrity checks passed. private bool ValidateContentIntegrity() { - this.Monitor.Log("Detecting common issues..."); + this.Monitor.Log("Detecting common issues...", LogLevel.Trace); bool issuesFound = false; // object format (commonly broken by outdated mods) @@ -603,6 +603,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { + this.Monitor.Log($"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}...", LogLevel.Trace); modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) @@ -659,7 +660,6 @@ namespace StardewModdingAPI metadata.SetMod(mod); this.ModRegistry.Add(metadata); modsLoaded++; - this.Monitor.Log($"Loaded {metadata.DisplayName} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { @@ -667,6 +667,13 @@ namespace StardewModdingAPI } } + // log mods + foreach (var metadata in this.ModRegistry.GetMods().OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log($"Loaded {metadata.DisplayName} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); + } + // initialise translations this.ReloadTranslations(); -- cgit From 6073d24cabe3fa93ddbba7e4a613e7342a8b20c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Jun 2017 11:08:45 -0400 Subject: change manifest.MinimumApiVersion to ISemanticVersion --- release-notes.md | 1 + src/StardewModdingAPI.Tests/Core/ModResolverTests.cs | 4 ++-- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 14 +++----------- src/StardewModdingAPI/Framework/Models/Manifest.cs | 3 ++- src/StardewModdingAPI/IManifest.cs | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index d1f02588..e5cfb7f8 100644 --- a/release-notes.md +++ b/release-notes.md @@ -23,6 +23,7 @@ For modders: * You can now specify minimum dependency versions in `manifest.json`. * Added `System.ValueTuple.dll` to the SMAPI install package so mods can use [C# 7 value tuples](https://docs.microsoft.com/en-us/dotnet/csharp/tuples). * Improved trace logging when SMAPI loads mods. +* Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. * Fixed `SemanticVersion` parsing some invalid versions into close approximations (like `1.apple` → `1.0-apple`). * Fixed `SemanticVersion` not treating hyphens as separators when comparing prerelease tags. _(While that was technically correct, it leads to unintuitive behaviour like sorting `-alpha-2` _after_ `-alpha-10`, even though `-alpha.2` sorts before `-alpha.10`.)_ diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index efb1c348..36cc3495 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -100,7 +100,7 @@ namespace StardewModdingAPI.Tests.Core Assert.AreEqual(original[nameof(IManifest.Author)], mod.Manifest.Author, "The manifest's author doesn't match."); Assert.AreEqual(original[nameof(IManifest.Description)], mod.Manifest.Description, "The manifest's description doesn't match."); Assert.AreEqual(original[nameof(IManifest.EntryDll)], mod.Manifest.EntryDll, "The manifest's entry DLL doesn't match."); - Assert.AreEqual(original[nameof(IManifest.MinimumApiVersion)], mod.Manifest.MinimumApiVersion, "The manifest's minimum API version doesn't match."); + Assert.AreEqual(original[nameof(IManifest.MinimumApiVersion)], mod.Manifest.MinimumApiVersion?.ToString(), "The manifest's minimum API version doesn't match."); Assert.AreEqual(original[nameof(IManifest.Version)]?.ToString(), mod.Manifest.Version?.ToString(), "The manifest's version doesn't match."); Assert.IsNotNull(mod.Manifest.ExtraFields, "The extra fields should not be null."); @@ -159,7 +159,7 @@ namespace StardewModdingAPI.Tests.Core Mock mock = new Mock(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mock.Setup(p => p.Compatibility).Returns(() => null); - mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = "1.1")); + mock.Setup(p => p.Manifest).Returns(this.GetManifest(m => m.MinimumApiVersion = new SemanticVersion("1.1"))); mock.Setup(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny())).Returns(() => mock.Object); // act diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 045b175c..cefc860b 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -122,18 +122,10 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate SMAPI version - if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion)) + if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { - if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {apiVersion}."); - continue; - } - if (minVersion.IsNewerThan(apiVersion)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); - continue; - } + mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + continue; } // validate DLL path diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index be781585..8e5d13f8 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -25,7 +25,8 @@ namespace StardewModdingAPI.Framework.Models public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - public string MinimumApiVersion { get; set; } + [JsonConverter(typeof(ManifestFieldConverter))] + public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 9533aadb..407db1ce 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI ISemanticVersion Version { get; } /// The minimum SMAPI version required by this mod, if any. - string MinimumApiVersion { get; } + ISemanticVersion MinimumApiVersion { get; } /// The unique mod ID. string UniqueID { get; } -- cgit From 49c75de5fc139144b152207ba05f2936a2d25904 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Jun 2017 21:13:01 -0400 Subject: rewrite content interception using latest proposed API (#255) --- src/StardewModdingAPI/Events/ContentEvents.cs | 65 ------------ .../Framework/Content/AssetData.cs | 44 ++++++++ .../Framework/Content/AssetDataForDictionary.cs | 45 +++++++++ .../Framework/Content/AssetDataForImage.cs | 70 +++++++++++++ .../Framework/Content/AssetDataForObject.cs | 47 +++++++++ .../Framework/Content/AssetInfo.cs | 82 +++++++++++++++ .../Framework/Content/ContentEventData.cs | 111 --------------------- .../Framework/Content/ContentEventHelper.cs | 47 --------- .../Content/ContentEventHelperForDictionary.cs | 45 --------- .../Content/ContentEventHelperForImage.cs | 70 ------------- src/StardewModdingAPI/Framework/ContentHelper.cs | 8 ++ src/StardewModdingAPI/Framework/SContentManager.cs | 64 ++++++++---- src/StardewModdingAPI/Framework/SGame.cs | 10 +- src/StardewModdingAPI/IAssetData.cs | 47 +++++++++ src/StardewModdingAPI/IAssetDataForDictionary.cs | 26 +++++ src/StardewModdingAPI/IAssetDataForImage.cs | 23 +++++ src/StardewModdingAPI/IAssetEditor.cs | 17 ++++ src/StardewModdingAPI/IAssetInfo.cs | 28 ++++++ src/StardewModdingAPI/IContentEventData.cs | 38 ------- src/StardewModdingAPI/IContentEventHelper.cs | 26 ----- .../IContentEventHelperForDictionary.cs | 26 ----- .../IContentEventHelperForImage.cs | 23 ----- src/StardewModdingAPI/Program.cs | 11 +- src/StardewModdingAPI/StardewModdingAPI.csproj | 18 ++-- 24 files changed, 507 insertions(+), 484 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Content/AssetData.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetInfo.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventData.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs create mode 100644 src/StardewModdingAPI/IAssetData.cs create mode 100644 src/StardewModdingAPI/IAssetDataForDictionary.cs create mode 100644 src/StardewModdingAPI/IAssetDataForImage.cs create mode 100644 src/StardewModdingAPI/IAssetEditor.cs create mode 100644 src/StardewModdingAPI/IAssetInfo.cs delete mode 100644 src/StardewModdingAPI/IContentEventData.cs delete mode 100644 src/StardewModdingAPI/IContentEventHelper.cs delete mode 100644 src/StardewModdingAPI/IContentEventHelperForDictionary.cs delete mode 100644 src/StardewModdingAPI/IContentEventHelperForImage.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs index 8fa9ae3c..4b4e2ad0 100644 --- a/src/StardewModdingAPI/Events/ContentEvents.cs +++ b/src/StardewModdingAPI/Events/ContentEvents.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; namespace StardewModdingAPI.Events @@ -8,21 +6,6 @@ namespace StardewModdingAPI.Events /// Events raised when the game loads content. public static class ContentEvents { - /********* - ** Properties - *********/ - /// Tracks the installed mods. - private static ModRegistry ModRegistry; - - /// Encapsulates monitoring and logging. - private static IMonitor Monitor; - - /// The mods using the experimental API for which a warning has been raised. - private static readonly HashSet WarnedMods = new HashSet(); - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _AfterAssetLoaded; /********* ** Events @@ -30,35 +13,10 @@ namespace StardewModdingAPI.Events /// Raised after the content language changes. public static event EventHandler> AfterLocaleChanged; - /// Raised when an XNB file is being read into the cache. Mods can change the data here before it's cached. -#if EXPERIMENTAL - public -#else - internal -#endif - static event EventHandler AfterAssetLoaded - { - add - { - ContentEvents.RaiseContentExperimentalWarning(); - ContentEvents._AfterAssetLoaded += value; - } - remove => ContentEvents._AfterAssetLoaded -= value; - } - /********* ** Internal methods *********/ - /// Injects types required for backwards compatibility. - /// Tracks the installed mods. - /// Encapsulates monitoring and logging. - internal static void Shim(ModRegistry modRegistry, IMonitor monitor) - { - ContentEvents.ModRegistry = modRegistry; - ContentEvents.Monitor = monitor; - } - /// Raise an event. /// Encapsulates monitoring and logging. /// The previous locale. @@ -67,28 +25,5 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterLocaleChanged)}", ContentEvents.AfterLocaleChanged?.GetInvocationList(), null, new EventArgsValueChanged(oldLocale, newLocale)); } - - /// Raise an event. - /// Encapsulates monitoring and logging. - /// Encapsulates access and changes to content being read from a data file. - internal static void InvokeAfterAssetLoaded(IMonitor monitor, IContentEventHelper contentHelper) - { - monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterAssetLoaded)}", ContentEvents._AfterAssetLoaded?.GetInvocationList(), null, contentHelper); - } - - - /********* - ** Private methods - *********/ - /// Raise an 'experimental API' warning for a mod using the content API. - private static void RaiseContentExperimentalWarning() - { - string modName = ContentEvents.ModRegistry.GetModFromStack() ?? "An unknown mod"; - if (!ContentEvents.WarnedMods.Contains(modName)) - { - ContentEvents.WarnedMods.Add(modName); - ContentEvents.Monitor.Log($"{modName} used the undocumented and experimental content API, which may change or be removed without warning.", LogLevel.Warn); - } - } } } diff --git a/src/StardewModdingAPI/Framework/Content/AssetData.cs b/src/StardewModdingAPI/Framework/Content/AssetData.cs new file mode 100644 index 00000000..1ab9eebd --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetData.cs @@ -0,0 +1,44 @@ +using System; + +namespace StardewModdingAPI.Framework.Content +{ + /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. + /// The interface value type. + internal class AssetData : AssetInfo, IAssetData + { + /********* + ** Accessors + *********/ + /// The content data being read. + public TValue Data { get; protected set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetData(string locale, string assetName, TValue data, Func getNormalisedPath) + : base(locale, assetName, data.GetType(), getNormalisedPath) + { + this.Data = data; + } + + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + public void ReplaceWith(TValue value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); + if (!this.DataType.IsInstanceOfType(value)) + throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); + + this.Data = value; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs new file mode 100644 index 00000000..e9b29b12 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForDictionary : AssetData>, IAssetDataForDictionary + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + public void Set(TKey key, TValue value) + { + this.Data[key] = value; + } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + public void Set(TKey key, Func value) + { + this.Data[key] = value(this.Data[key]); + } + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + public void Set(Func replacer) + { + foreach (var pair in this.Data.ToArray()) + this.Data[pair.Key] = replacer(pair.Key, pair.Value); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs new file mode 100644 index 00000000..45c5588b --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForImage : AssetData, IAssetDataForImage + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + // get texture + Texture2D target = this.Data; + + // get areas + sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); + targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + + // validate + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); + if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // get source data + int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; + Color[] sourceData = new Color[pixelCount]; + source.GetData(0, sourceArea, sourceData, 0, pixelCount); + + // merge data in overlay mode + if (patchMode == PatchMode.Overlay) + { + Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; + target.GetData(0, targetArea, newData, 0, newData.Length); + for (int i = 0; i < sourceData.Length; i++) + { + Color pixel = sourceData[i]; + if (pixel.A != 0) // not transparent + newData[i] = pixel; + } + sourceData = newData; + } + + // patch target texture + target.SetData(0, targetArea, sourceData, 0, pixelCount); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs new file mode 100644 index 00000000..af2f54ae --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to content being read from a data file. + internal class AssetDataForObject : AssetData, IAssetData + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary balue. + /// The content being read isn't a dictionary. + public IAssetDataForDictionary AsDictionary() + { + return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath); + } + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + public IAssetDataForImage AsImage() + { + return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath); + } + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + public TData GetData() + { + if (!(this.Data is TData)) + throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); + return (TData)this.Data; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs new file mode 100644 index 00000000..08bc3a03 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + internal class AssetInfo : IAssetInfo + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + public string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + public string AssetName { get; } + + /// The content data type. + public Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content type being read. + /// Normalises an asset key to match the cache key. + public AssetInfo(string locale, string assetName, Type type, Func getNormalisedPath) + { + this.Locale = locale; + this.AssetName = assetName; + this.DataType = type; + this.GetNormalisedPath = getNormalisedPath; + } + + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + public bool IsAssetName(string path) + { + path = this.GetNormalisedPath(path); + return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); + } + + + /********* + ** Protected methods + *********/ + /// Get a human-readable type name. + /// The type to name. + protected string GetFriendlyTypeName(Type type) + { + // dictionary + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type[] genericArgs = type.GetGenericArguments(); + return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; + } + + // texture + if (type == typeof(Texture2D)) + return type.Name; + + // native type + if (type == typeof(int)) + return "int"; + if (type == typeof(string)) + return "string"; + + // default + return type.FullName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventData.cs b/src/StardewModdingAPI/Framework/Content/ContentEventData.cs deleted file mode 100644 index 1a1779d4..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventData.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. - /// The interface value type. - internal class ContentEventData : EventArgs, IContentEventData - { - /********* - ** Properties - *********/ - /// Normalises an asset key to match the cache key. - protected readonly Func GetNormalisedPath; - - - /********* - ** Accessors - *********/ - /// The content's locale code, if the content is localised. - public string Locale { get; } - - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. - public string AssetName { get; } - - /// The content data being read. - public TValue Data { get; protected set; } - - /// The content data type. - public Type DataType { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventData(string locale, string assetName, TValue data, Func getNormalisedPath) - : this(locale, assetName, data, data.GetType(), getNormalisedPath) { } - - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// The content data type being read. - /// Normalises an asset key to match the cache key. - public ContentEventData(string locale, string assetName, TValue data, Type dataType, Func getNormalisedPath) - { - this.Locale = locale; - this.AssetName = assetName; - this.Data = data; - this.DataType = dataType; - this.GetNormalisedPath = getNormalisedPath; - } - - /// Get whether the asset name being loaded matches a given name after normalisation. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - public bool IsAssetName(string path) - { - path = this.GetNormalisedPath(path); - return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); - } - - /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. - /// The new content value. - /// The is null. - /// The 's type is not compatible with the loaded asset's type. - public void ReplaceWith(TValue value) - { - if (value == null) - throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); - if (!this.DataType.IsInstanceOfType(value)) - throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); - - this.Data = value; - } - - - /********* - ** Protected methods - *********/ - /// Get a human-readable type name. - /// The type to name. - protected string GetFriendlyTypeName(Type type) - { - // dictionary - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - { - Type[] genericArgs = type.GetGenericArguments(); - return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; - } - - // texture - if (type == typeof(Texture2D)) - return type.Name; - - // native type - if (type == typeof(int)) - return "int"; - if (type == typeof(string)) - return "string"; - - // default - return type.FullName; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs deleted file mode 100644 index 9bf1ea17..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to content being read from a data file. - internal class ContentEventHelper : ContentEventData, IContentEventHelper - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventHelper(string locale, string assetName, object data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Get a helper to manipulate the data as a dictionary. - /// The expected dictionary key. - /// The expected dictionary balue. - /// The content being read isn't a dictionary. - public IContentEventHelperForDictionary AsDictionary() - { - return new ContentEventHelperForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath); - } - - /// Get a helper to manipulate the data as an image. - /// The content being read isn't an image. - public IContentEventHelperForImage AsImage() - { - return new ContentEventHelperForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath); - } - - /// Get the data as a given type. - /// The expected data type. - /// The data can't be converted to . - public TData GetData() - { - if (!(this.Data is TData)) - throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); - return (TData)this.Data; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs deleted file mode 100644 index 26f059e4..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - internal class ContentEventHelperForDictionary : ContentEventData>, IContentEventHelperForDictionary - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventHelperForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - public void Set(TKey key, TValue value) - { - this.Data[key] = value; - } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - public void Set(TKey key, Func value) - { - this.Data[key] = value(this.Data[key]); - } - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - public void Set(Func replacer) - { - foreach (var pair in this.Data.ToArray()) - this.Data[pair.Key] = replacer(pair.Key, pair.Value); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs deleted file mode 100644 index da30590b..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - internal class ContentEventHelperForImage : ContentEventData, IContentEventHelperForImage - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventHelperForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Overwrite part of the image. - /// The image to patch into the content. - /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. - /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. - /// Indicates how an image should be patched. - /// One of the arguments is null. - /// The is outside the bounds of the spritesheet. - public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) - { - // get texture - Texture2D target = this.Data; - - // get areas - sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); - targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); - - // validate - if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); - if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) - throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) - throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) - throw new InvalidOperationException("The source and target areas must be the same size."); - - // get source data - int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; - Color[] sourceData = new Color[pixelCount]; - source.GetData(0, sourceArea, sourceData, 0, pixelCount); - - // merge data in overlay mode - if (patchMode == PatchMode.Overlay) - { - Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; - target.GetData(0, targetArea, newData, 0, newData.Length); - for (int i = 0; i < sourceData.Length; i++) - { - Color pixel = sourceData[i]; - if (pixel.A != 0) // not transparent - newData[i] = pixel; - } - sourceData = newData; - } - - // patch target texture - target.SetData(0, targetArea, sourceData, 0, pixelCount); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 7fd5e803..f4b541e9 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -32,6 +33,13 @@ namespace StardewModdingAPI.Framework private readonly string ModName; + /********* + ** Accessors + *********/ + /// Editors which change content assets after they're loaded. + internal IList AssetEditors { get; } = new List(); + + /********* ** Public methods *********/ diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index acd3e108..38457862 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -42,6 +40,9 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// Implementations which change assets after they're loaded. + internal IDictionary> Editors { get; } = new Dictionary>(); + /// The absolute path to the . public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); @@ -49,13 +50,6 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ - /// Construct an instance. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// Encapsulates monitoring and logging. - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, IMonitor monitor) - : this(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, null, monitor) { } - /// Construct an instance. /// The service provider to use to locate services. /// The root directory to search for content. @@ -66,8 +60,8 @@ namespace StardewModdingAPI.Framework : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { // initialise - this.Monitor = monitor; IReflectionHelper reflection = new ReflectionHelper(); + this.Monitor = monitor; // get underlying fields for interception this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); @@ -125,14 +119,20 @@ namespace StardewModdingAPI.Framework if (this.IsNormalisedKeyLoaded(assetName)) return base.Load(assetName); - // load data - T data = base.Load(assetName); - // let mods intercept content - IContentEventHelper helper = new ContentEventHelper(cacheLocale, assetName, data, this.NormaliseAssetName); - ContentEvents.InvokeAfterAssetLoaded(this.Monitor, helper); - this.Cache[assetName] = helper.Data; - return (T)helper.Data; + IAssetInfo info = new AssetInfo(cacheLocale, assetName, typeof(T), this.NormaliseAssetName); + Lazy data = new Lazy(() => new AssetDataForObject(info.Locale, info.AssetName, base.Load(assetName), this.NormaliseAssetName)); + if (this.TryOverrideAssetLoad(info, data, out T result)) + { + if (result == null) + throw new InvalidCastException($"Can't override asset '{assetName}' with a null value."); + + this.Cache[assetName] = result; + return result; + } + + // fallback to default behavior + return base.Load(assetName); } /// Inject an asset into the cache. @@ -171,5 +171,35 @@ namespace StardewModdingAPI.Framework ? locale : null; } + + /// Try to override an asset being loaded. + /// The asset type. + /// The asset metadata. + /// The loaded asset data. + /// The asset to use instead. + /// Returns whether the asset should be overridden by . + private bool TryOverrideAssetLoad(IAssetInfo info, Lazy data, out T result) + { + bool edited = false; + + // apply editors + foreach (var modEditors in this.Editors) + { + IModMetadata mod = modEditors.Key; + foreach (IAssetEditor editor in modEditors.Value) + { + if (!editor.CanEdit(info)) + continue; + + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + editor.Edit(data.Value); + edited = true; + } + } + + // return result + result = edited ? (T)data.Value.Data : default(T); + return edited; + } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 602a522b..e4c2a233 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -29,6 +30,9 @@ namespace StardewModdingAPI.Framework /**** ** SMAPI state ****/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -48,8 +52,6 @@ namespace StardewModdingAPI.Framework /// Whether the game's zoom level is at 100% (i.e. nothing should be scaled). public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f); - /// Encapsulates monitoring and logging. - private readonly IMonitor Monitor; /**** ** Game state @@ -189,7 +191,7 @@ namespace StardewModdingAPI.Framework // 2. Since Game1.content isn't initialised yet, and we need one main instance to // support custom map tilesheets, detect when Game1.content is being initialised // and use the same instance. - this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); + this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); } /**** @@ -206,7 +208,7 @@ namespace StardewModdingAPI.Framework return mainContentManager; // build new instance - return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); + return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); } /// The method called when the game is updating its state. This happens roughly 60 times per second. diff --git a/src/StardewModdingAPI/IAssetData.cs b/src/StardewModdingAPI/IAssetData.cs new file mode 100644 index 00000000..c3021144 --- /dev/null +++ b/src/StardewModdingAPI/IAssetData.cs @@ -0,0 +1,47 @@ +using System; + +namespace StardewModdingAPI +{ + /// Generic metadata and methods for a content asset being loaded. + /// The expected data type. + public interface IAssetData : IAssetInfo + { + /********* + ** Accessors + *********/ + /// The content data being read. + TValue Data { get; } + + + /********* + ** Public methods + *********/ + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + void ReplaceWith(TValue value); + } + + /// Generic metadata and methods for a content asset being loaded. + public interface IAssetData : IAssetData + { + /********* + ** Public methods + *********/ + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary value. + /// The content being read isn't a dictionary. + IAssetDataForDictionary AsDictionary(); + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + IAssetDataForImage AsImage(); + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + TData GetData(); + } +} diff --git a/src/StardewModdingAPI/IAssetDataForDictionary.cs b/src/StardewModdingAPI/IAssetDataForDictionary.cs new file mode 100644 index 00000000..53c24346 --- /dev/null +++ b/src/StardewModdingAPI/IAssetDataForDictionary.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + public interface IAssetDataForDictionary : IAssetData> + { + /********* + ** Public methods + *********/ + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + void Set(TKey key, TValue value); + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + void Set(TKey key, Func value); + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + void Set(Func replacer); + } +} diff --git a/src/StardewModdingAPI/IAssetDataForImage.cs b/src/StardewModdingAPI/IAssetDataForImage.cs new file mode 100644 index 00000000..4584a20e --- /dev/null +++ b/src/StardewModdingAPI/IAssetDataForImage.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + public interface IAssetDataForImage : IAssetData + { + /********* + ** Public methods + *********/ + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + /// The content being read isn't an image. + void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); + } +} diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/StardewModdingAPI/IAssetEditor.cs new file mode 100644 index 00000000..b66ec15e --- /dev/null +++ b/src/StardewModdingAPI/IAssetEditor.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI +{ + /// Edits a loaded content asset. + public interface IAssetEditor + { + /********* + ** Public methods + *********/ + /// Get whether this instance can edit the given asset. + /// Basic metadata about the asset being loaded. + bool CanEdit(IAssetInfo asset); + + /// Edit a matched asset. + /// A helper which encapsulates metadata about an asset and enables changes to it. + void Edit(IAssetData asset); + } +} diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/StardewModdingAPI/IAssetInfo.cs new file mode 100644 index 00000000..dc65a750 --- /dev/null +++ b/src/StardewModdingAPI/IAssetInfo.cs @@ -0,0 +1,28 @@ +using System; + +namespace StardewModdingAPI +{ + /// Basic metadata for a content asset. + public interface IAssetInfo + { + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + string AssetName { get; } + + /// The content data type. + Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + bool IsAssetName(string path); + } +} diff --git a/src/StardewModdingAPI/IContentEventData.cs b/src/StardewModdingAPI/IContentEventData.cs deleted file mode 100644 index 7e2d4df1..00000000 --- a/src/StardewModdingAPI/IContentEventData.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// Generic metadata and methods for a content asset being loaded. - /// The expected data type. - public interface IContentEventData - { - /********* - ** Accessors - *********/ - /// The content's locale code, if the content is localised. - string Locale { get; } - - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. - string AssetName { get; } - - /// The content data being read. - TValue Data { get; } - - /// The content data type. - Type DataType { get; } - - - /********* - ** Public methods - *********/ - /// Get whether the asset name being loaded matches a given name after normalisation. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - bool IsAssetName(string path); - - /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. - /// The new content value. - /// The is null. - /// The 's type is not compatible with the loaded asset's type. - void ReplaceWith(TValue value); - } -} diff --git a/src/StardewModdingAPI/IContentEventHelper.cs b/src/StardewModdingAPI/IContentEventHelper.cs deleted file mode 100644 index 421a1e06..00000000 --- a/src/StardewModdingAPI/IContentEventHelper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// Encapsulates access and changes to content being read from a data file. - public interface IContentEventHelper : IContentEventData - { - /********* - ** Public methods - *********/ - /// Get a helper to manipulate the data as a dictionary. - /// The expected dictionary key. - /// The expected dictionary balue. - /// The content being read isn't a dictionary. - IContentEventHelperForDictionary AsDictionary(); - - /// Get a helper to manipulate the data as an image. - /// The content being read isn't an image. - IContentEventHelperForImage AsImage(); - - /// Get the data as a given type. - /// The expected data type. - /// The data can't be converted to . - TData GetData(); - } -} diff --git a/src/StardewModdingAPI/IContentEventHelperForDictionary.cs b/src/StardewModdingAPI/IContentEventHelperForDictionary.cs deleted file mode 100644 index 2f9d5a65..00000000 --- a/src/StardewModdingAPI/IContentEventHelperForDictionary.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - public interface IContentEventHelperForDictionary : IContentEventData> - { - /********* - ** Public methods - *********/ - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - void Set(TKey key, TValue value); - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - void Set(TKey key, Func value); - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - void Set(Func replacer); - } -} diff --git a/src/StardewModdingAPI/IContentEventHelperForImage.cs b/src/StardewModdingAPI/IContentEventHelperForImage.cs deleted file mode 100644 index 1158c868..00000000 --- a/src/StardewModdingAPI/IContentEventHelperForImage.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - public interface IContentEventHelperForImage : IContentEventData - { - /********* - ** Public methods - *********/ - /// Overwrite part of the image. - /// The image to patch into the content. - /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. - /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. - /// Indicates how an image should be patched. - /// One of the arguments is null. - /// The is outside the bounds of the spritesheet. - /// The content being read isn't an image. - void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); - } -} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 7b843748..4fbd35dc 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -330,7 +330,6 @@ namespace StardewModdingAPI Config.Shim(this.DeprecationManager); Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); Mod.Shim(this.DeprecationManager); - ContentEvents.Shim(this.ModRegistry, this.Monitor); GameEvents.Shim(this.DeprecationManager); PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); @@ -489,7 +488,8 @@ namespace StardewModdingAPI this.Monitor.Log("Detecting common issues...", LogLevel.Trace); bool issuesFound = false; - // object format (commonly broken by outdated mods) + + // object format (commonly broken by outdated files) { // detect issues bool hasObjectIssues = false; @@ -689,11 +689,14 @@ namespace StardewModdingAPI // initialise loaded mods foreach (IModMetadata metadata in this.ModRegistry.GetMods()) { + // add interceptors + if (metadata.Mod.Helper.Content is ContentHelper helper) + this.ContentManager.Editors[metadata] = helper.AssetEditors; + + // call entry method try { IMod mod = metadata.Mod; - - // call entry methods (mod as Mod)?.Entry(); // deprecated since 1.0 mod.Entry(mod.Helper); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 77d3b12b..1f2bd4bb 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -97,6 +97,7 @@ + @@ -136,10 +137,10 @@ - - - - + + + + @@ -156,11 +157,12 @@ + + - - - - + + + -- cgit From 271843d8614b916aa69273b778971cff0a02ce0d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Jun 2017 22:10:59 -0400 Subject: tweak asset interception code to simplify future work (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 60 ++++++++-------------- 1 file changed, 22 insertions(+), 38 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 38457862..d269cafa 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -111,28 +111,16 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) { - // get normalised metadata assetName = this.NormaliseAssetName(assetName); - string cacheLocale = this.GetCacheLocale(assetName); // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) return base.Load(assetName); - // let mods intercept content - IAssetInfo info = new AssetInfo(cacheLocale, assetName, typeof(T), this.NormaliseAssetName); - Lazy data = new Lazy(() => new AssetDataForObject(info.Locale, info.AssetName, base.Load(assetName), this.NormaliseAssetName)); - if (this.TryOverrideAssetLoad(info, data, out T result)) - { - if (result == null) - throw new InvalidCastException($"Can't override asset '{assetName}' with a null value."); - - this.Cache[assetName] = result; - return result; - } - - // fallback to default behavior - return base.Load(assetName); + // load asset + T asset = this.GetAssetWithInterceptors(this.GetLocale(), assetName, () => base.Load(assetName)); + this.Cache[assetName] = asset; + return asset; } /// Inject an asset into the cache. @@ -162,27 +150,21 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } - /// Get the locale for which the asset name was saved, if any. - /// The normalised asset name. - private string GetCacheLocale(string normalisedAssetName) - { - string locale = this.GetKeyLocale.Invoke(); - return this.Cache.ContainsKey($"{normalisedAssetName}.{locale}") - ? locale - : null; - } - - /// Try to override an asset being loaded. + /// Read an asset with support for asset interceptors. /// The asset type. - /// The asset metadata. - /// The loaded asset data. - /// The asset to use instead. - /// Returns whether the asset should be overridden by . - private bool TryOverrideAssetLoad(IAssetInfo info, Lazy data, out T result) + /// The current content locale. + /// The normalised asset path relative to the loader root directory, not including the .xnb extension. + /// Get the asset from the underlying content manager. + private T GetAssetWithInterceptors(string locale, string normalisedKey, Func getData) { - bool edited = false; + // get metadata + IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName); + + // load asset + T asset = getData(); - // apply editors + // edit asset + IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); foreach (var modEditors in this.Editors) { IModMetadata mod = modEditors.Key; @@ -192,14 +174,16 @@ namespace StardewModdingAPI.Framework continue; this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - editor.Edit(data.Value); - edited = true; + editor.Edit(data); + if (data.Data == null) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); + if (!(data.Data is T)) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); } } // return result - result = edited ? (T)data.Value.Data : default(T); - return edited; + return (T)data.Data; } } } -- cgit From 4568f2259ba6a0808658229122daa6ff6335a4fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 19:35:24 -0400 Subject: ensure there's only one content manager instance (#255) --- src/StardewModdingAPI/Framework/SGame.cs | 38 +++++++++++++++++++------------- 1 file changed, 23 insertions(+), 15 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index e4c2a233..80ae20ac 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// SMAPI's content manager. + private SContentManager SContentManager; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -177,21 +180,20 @@ namespace StardewModdingAPI.Framework /// Simplifies access to private game code. internal SGame(IMonitor monitor, IReflectionHelper reflection) { + // initialise this.Monitor = monitor; this.FirstUpdate = true; SGame.Instance = this; SGame.Reflection = reflection; - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // required by Stardew Valley + // set XNA option required by Stardew Valley + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - // The game uses the default content manager instead of Game1.CreateContentManager in - // several cases (See http://community.playstarbound.com/threads/130058/page-27#post-3159274). - // The workaround is... - // 1. Override the default content manager. - // 2. Since Game1.content isn't initialised yet, and we need one main instance to - // support custom map tilesheets, detect when Game1.content is being initialised - // and use the same instance. - this.Content = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + // override content manager + this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); + this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + this.Content = this.SContentManager; + Game1.content = this.SContentManager; } /**** @@ -202,13 +204,19 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // When Game1.content is being initialised, use SMAPI's main content manager instance. - // See comment in SGame constructor. - if (Game1.content == null && this.Content is SContentManager mainContentManager) - return mainContentManager; + // return default if SMAPI's content manager isn't initialised yet + if (this.SContentManager == null) + { + this.Monitor?.Log("SMAPI's content manager isn't initialised; skipping content manager interception.", LogLevel.Trace); + return base.CreateContentManager(serviceProvider, rootDirectory); + } - // build new instance - return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); + // 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 this.SContentManager; } /// The method called when the game is updating its state. This happens roughly 60 times per second. -- cgit From 3b6adf3c2676fa8f73997f9c1f8ec5f727f73690 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 19:39:04 -0400 Subject: reset asset cache when a new interceptor is added (#255) This lets new interceptors edit assets loaded before they were added, particularly assets loaded before mods are initialised. --- src/StardewModdingAPI/Framework/ContentHelper.cs | 6 ++- src/StardewModdingAPI/Framework/SContentManager.cs | 59 ++++++++++++++++++++++ src/StardewModdingAPI/Program.cs | 16 +++++- 3 files changed, 79 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index f4b541e9..b7773d6a 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -36,8 +37,11 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// The observable implementation of . + internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); + /// Editors which change content assets after they're loaded. - internal IList AssetEditors { get; } = new List(); + internal IList AssetEditors => this.ObservableAssetEditors; /********* diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index d269cafa..24585963 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -5,10 +5,14 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Objects; +using StardewValley.Projectiles; namespace StardewModdingAPI.Framework { @@ -59,6 +63,10 @@ namespace StardewModdingAPI.Framework public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { + // validate + if (monitor == null) + throw new ArgumentNullException(nameof(monitor)); + // initialise IReflectionHelper reflection = new ReflectionHelper(); this.Monitor = monitor; @@ -130,6 +138,7 @@ namespace StardewModdingAPI.Framework public void Inject(string assetName, T value) { assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; } @@ -139,6 +148,56 @@ namespace StardewModdingAPI.Framework return this.GetKeyLocale.Invoke(); } + /// Reset the asset cache and reload the game's static assets. + /// This implementation is derived from . + public void Reset() + { + this.Monitor.Log("Resetting asset cache...", LogLevel.Trace); + this.Cache.Clear(); + + // from Game1.LoadContent + Game1.daybg = this.Load("LooseSprites\\daybg"); + Game1.nightbg = this.Load("LooseSprites\\nightbg"); + Game1.menuTexture = this.Load("Maps\\MenuTiles"); + Game1.lantern = this.Load("LooseSprites\\Lighting\\lantern"); + Game1.windowLight = this.Load("LooseSprites\\Lighting\\windowLight"); + Game1.sconceLight = this.Load("LooseSprites\\Lighting\\sconceLight"); + Game1.cauldronLight = this.Load("LooseSprites\\Lighting\\greenLight"); + Game1.indoorWindowLight = this.Load("LooseSprites\\Lighting\\indoorWindowLight"); + Game1.shadowTexture = this.Load("LooseSprites\\shadow"); + Game1.mouseCursors = this.Load("LooseSprites\\Cursors"); + Game1.controllerMaps = this.Load("LooseSprites\\ControllerMaps"); + Game1.animations = this.Load("TileSheets\\animations"); + Game1.achievements = this.Load>("Data\\Achievements"); + Game1.NPCGiftTastes = this.Load>("Data\\NPCGiftTastes"); + Game1.dialogueFont = this.Load("Fonts\\SpriteFont1"); + Game1.smallFont = this.Load("Fonts\\SmallFont"); + Game1.tinyFont = this.Load("Fonts\\tinyFont"); + Game1.tinyFontBorder = this.Load("Fonts\\tinyFontBorder"); + Game1.objectSpriteSheet = this.Load("Maps\\springobjects"); + Game1.cropSpriteSheet = this.Load("TileSheets\\crops"); + Game1.emoteSpriteSheet = this.Load("TileSheets\\emotes"); + Game1.debrisSpriteSheet = this.Load("TileSheets\\debris"); + Game1.bigCraftableSpriteSheet = this.Load("TileSheets\\Craftables"); + Game1.rainTexture = this.Load("TileSheets\\rain"); + Game1.buffsIcons = this.Load("TileSheets\\BuffsIcons"); + Game1.objectInformation = this.Load>("Data\\ObjectInformation"); + Game1.bigCraftablesInformation = this.Load>("Data\\BigCraftablesInformation"); + FarmerRenderer.hairStylesTexture = this.Load("Characters\\Farmer\\hairstyles"); + FarmerRenderer.shirtsTexture = this.Load("Characters\\Farmer\\shirts"); + FarmerRenderer.hatsTexture = this.Load("Characters\\Farmer\\hats"); + FarmerRenderer.accessoriesTexture = this.Load("Characters\\Farmer\\accessories"); + Furniture.furnitureTexture = this.Load("TileSheets\\furniture"); + SpriteText.spriteTexture = this.Load("LooseSprites\\font_bold"); + SpriteText.coloredTexture = this.Load("LooseSprites\\font_colored"); + Tool.weaponsTexture = this.Load("TileSheets\\weapons"); + Projectile.projectileSheet = this.Load("TileSheets\\Projectiles"); + + // from Farmer constructor + if (Game1.player != null) + Game1.player.FarmerRenderer = new FarmerRenderer(this.Load($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + } + /********* ** Private methods *********/ diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 98de4608..53efe1e3 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -708,7 +708,7 @@ namespace StardewModdingAPI { // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) - this.ContentManager.Editors[metadata] = helper.AssetEditors; + this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; // call entry method try @@ -727,6 +727,20 @@ namespace StardewModdingAPI } } + // reset cache when needed + // only register listeners after Entry to avoid repeatedly reloading assets during load + foreach (IModMetadata metadata in loadedMods) + { + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => + { + if (e.NewItems.Count > 0) + this.ContentManager.Reset(); + }; + } + } + this.ContentManager.Reset(); } /// Reload translations for all mods. -- cgit From 306427786b9ae349e3f33ca2e4be6a79b63cf6ce Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 19:55:08 -0400 Subject: let mods implement IAssetEditor for simple cases (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 46 +++++++++++++++------- 1 file changed, 32 insertions(+), 14 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 24585963..1ee1eae6 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -224,25 +224,43 @@ namespace StardewModdingAPI.Framework // edit asset IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); - foreach (var modEditors in this.Editors) + foreach (var entry in this.GetAssetEditors()) { - IModMetadata mod = modEditors.Key; - foreach (IAssetEditor editor in modEditors.Value) - { - if (!editor.CanEdit(info)) - continue; - - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - editor.Edit(data); - if (data.Data == null) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); - if (!(data.Data is T)) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); - } + IModMetadata mod = entry.Mod; + IAssetEditor editor = entry.Editor; + + if (!editor.CanEdit(info)) + continue; + + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + editor.Edit(data); + if (data.Data == null) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); + if (!(data.Data is T)) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); } // return result return (T)data.Data; } + + /// Get all registered asset editors. + private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors() + { + foreach (var entry in this.Editors) + { + IModMetadata metadata = entry.Key; + IList editors = entry.Value; + + // special case if mod implements interface + // ReSharper disable once SuspiciousTypeConversion.Global + if (metadata.Mod is IAssetEditor modAsEditor) + yield return (metadata, modAsEditor); + + // registered editors + foreach (IAssetEditor editor in editors) + yield return (metadata, editor); + } + } } } -- cgit From 600ef562861fe306390b78ee8f08036f0872e92c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 21:31:21 -0400 Subject: improve error handling when mods set invalid asset value (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 30 +++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 1ee1eae6..53afb729 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -219,31 +219,47 @@ namespace StardewModdingAPI.Framework // get metadata IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName); - // load asset - T asset = getData(); // edit asset - IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); + IAssetData data = this.GetAssetData(info, getData()); foreach (var entry in this.GetAssetEditors()) { + // check for match IModMetadata mod = entry.Mod; IAssetEditor editor = entry.Editor; - if (!editor.CanEdit(info)) continue; + // try edit this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + object prevAsset = data.Data; editor.Edit(data); + + // validate edit if (data.Data == null) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); - if (!(data.Data is T)) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); + { + data = this.GetAssetData(info, prevAsset); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value; ignoring override.", LogLevel.Warn); + } + else if (!(data.Data is T)) + { + data = this.GetAssetData(info, prevAsset); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + } } // return result return (T)data.Data; } + /// Get an asset edit helper. + /// The asset info. + /// The loaded asset data. + private IAssetData GetAssetData(IAssetInfo info, object asset) + { + return new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); + } + /// Get all registered asset editors. private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors() { -- cgit From f95c7e8d72014f8008886031cebf7b12aeb7ed46 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 23:13:43 -0400 Subject: add support for asset loaders (#255) --- .../Framework/Content/AssetDataForObject.cs | 7 + src/StardewModdingAPI/Framework/ContentHelper.cs | 8 +- src/StardewModdingAPI/Framework/SContentManager.cs | 159 +++++++++++++++------ src/StardewModdingAPI/IAssetEditor.cs | 2 +- src/StardewModdingAPI/IAssetLoader.cs | 17 +++ src/StardewModdingAPI/Program.cs | 8 ++ src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 156 insertions(+), 46 deletions(-) create mode 100644 src/StardewModdingAPI/IAssetLoader.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs index af2f54ae..f30003e4 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs @@ -18,6 +18,13 @@ namespace StardewModdingAPI.Framework.Content public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) : base(locale, assetName, data, getNormalisedPath) { } + /// Construct an instance. + /// The asset metadata. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(IAssetInfo info, object data, Func getNormalisedPath) + : this(info.Locale, info.AssetName, data, getNormalisedPath) { } + /// Get a helper to manipulate the data as a dictionary. /// The expected dictionary key. /// The expected dictionary balue. diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index b7773d6a..0c09fe94 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -40,7 +40,13 @@ namespace StardewModdingAPI.Framework /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); - /// Editors which change content assets after they're loaded. + /// The observable implementation of . + internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); + + /// Interceptors which provide the initial versions of matching content assets. + internal IList AssetLoaders => this.ObservableAssetLoaders; + + /// Interceptors which edit matching content assets after they're loaded. internal IList AssetEditors => this.ObservableAssetEditors; diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 53afb729..0a8a0873 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -44,7 +44,10 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// Implementations which change assets after they're loaded. + /// Interceptors which provide the initial versions of matching assets. + internal IDictionary> Loaders { get; } = new Dictionary>(); + + /// Interceptors which edit matching assets after they're loaded. internal IDictionary> Editors { get; } = new Dictionary>(); /// The absolute path to the . @@ -126,9 +129,17 @@ namespace StardewModdingAPI.Framework return base.Load(assetName); // load asset - T asset = this.GetAssetWithInterceptors(this.GetLocale(), assetName, () => base.Load(assetName)); - this.Cache[assetName] = asset; - return asset; + T data; + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + data = (T)asset.Data; + } + + // update cache & return data + this.Cache[assetName] = data; + return data; } /// Inject an asset into the cache. @@ -198,6 +209,7 @@ namespace StardewModdingAPI.Framework Game1.player.FarmerRenderer = new FarmerRenderer(this.Load($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); } + /********* ** Private methods *********/ @@ -209,73 +221,132 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } - /// Read an asset with support for asset interceptors. - /// The asset type. - /// The current content locale. - /// The normalised asset path relative to the loader root directory, not including the .xnb extension. - /// Get the asset from the underlying content manager. - private T GetAssetWithInterceptors(string locale, string normalisedKey, Func getData) + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) { - // get metadata - IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName); + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Interceptor.CanLoad(info); + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); + this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + return null; + } + + // fetch asset from loader + IModMetadata mod = loaders[0].Mod; + IAssetLoader loader = loaders[0].Interceptor; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.NormaliseAssetName); + } + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); // edit asset - IAssetData data = this.GetAssetData(info, getData()); - foreach (var entry in this.GetAssetEditors()) + foreach (var entry in this.GetInterceptors(this.Editors)) { // check for match IModMetadata mod = entry.Mod; - IAssetEditor editor = entry.Editor; - if (!editor.CanEdit(info)) + IAssetEditor editor = entry.Interceptor; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; + } // try edit - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - object prevAsset = data.Data; - editor.Edit(data); + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } // validate edit - if (data.Data == null) + if (asset.Data == null) { - data = this.GetAssetData(info, prevAsset); - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value; ignoring override.", LogLevel.Warn); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); } - else if (!(data.Data is T)) + else if (!(asset.Data is T)) { - data = this.GetAssetData(info, prevAsset); - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); } } // return result - return (T)data.Data; - } - - /// Get an asset edit helper. - /// The asset info. - /// The loaded asset data. - private IAssetData GetAssetData(IAssetInfo info, object asset) - { - return new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); + return asset; } - /// Get all registered asset editors. - private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors() + /// Get all registered interceptors from a list. + private IEnumerable<(IModMetadata Mod, T Interceptor)> GetInterceptors(IDictionary> entries) { - foreach (var entry in this.Editors) + foreach (var entry in entries) { IModMetadata metadata = entry.Key; - IList editors = entry.Value; + IList interceptors = entry.Value; - // special case if mod implements interface - // ReSharper disable once SuspiciousTypeConversion.Global - if (metadata.Mod is IAssetEditor modAsEditor) - yield return (metadata, modAsEditor); + // special case if mod is an interceptor + if (metadata.Mod is T modAsInterceptor) + yield return (metadata, modAsInterceptor); // registered editors - foreach (IAssetEditor editor in editors) - yield return (metadata, editor); + foreach (T interceptor in interceptors) + yield return (metadata, interceptor); } } } diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/StardewModdingAPI/IAssetEditor.cs index b66ec15e..d2c6f295 100644 --- a/src/StardewModdingAPI/IAssetEditor.cs +++ b/src/StardewModdingAPI/IAssetEditor.cs @@ -1,6 +1,6 @@ namespace StardewModdingAPI { - /// Edits a loaded content asset. + /// Edits matching content assets. public interface IAssetEditor { /********* diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/StardewModdingAPI/IAssetLoader.cs new file mode 100644 index 00000000..ad97b941 --- /dev/null +++ b/src/StardewModdingAPI/IAssetLoader.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI +{ + /// Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use instead. + public interface IAssetLoader + { + /********* + ** Public methods + *********/ + /// Get whether this instance can load the initial version of the given asset. + /// Basic metadata about the asset being loaded. + bool CanLoad(IAssetInfo asset); + + /// Load a matched asset. + /// Basic metadata about the asset being loaded. + T Load(IAssetInfo asset); + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 53efe1e3..483d2bc2 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -708,7 +708,10 @@ namespace StardewModdingAPI { // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) + { this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; + this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; + } // call entry method try @@ -738,6 +741,11 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) this.ContentManager.Reset(); }; + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => + { + if (e.NewItems.Count > 0) + this.ContentManager.Reset(); + }; } } this.ContentManager.Reset(); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 1f2bd4bb..4d65b1af 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -159,6 +159,7 @@ + -- cgit From e69d1615c4ff1cf93e51f83b66f7d32fe6baf942 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Jul 2017 19:32:40 -0400 Subject: throw more useful error when JSON file is invalid (#314) --- release-notes.md | 1 + .../Framework/Serialisation/JsonHelper.cs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 4b4a3447..ae2f853d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -21,6 +21,7 @@ For players: * `list_items` now shows all items in the game. You can search by item type like `list_items weapon`, or search by item name like `list_items galaxy sword`. * `list_items` now also matches translated item names when playing in another language. * `list_item_types` is a new command to see a list of item types. +* Added clearer error when a `config.json` is invalid. For modders: * You can now specify minimum dependency versions in `manifest.json`. diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs index 64d8738e..6431394c 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs @@ -51,7 +51,21 @@ namespace StardewModdingAPI.Framework.Serialisation } // deserialise model - return JsonConvert.DeserializeObject(json, this.JsonSettings); + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException ex) + { + string message = $"The file at {fullPath} doesn't seem to be valid JSON."; + + string text = File.ReadAllText(fullPath); + if (text.Contains("“") || text.Contains("”")) + message += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + + message += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(message); + } } /// Save to a JSON file. -- cgit From 698328c52f60e6f825086585ef79f8c6eedb944e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Jul 2017 19:42:05 -0400 Subject: fix rare crash for some players when window loses focus (#306) --- release-notes.md | 1 + src/StardewModdingAPI/Framework/SGame.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index ae2f853d..b1917ef7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -22,6 +22,7 @@ For players: * `list_items` now also matches translated item names when playing in another language. * `list_item_types` is a new command to see a list of item types. * Added clearer error when a `config.json` is invalid. +* Fixed rare crash when window loses focus for a few players (further to fix in 1.14). For modders: * You can now specify minimum dependency versions in `manifest.json`. diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 80ae20ac..39713d4a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -344,9 +344,21 @@ namespace StardewModdingAPI.Framework if (Game1.game1.IsActive) { // get latest state - KeyboardState keyState = Keyboard.GetState(); - MouseState mouseState = Mouse.GetState(); - Point mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); + KeyboardState keyState; + MouseState mouseState; + Point mousePosition; + try + { + keyState = Keyboard.GetState(); + mouseState = Mouse.GetState(); + mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); + } + catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + { + keyState = this.PreviousKeyState; + mouseState = this.PreviousMouseState; + mousePosition = this.PreviousMousePosition; + } // analyse state Keys[] currentlyPressedKeys = keyState.GetPressedKeys(); -- cgit From 6a628a4d8a21b98a55ff29065980fc818d4f39dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Jul 2017 21:24:32 -0400 Subject: simplify log timestamps in console (except in developer mode) --- release-notes.md | 2 +- src/StardewModdingAPI/Framework/Monitor.cs | 13 +++++++++---- src/StardewModdingAPI/Program.cs | 8 +++++++- 3 files changed, 17 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index b5a8a529..c5d0ccef 100644 --- a/release-notes.md +++ b/release-notes.md @@ -13,7 +13,7 @@ For mod developers: See [log](https://github.com/Pathoschild/SMAPI/compare/1.14...1.15). For players: -* Many changes to the SMAPI console to make it simpler for players. +* Several changes to the SMAPI console to make it simpler for players. * Revamped TrainerMod's item commands: * `player_add` is a new command which lets you add any game item to your inventory (including tools, weapons, equipment, craftables, wallpaper, etc). This replaces the former `player_additem`, `player_addring`, and `player_addweapon`. * `list_items` now shows all items in the game. You can search by item type like `list_items weapon`, or search by item name like `list_items galaxy sword`. diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 925efc33..b64b3b0b 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -45,6 +45,9 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; + /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. + internal bool ShowFullStampInConsole { get; set; } + /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } @@ -124,7 +127,9 @@ namespace StardewModdingAPI.Framework { // generate message string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); - message = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; + + string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; + string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) @@ -136,17 +141,17 @@ namespace StardewModdingAPI.Framework if (background.HasValue) Console.BackgroundColor = background.Value; Console.ForegroundColor = color; - Console.WriteLine(message); + Console.WriteLine(consoleMessage); Console.ResetColor(); } else - Console.WriteLine(message); + Console.WriteLine(consoleMessage); }); } // write to log file if (this.WriteToFile) - this.LogFile.WriteLine(message); + this.LogFile.WriteLine(fullMessage); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 70e53f5a..6a240a7b 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -346,6 +346,7 @@ namespace StardewModdingAPI if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; + this.Monitor.ShowFullStampInConsole = true; this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); } if (!this.Settings.CheckForUpdates) @@ -864,7 +865,12 @@ namespace StardewModdingAPI /// The name of the module which will log messages with this instance. private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode }; + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) + { + WriteToConsole = this.Monitor.WriteToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; } /// Get a human-readable name for the current platform. -- cgit From c9c354a66f3659bda9f1c8915ab61bc1a38412ef Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Jul 2017 21:36:04 -0400 Subject: slim down console output for players some more --- src/StardewModdingAPI/Framework/Monitor.cs | 9 +++++++++ src/StardewModdingAPI/Program.cs | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index b64b3b0b..7d40b72b 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -95,6 +95,15 @@ namespace StardewModdingAPI.Framework this.ExitTokenSource.Cancel(); } + /// Write a newline to the console and log file. + internal void Newline() + { + if (this.WriteToConsole) + this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); + if (this.WriteToFile) + this.LogFile.WriteLine(""); + } + /// Log a message for the player or developer, using the specified console color. /// The name of the mod logging the message. /// The message to log. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 6a240a7b..e960a684 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -126,7 +126,6 @@ namespace StardewModdingAPI // init logging this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); - this.Monitor.Log("Starting SMAPI..."); // validate paths this.VerifyPath(Constants.ModPath); @@ -210,7 +209,7 @@ namespace StardewModdingAPI } // start game - this.Monitor.Log("Starting game..."); + this.Monitor.Log("Starting game...", LogLevel.Trace); try { this.IsGameRunning = true; @@ -685,6 +684,7 @@ namespace StardewModdingAPI IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods + this.Monitor.Newline(); if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); @@ -695,6 +695,7 @@ namespace StardewModdingAPI this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); } + this.Monitor.Newline(); } // log loaded mods @@ -709,6 +710,7 @@ namespace StardewModdingAPI LogLevel.Info ); } + this.Monitor.Newline(); // initialise translations this.ReloadTranslations(); -- cgit From 771263299cae11d464c25c5291e59507c639e822 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 01:03:13 -0400 Subject: add SMAPI 2.0 compile mode --- README.md | 2 +- release-notes.md | 1 + .../Finders/PropertyFinder.cs | 83 ++++++++++++++++++++++ .../StardewModdingAPI.AssemblyRewriters.csproj | 1 + src/StardewModdingAPI/Command.cs | 4 +- src/StardewModdingAPI/Config.cs | 4 +- src/StardewModdingAPI/Constants.cs | 33 ++++++++- src/StardewModdingAPI/Events/EventArgsCommand.cs | 4 +- .../Events/EventArgsFarmerChanged.cs | 2 + .../Events/EventArgsLoadedGameChanged.cs | 4 +- src/StardewModdingAPI/Events/EventArgsNewDay.cs | 4 +- .../Events/EventArgsStringChanged.cs | 4 +- src/StardewModdingAPI/Events/GameEvents.cs | 22 ++++-- src/StardewModdingAPI/Events/PlayerEvents.cs | 6 ++ src/StardewModdingAPI/Events/TimeEvents.cs | 11 ++- .../Framework/ModLoading/ModResolver.cs | 22 +++++- src/StardewModdingAPI/Framework/Models/Manifest.cs | 2 + src/StardewModdingAPI/Framework/Monitor.cs | 2 + src/StardewModdingAPI/Framework/SContentManager.cs | 2 +- src/StardewModdingAPI/Framework/SGame.cs | 24 +++++-- src/StardewModdingAPI/Log.cs | 4 +- src/StardewModdingAPI/Mod.cs | 9 +++ src/StardewModdingAPI/Program.cs | 28 ++++++-- 23 files changed, 254 insertions(+), 24 deletions(-) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/README.md b/README.md index 395cd314..ca3128e4 100644 --- a/README.md +++ b/README.md @@ -167,4 +167,4 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. - +`SMAPI_2_0` | Sets SMAPI 2.0 mode, which enables features planned for SMAPI 2.0 and removes all deprecated code. This helps test how mods will work when SMAPI 2.0 is released. diff --git a/release-notes.md b/release-notes.md index 7a7045fc..94106ba6 100644 --- a/release-notes.md +++ b/release-notes.md @@ -36,6 +36,7 @@ For modders: * Fixed corrupted state exceptions not being logged by SMAPI. For SMAPI developers: +* Added SMAPI 2.0 compile mode, for testing how mods will work with SMAPI 2.0. * Added prototype SMAPI 2.0 feature to override XNB files (not enabled for mods yet). * Added prototype SMAPI 2.0 support for version strings in `manifest.json` (not recommended for mods yet). diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs new file mode 100644 index 00000000..441f15f2 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs @@ -0,0 +1,83 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.AssemblyRewriters.Finders +{ + /// Finds incompatible CIL instructions that reference a given property and throws an . + public class PropertyFinder : IInstructionRewriter + { + /********* + ** Properties + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The property name for which to find references. + private readonly string PropertyName; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The property name for which to find references. + /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). + public PropertyFinder(string fullTypeName, string propertyName, string nounPhrase = null) + { + this.FullTypeName = fullTypeName; + this.PropertyName = propertyName; + this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{propertyName} property"; + } + + /// Rewrite a method definition for compatibility. + /// The module being rewritten. + /// The method definition to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return false; + } + + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return false; + + throw new IncompatibleInstructionException(this.NounPhrase); + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && (methodRef.Name == "get_" + this.PropertyName || methodRef.Name == "set_" + this.PropertyName); + } + } +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index e25b201e..7a12a8e9 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -49,6 +49,7 @@ Properties\GlobalAssemblyInfo.cs + diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs index e2d08538..7613b240 100644 --- a/src/StardewModdingAPI/Command.cs +++ b/src/StardewModdingAPI/Command.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; using System.Collections.Generic; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; @@ -155,3 +156,4 @@ namespace StardewModdingAPI } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs index 9f4bfad2..f6fe37d9 100644 --- a/src/StardewModdingAPI/Config.cs +++ b/src/StardewModdingAPI/Config.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; using System.IO; using System.Linq; using Newtonsoft.Json; @@ -184,3 +185,4 @@ namespace StardewModdingAPI } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index bd489b29..06a8c486 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -33,7 +33,12 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 14, 1); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} + public static ISemanticVersion ApiVersion { get; } = +#if SMAPI_2_0 + new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); +#else + new SemanticVersion(1, 15, 0, "prerelease.1"); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} +#endif /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); @@ -169,6 +174,32 @@ namespace StardewModdingAPI new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), + // APIs removed in SMAPI 2.0 +#if SMAPI_2_0 + new TypeFinder("StardewModdingAPI.Command"), + new TypeFinder("StardewModdingAPI.Config"), + new TypeFinder("StardewModdingAPI.Log"), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), +#endif + /**** ** Rewriters change CIL as needed to fix incompatible code ****/ diff --git a/src/StardewModdingAPI/Events/EventArgsCommand.cs b/src/StardewModdingAPI/Events/EventArgsCommand.cs index 88a9e5a3..f0435904 100644 --- a/src/StardewModdingAPI/Events/EventArgsCommand.cs +++ b/src/StardewModdingAPI/Events/EventArgsCommand.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -24,3 +25,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs index 699d90be..c34fc4ab 100644 --- a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs @@ -1,3 +1,4 @@ +#if !SMAPI_2_0 using System; using SFarmer = StardewValley.Farmer; @@ -29,3 +30,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs b/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs index 51d64016..d6fc4594 100644 --- a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -23,3 +24,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsNewDay.cs b/src/StardewModdingAPI/Events/EventArgsNewDay.cs index aba837e4..5bd2ba66 100644 --- a/src/StardewModdingAPI/Events/EventArgsNewDay.cs +++ b/src/StardewModdingAPI/Events/EventArgsNewDay.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -33,3 +34,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs b/src/StardewModdingAPI/Events/EventArgsStringChanged.cs index 85b6fab5..1498cb71 100644 --- a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs +++ b/src/StardewModdingAPI/Events/EventArgsStringChanged.cs @@ -1,4 +1,5 @@ -using System; +#if !SMAPI_2_0 +using System; namespace StardewModdingAPI.Events { @@ -27,3 +28,4 @@ namespace StardewModdingAPI.Events } } } +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 8e3cf662..c97b2c36 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; @@ -29,6 +30,7 @@ namespace StardewModdingAPI.Events /// The backing field for . [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _FirstUpdateTick; +#endif /********* @@ -40,6 +42,7 @@ namespace StardewModdingAPI.Events /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. internal static event EventHandler GameLoadedInternal; +#if !SMAPI_2_0 /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] public static event EventHandler Initialize @@ -87,6 +90,7 @@ namespace StardewModdingAPI.Events } remove => GameEvents._FirstUpdateTick -= value; } +#endif /// Raised when the game updates its state (≈60 times per second). public static event EventHandler UpdateTick; @@ -113,42 +117,52 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) { GameEvents.DeprecationManager = deprecationManager; } +#endif - /// Raise an event. - /// Encapsulates logging and monitoring. - internal static void InvokeInitialize(IMonitor monitor) + /// Raise an event. + /// Encapsulates logging and monitoring. + internal static void InvokeInitialize(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); +#if !SMAPI_2_0 monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", GameEvents._Initialize?.GetInvocationList()); +#endif } +#if !SMAPI_2_0 /// Raise a event. /// Encapsulates logging and monitoring. internal static void InvokeLoadContent(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); } +#endif - /// Raise a event. + /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeGameLoaded(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList()); +#if !SMAPI_2_0 monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); +#endif } +#if !SMAPI_2_0 /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeFirstUpdateTick(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents._FirstUpdateTick?.GetInvocationList()); } +#endif /// Raise an event. /// Encapsulates logging and monitoring. diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index 37649fee..efada876 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -15,6 +15,7 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; @@ -25,11 +26,13 @@ namespace StardewModdingAPI.Events /// The backing field for . [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _FarmerChanged; +#endif /********* ** Events *********/ +#if !SMAPI_2_0 /// Raised after the player loads a saved game. [Obsolete("Use " + nameof(SaveEvents) + "." + nameof(SaveEvents.AfterLoad) + " instead")] public static event EventHandler LoadedGame @@ -53,6 +56,7 @@ namespace StardewModdingAPI.Events } remove => PlayerEvents._FarmerChanged -= value; } +#endif /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). public static event EventHandler InventoryChanged; @@ -64,6 +68,7 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) @@ -87,6 +92,7 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", PlayerEvents._FarmerChanged?.GetInvocationList(), null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); } +#endif /// Raise an event. /// Encapsulates monitoring and logging. diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index 5dadf567..520f8b24 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -11,6 +11,7 @@ namespace StardewModdingAPI.Events /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; @@ -29,6 +30,8 @@ namespace StardewModdingAPI.Events /// The backing field for . [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _YearOfGameChanged; +#endif + /********* ** Events @@ -39,6 +42,7 @@ namespace StardewModdingAPI.Events /// Raised after the in-game clock changes. public static event EventHandler TimeOfDayChanged; +#if !SMAPI_2_0 /// Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use instead. [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] public static event EventHandler DayOfMonthChanged @@ -86,17 +90,20 @@ namespace StardewModdingAPI.Events } remove => TimeEvents._OnNewDay -= value; } +#endif /********* ** Internal methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) { TimeEvents.DeprecationManager = deprecationManager; } +#endif /// Raise an event. /// Encapsulates monitoring and logging. @@ -105,7 +112,7 @@ namespace StardewModdingAPI.Events monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty); } - /// Raise a event. + /// Raise a event. /// Encapsulates monitoring and logging. /// The previous time in military time format (e.g. 6:00pm is 1800). /// The current time in military time format (e.g. 6:10pm is 1810). @@ -114,6 +121,7 @@ namespace StardewModdingAPI.Events monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); } +#if !SMAPI_2_0 /// Raise a event. /// Encapsulates monitoring and logging. /// The previous day value. @@ -150,5 +158,6 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", TimeEvents._OnNewDay?.GetInvocationList(), null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); } +#endif } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index cefc860b..ceb51bbb 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -109,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModLoading bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl); bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl); - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; + string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:"; if (hasOfficialUrl) error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; @@ -131,7 +131,27 @@ namespace StardewModdingAPI.Framework.ModLoading // validate DLL path string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll); if (!File.Exists(assemblyPath)) + { mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + + // validate required fields +#if SMAPI_2_0 + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + } +#endif } } diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 8e5d13f8..08b88025 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -38,9 +38,11 @@ namespace StardewModdingAPI.Framework.Models /// The unique mod ID. public string UniqueID { get; set; } +#if !SMAPI_2_0 /// Whether the mod uses per-save config files. [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] public bool PerSaveConfigs { get; set; } +#endif /// Any manifest fields which didn't match a valid field. [JsonExtensionData] diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 7d40b72b..64cc0bdc 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -104,6 +104,7 @@ namespace StardewModdingAPI.Framework this.LogFile.WriteLine(""); } +#if !SMAPI_2_0 /// Log a message for the player or developer, using the specified console color. /// The name of the mod logging the message. /// The message to log. @@ -114,6 +115,7 @@ namespace StardewModdingAPI.Framework { this.LogImpl(source, message, level, color); } +#endif /********* diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0a8a0873..5707aab1 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -206,7 +206,7 @@ namespace StardewModdingAPI.Framework // from Farmer constructor if (Game1.player != null) - Game1.player.FarmerRenderer = new FarmerRenderer(this.Load($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + Game1.player.FarmerRenderer = new FarmerRenderer(this.Load("Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 39713d4a..678dcf3a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Framework private readonly IMonitor Monitor; /// SMAPI's content manager. - private SContentManager SContentManager; + private readonly SContentManager SContentManager; /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -113,6 +113,7 @@ namespace StardewModdingAPI.Framework /// The time of day (in 24-hour military format) at last check. private int PreviousTime; +#if !SMAPI_2_0 /// The day of month (1–28) at last check. private int PreviousDay; @@ -127,6 +128,7 @@ namespace StardewModdingAPI.Framework /// The player character at last check. private SFarmer PreviousFarmer; +#endif /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -285,7 +287,9 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) { GameEvents.InvokeInitialize(this.Monitor); +#if !SMAPI_2_0 GameEvents.InvokeLoadContent(this.Monitor); +#endif GameEvents.InvokeGameLoaded(this.Monitor); } @@ -315,7 +319,9 @@ namespace StardewModdingAPI.Framework Context.IsWorldReady = true; SaveEvents.InvokeAfterLoad(this.Monitor); +#if !SMAPI_2_0 PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); +#endif TimeEvents.InvokeAfterDayStarted(this.Monitor); } this.AfterLoadTimer--; @@ -460,9 +466,11 @@ namespace StardewModdingAPI.Framework if (this.GetHash(Game1.locations) != this.PreviousGameLocations) LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); +#if !SMAPI_2_0 // raise player changed if (Game1.player != this.PreviousFarmer) PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); +#endif // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) @@ -493,12 +501,14 @@ namespace StardewModdingAPI.Framework // raise time changed if (Game1.timeOfDay != this.PreviousTime) TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); +#if !SMAPI_2_0 if (Game1.dayOfMonth != this.PreviousDay) TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); if (Game1.currentSeason != this.PreviousSeason) TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); if (Game1.year != this.PreviousYear) TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); +#endif // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) @@ -508,7 +518,6 @@ namespace StardewModdingAPI.Framework // update state this.PreviousGameLocations = this.GetHash(Game1.locations); this.PreviousGameLocation = Game1.currentLocation; - this.PreviousFarmer = Game1.player; this.PreviousCombatLevel = Game1.player.combatLevel; this.PreviousFarmingLevel = Game1.player.farmingLevel; this.PreviousFishingLevel = Game1.player.fishingLevel; @@ -518,21 +527,26 @@ namespace StardewModdingAPI.Framework this.PreviousItems = Game1.player.items.Where(n => n != null).ToDictionary(n => n, n => n.Stack); this.PreviousLocationObjects = this.GetHash(Game1.currentLocation.objects); this.PreviousTime = Game1.timeOfDay; + this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; + this.PreviousSaveID = Game1.uniqueIDForThisGame; +#if !SMAPI_2_0 + this.PreviousFarmer = Game1.player; this.PreviousDay = Game1.dayOfMonth; this.PreviousSeason = Game1.currentSeason; this.PreviousYear = Game1.year; - this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; - this.PreviousSaveID = Game1.uniqueIDForThisGame; +#endif } /********* ** Game day transition event (obsolete) *********/ +#if !SMAPI_2_0 if (Game1.newDay != this.PreviousIsNewDay) { TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); this.PreviousIsNewDay = Game1.newDay; } +#endif /********* ** Game update @@ -552,7 +566,9 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeUpdateTick(this.Monitor); if (this.FirstUpdate) { +#if !SMAPI_2_0 GameEvents.InvokeFirstUpdateTick(this.Monitor); +#endif this.FirstUpdate = false; } if (this.CurrentUpdateTick % 2 == 0) diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs index d58cebfe..562fa1f8 100644 --- a/src/StardewModdingAPI/Log.cs +++ b/src/StardewModdingAPI/Log.cs @@ -1,3 +1,4 @@ +#if !SMAPI_2_0 using System; using System.Threading; using StardewModdingAPI.Framework; @@ -315,4 +316,5 @@ namespace StardewModdingAPI return Log.ModRegistry.GetModFromStack() ?? ""; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index 171088cf..302f16ec 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -11,11 +11,14 @@ namespace StardewModdingAPI /********* ** Properties *********/ +#if !SMAPI_2_0 /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; + /// The backing field for . private string _pathOnDisk; +#endif /********* @@ -30,6 +33,7 @@ namespace StardewModdingAPI /// The mod's manifest. public IManifest ModManifest { get; internal set; } +#if !SMAPI_2_0 /// The full path to the mod's directory on the disk. [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.DirectoryPath) + " instead")] public string PathOnDisk @@ -69,11 +73,13 @@ namespace StardewModdingAPI return Context.IsSaveLoaded ? Path.Combine(this.PerSaveConfigFolder, $"{Constants.SaveFolderName}.json") : ""; } } +#endif /********* ** Public methods *********/ +#if !SMAPI_2_0 /// Injects types required for backwards compatibility. /// Manages deprecation warnings. internal static void Shim(DeprecationManager deprecationManager) @@ -84,6 +90,7 @@ namespace StardewModdingAPI /// The mod entry point, called after the mod is first loaded. [Obsolete("This overload is obsolete since SMAPI 1.0.")] public virtual void Entry(params object[] objects) { } +#endif /// The mod entry point, called after the mod is first loaded. /// Provides simplified APIs for writing mods. @@ -101,6 +108,7 @@ namespace StardewModdingAPI /********* ** Private methods *********/ +#if !SMAPI_2_0 /// Get the full path to the per-save configuration file for the current save (if is true). [Obsolete] private string GetPerSaveConfigFolder() @@ -115,6 +123,7 @@ namespace StardewModdingAPI } return Path.Combine(this.PathOnDisk, "psconfigs"); } +#endif /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index e960a684..d722b43e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -226,6 +226,7 @@ namespace StardewModdingAPI } } +#if !SMAPI_2_0 /// Get a monitor for legacy code which doesn't have one passed in. [Obsolete("This method should only be used when needed for backwards compatibility.")] internal IMonitor GetLegacyMonitorForMod() @@ -233,6 +234,7 @@ namespace StardewModdingAPI string modName = this.ModRegistry.GetModFromStack() ?? "unknown"; return this.GetSecondaryMonitor(modName); } +#endif /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() @@ -323,6 +325,7 @@ namespace StardewModdingAPI this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); +#if !SMAPI_2_0 // inject compatibility shims #pragma warning disable 618 Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); @@ -333,6 +336,7 @@ namespace StardewModdingAPI PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); #pragma warning restore 618 +#endif // redirect direct console output { @@ -369,6 +373,7 @@ namespace StardewModdingAPI resolver.ValidateManifests(mods, Constants.ApiVersion); // check for deprecated metadata +#if !SMAPI_2_0 IList deprecationWarnings = new List(); foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) { @@ -403,15 +408,20 @@ namespace StardewModdingAPI mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); } } - } + } +#endif // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); // load mods +#if SMAPI_2_0 + this.LoadMods(mods, new JsonHelper(), this.ContentManager); +#else this.LoadMods(mods, new JsonHelper(), this.ContentManager, deprecationWarnings); foreach (Action warning in deprecationWarnings) warning(); +#endif } if (this.Monitor.IsExiting) { @@ -576,8 +586,12 @@ namespace StardewModdingAPI /// The mods to load. /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. - /// A list to populate with any deprecation warnings. +#if !SMAPI_2_0 +/// A list to populate with any deprecation warnings. private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) +#else + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) +#endif { this.Monitor.Log("Loading mods...", LogLevel.Trace); @@ -615,7 +629,7 @@ namespace StardewModdingAPI } catch (IncompatibleInstructionException ex) { - TrackSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version})."); + TrackSkip(metadata, $"it's not compatible with the latest version of the game or SMAPI (detected {ex.NounPhrase}). Please check for a newer version of the mod."); continue; } catch (Exception ex) @@ -657,6 +671,7 @@ namespace StardewModdingAPI continue; } +#if !SMAPI_2_0 // prevent mods from using SMAPI 2.0 content interception before release // ReSharper disable SuspiciousTypeConversion.Global if (mod is IAssetEditor || mod is IAssetLoader) @@ -664,12 +679,15 @@ namespace StardewModdingAPI TrackSkip(metadata, $"its entry class implements {nameof(IAssetEditor)} or {nameof(IAssetLoader)}. These are part of a prototype API that isn't available for mods to use yet."); } // ReSharper restore SuspiciousTypeConversion.Global +#endif // inject data mod.ModManifest = manifest; mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); +#if !SMAPI_2_0 mod.PathOnDisk = metadata.DirectoryPath; +#endif // track mod metadata.SetMod(mod); @@ -729,12 +747,14 @@ namespace StardewModdingAPI try { IMod mod = metadata.Mod; - (mod as Mod)?.Entry(); // deprecated since 1.0 mod.Entry(mod.Helper); +#if !SMAPI_2_0 + (mod as Mod)?.Entry(); // deprecated since 1.0 // raise deprecation warning for old Entry() methods if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info)); +#endif } catch (Exception ex) { -- cgit From 136525b40df5d47b8e398a394af081e19efcf86c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 01:29:56 -0400 Subject: remove System.ValueTuple This caused reference errors on Linux/Mac, and there aren't enough use cases to look into it further for now. --- release-notes.md | 1 - .../StardewModdingAPI.Tests.csproj | 3 --- src/StardewModdingAPI.Tests/packages.config | 1 - .../Framework/ModLoading/ModResolver.cs | 2 +- src/StardewModdingAPI/Framework/SContentManager.cs | 24 +++++++++++----------- src/StardewModdingAPI/StardewModdingAPI.csproj | 3 --- src/StardewModdingAPI/packages.config | 1 - src/prepare-install-package.targets | 1 - 8 files changed, 13 insertions(+), 23 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 94106ba6..c1efc5ae 100644 --- a/release-notes.md +++ b/release-notes.md @@ -27,7 +27,6 @@ For players: For modders: * Added `SDate` utility for in-game date calculations (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Dates)). * Added support for minimum dependency versions in `manifest.json` (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Manifest)). -* Added `System.ValueTuple.dll` to the SMAPI install package so mods can use [C# 7 value tuples](https://docs.microsoft.com/en-us/dotnet/csharp/tuples). * Added more useful logging when loading mods. * Changed `manifest.MinimumApiVersion` from string to `ISemanticVersion`. This shouldn't affect mods unless they referenced that field in code. * Fixed `SemanticVersion` parsing some invalid versions into close approximations (like `1.apple` → `1.0-apple`). diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj index 7129cfb7..9bfd7567 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj @@ -43,9 +43,6 @@ ..\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll - - ..\packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll - diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/StardewModdingAPI.Tests/packages.config index 7ba8c7b2..ba954308 100644 --- a/src/StardewModdingAPI.Tests/packages.config +++ b/src/StardewModdingAPI.Tests/packages.config @@ -4,5 +4,4 @@ - \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index ceb51bbb..9c56aaa4 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -228,7 +228,7 @@ namespace StardewModdingAPI.Framework.ModLoading from entry in mod.Manifest.Dependencies let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) orderby entry.UniqueID - select (ID: entry.UniqueID, MinVersion: entry.MinimumVersion, Mod: dependencyMod) + select new { ID = entry.UniqueID, MinVersion = entry.MinimumVersion, Mod = dependencyMod } ) .ToArray(); diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 5707aab1..ebf1c8a5 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -232,11 +232,11 @@ namespace StardewModdingAPI.Framework { try { - return entry.Interceptor.CanLoad(info); + return entry.Value.CanLoad(info); } catch (Exception ex) { - this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -247,14 +247,14 @@ namespace StardewModdingAPI.Framework return null; if (loaders.Length > 1) { - string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); + string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); return null; } // fetch asset from loader - IModMetadata mod = loaders[0].Mod; - IAssetLoader loader = loaders[0].Interceptor; + IModMetadata mod = loaders[0].Key; + IAssetLoader loader = loaders[0].Value; T data; try { @@ -290,8 +290,8 @@ namespace StardewModdingAPI.Framework foreach (var entry in this.GetInterceptors(this.Editors)) { // check for match - IModMetadata mod = entry.Mod; - IAssetEditor editor = entry.Interceptor; + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; try { if (!editor.CanEdit(info)) @@ -299,7 +299,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -312,7 +312,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{entry.Mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit @@ -333,7 +333,7 @@ namespace StardewModdingAPI.Framework } /// Get all registered interceptors from a list. - private IEnumerable<(IModMetadata Mod, T Interceptor)> GetInterceptors(IDictionary> entries) + private IEnumerable> GetInterceptors(IDictionary> entries) { foreach (var entry in entries) { @@ -342,11 +342,11 @@ namespace StardewModdingAPI.Framework // special case if mod is an interceptor if (metadata.Mod is T modAsInterceptor) - yield return (metadata, modAsInterceptor); + yield return new KeyValuePair(metadata, modAsInterceptor); // registered editors foreach (T interceptor in interceptors) - yield return (metadata, interceptor); + yield return new KeyValuePair(metadata, interceptor); } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 4d65b1af..bf1c43d1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -79,9 +79,6 @@ True - - ..\packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll - diff --git a/src/StardewModdingAPI/packages.config b/src/StardewModdingAPI/packages.config index 6a2a8d1b..e5fa3c3a 100644 --- a/src/StardewModdingAPI/packages.config +++ b/src/StardewModdingAPI/packages.config @@ -2,5 +2,4 @@ - \ No newline at end of file diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets index df8bb100..f0debdd2 100644 --- a/src/prepare-install-package.targets +++ b/src/prepare-install-package.targets @@ -31,7 +31,6 @@ - -- cgit From 697155c8a239a48ecaaaca0490584b78f3e3c26e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 14:11:39 -0400 Subject: update deprecation warning text --- src/StardewModdingAPI/Framework/DeprecationManager.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs index 6b95960b..cc6754b5 100644 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -52,13 +52,12 @@ namespace StardewModdingAPI.Framework if (!this.MarkWarned(source ?? "", nounPhrase, version)) return; + // show SMAPI 2.0 meta-warning + if(this.MarkWarned("SMAPI", "SMAPI 2.0 meta-warning", "2.0")) + this.Monitor.Log("Some of your mods will break in the upcoming SMAPI 2.0 release because they use obsolete APIs. Please check for a newer version of any mod showing 'may break' warnings, or let the author know about this message. For more information, see http://community.playstarbound.com/threads/135000.", LogLevel.Warn); + // build message - string message = source != null - ? $"{source} used {nounPhrase}, which is deprecated since SMAPI {version}." - : $"An unknown mod used {nounPhrase}, which is deprecated since SMAPI {version}."; - message += severity != DeprecationLevel.PendingRemoval - ? " This will break in a future version of SMAPI." - : " It will be removed soon, so the mod will break if it's not updated."; + string message = $"{source ?? "An unknown mod"} may break in the upcoming SMAPI 2.0 release (detected {nounPhrase})."; if (source == null) message += $"{Environment.NewLine}{Environment.StackTrace}"; @@ -70,7 +69,7 @@ namespace StardewModdingAPI.Framework break; case DeprecationLevel.Info: - this.Monitor.Log(message, LogLevel.Warn); + this.Monitor.Log(message, LogLevel.Debug); break; case DeprecationLevel.PendingRemoval: -- cgit From 18e5e42529e13f21553651014a93c0f48cd93a59 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 14:26:45 -0400 Subject: defer some console changes until SMAPI 2.0 --- release-notes.md | 7 ++++-- src/StardewModdingAPI/Framework/Monitor.cs | 8 +++++++ src/StardewModdingAPI/Program.cs | 34 +++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index e34990aa..0d34e8e0 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,21 +4,24 @@ ## 2.0 See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...2.0). +For players: +* The SMAPI console is now cleaner and simpler. + For mod developers: -* The manifest.json version can now be specified as a string. * SMAPI mods can now edit XNB images and data loaded by the game (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* The manifest.json version can now be specified as a string. --> ## 1.15 See [log](https://github.com/Pathoschild/SMAPI/compare/1.14...1.15). For players: -* Several changes to the SMAPI console to make it simpler for players. * Revamped TrainerMod's item commands: * `player_add` is a new command to add any item to your inventory (including tools, weapons, equipment, craftables, wallpaper, etc). This replaces the former `player_additem`, `player_addring`, and `player_addweapon`. * `list_items` now shows all items in the game. You can search by item type like `list_items weapon`, or search by item name like `list_items galaxy sword`. * `list_items` now also matches translated item names when playing in another language. * `list_item_types` is a new command to see a list of item types. +* Cleaned up SMAPI console a bit. * Fixed unhelpful error when a `config.json` is invalid. * Fixed rare crash when window loses focus for a few players (further to fix in 1.14). * Fixed invalid `ObjectInformation.xnb` causing a flood of warnings; SMAPI now shows one error instead. diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 64cc0bdc..6359b454 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -45,8 +45,10 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; +#if SMAPI_2_0 /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } +#endif /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } @@ -95,6 +97,7 @@ namespace StardewModdingAPI.Framework this.ExitTokenSource.Cancel(); } +#if SMAPI_2_0 /// Write a newline to the console and log file. internal void Newline() { @@ -103,6 +106,7 @@ namespace StardewModdingAPI.Framework if (this.WriteToFile) this.LogFile.WriteLine(""); } +#endif #if !SMAPI_2_0 /// Log a message for the player or developer, using the specified console color. @@ -140,7 +144,11 @@ namespace StardewModdingAPI.Framework string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; +#if SMAPI_2_0 string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; +#else + string consoleMessage = fullMessage; +#endif // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ed1fe2e7..c7adcb94 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -126,6 +126,9 @@ namespace StardewModdingAPI // init logging this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); +#if !SMAPI_2_0 + this.Monitor.Log("Preparing SMAPI..."); +#endif // validate paths this.VerifyPath(Constants.ModPath); @@ -209,7 +212,11 @@ namespace StardewModdingAPI } // start game +#if SMAPI_2_0 this.Monitor.Log("Starting game...", LogLevel.Trace); +#else + this.Monitor.Log("Starting game..."); +#endif try { this.IsGameRunning = true; @@ -349,7 +356,9 @@ namespace StardewModdingAPI if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; +#if SMAPI_2_0 this.Monitor.ShowFullStampInConsole = true; +#endif this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); } if (!this.Settings.CheckForUpdates) @@ -365,7 +374,11 @@ namespace StardewModdingAPI // load mods { +#if SMAPI_2_0 this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); +#else + this.Monitor.Log("Loading mod metadata..."); +#endif ModResolver resolver = new ModResolver(); // load manifests @@ -455,6 +468,9 @@ namespace StardewModdingAPI private void RunConsoleLoop() { // prepare console +#if !SMAPI_2_0 + this.Monitor.Log("Starting console..."); +#endif this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); @@ -593,8 +609,11 @@ namespace StardewModdingAPI private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) #endif { +#if SMAPI_2_0 this.Monitor.Log("Loading mods...", LogLevel.Trace); - +#else + this.Monitor.Log("Loading mods..."); +#endif // load mod assemblies IDictionary skippedMods = new Dictionary(); { @@ -702,7 +721,9 @@ namespace StardewModdingAPI IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods +#if SMAPI_2_0 this.Monitor.Newline(); +#endif if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); @@ -716,7 +737,9 @@ namespace StardewModdingAPI else this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); } +#if SMAPI_2_0 this.Monitor.Newline(); +#endif } // log loaded mods @@ -731,7 +754,9 @@ namespace StardewModdingAPI LogLevel.Info ); } +#if SMAPI_2_0 this.Monitor.Newline(); +#endif // initialise translations this.ReloadTranslations(); @@ -835,6 +860,7 @@ namespace StardewModdingAPI } else { +#if SMAPI_2_0 string message = "The following commands are registered:\n"; IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); foreach (var group in groups) @@ -846,6 +872,10 @@ namespace StardewModdingAPI message += "For more information about a command, type 'help command_name'."; this.Monitor.Log(message, LogLevel.Info); +#else + this.Monitor.Log("The following commands are registered: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)) + ".", LogLevel.Info); + this.Monitor.Log("For more information about a command, type 'help command_name'.", LogLevel.Info); +#endif } break; @@ -894,7 +924,9 @@ namespace StardewModdingAPI { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, +#if SMAPI_2_0 ShowFullStampInConsole = this.Settings.DeveloperMode +#endif }; } -- cgit From 96da7c1cbc19e079e06fe8c7c857ffe86c0d9848 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 14:49:29 -0400 Subject: fix crash in new content manager when returning to title (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index ebf1c8a5..42c3b0e6 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -349,5 +349,23 @@ namespace StardewModdingAPI.Framework yield return new KeyValuePair(metadata, interceptor); } } + + /// Dispose all game resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + if (!disposing) + return; + + // Clear cache & reload all assets. While that may seem perverse during disposal, it's + // necessary due to limitations in the way SMAPI currently intercepts content assets. + // + // The game uses multiple content managers while SMAPI needs one and only one. The game + // only disposes some of its content managers when returning to title, which means SMAPI + // can't know which assets are meant to be disposed. Here we remove current assets from + // the cache, but don't dispose them to avoid crashing any code that still references + // them. The garbage collector will eventually clean up any unused assets. + this.Reset(); + } } } -- cgit From 2f42051cc95f69a74e078ca8d0f9ae8ddbdbbbf0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 4 Jul 2017 18:18:52 -0400 Subject: tweak method name per feedback (#255) --- src/StardewModdingAPI/Framework/Content/AssetInfo.cs | 4 ++-- src/StardewModdingAPI/IAssetInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs index 08bc3a03..d580dc06 100644 --- a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs +++ b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework.Content /// The content's locale code, if the content is localised. public string Locale { get; } - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. public string AssetName { get; } /// The content data type. @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Content /// Get whether the asset name being loaded matches a given name after normalisation. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - public bool IsAssetName(string path) + public bool AssetNameEquals(string path) { path = this.GetNormalisedPath(path); return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/StardewModdingAPI/IAssetInfo.cs index dc65a750..5dd58e2e 100644 --- a/src/StardewModdingAPI/IAssetInfo.cs +++ b/src/StardewModdingAPI/IAssetInfo.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI /// The content's locale code, if the content is localised. string Locale { get; } - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. string AssetName { get; } /// The content data type. @@ -23,6 +23,6 @@ namespace StardewModdingAPI *********/ /// Get whether the asset name being loaded matches a given name after normalisation. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - bool IsAssetName(string path); + bool AssetNameEquals(string path); } } -- cgit From 8d301162d87558826ed8fc8f2352800bf674ddf0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 5 Jul 2017 15:41:58 -0400 Subject: add InputEvents which unify keyboard, mouse, and controller input with more metadata (#316) --- release-notes.md | 6 +- src/StardewModdingAPI/Events/EventArgsInput.cs | 38 ++ src/StardewModdingAPI/Events/InputEvents.cs | 45 ++ src/StardewModdingAPI/Framework/CursorPosition.cs | 37 ++ src/StardewModdingAPI/Framework/SGame.cs | 268 ++++----- src/StardewModdingAPI/ICursorPosition.cs | 19 + src/StardewModdingAPI/StardewModdingAPI.csproj | 5 + src/StardewModdingAPI/Utilities/SButton.cs | 659 ++++++++++++++++++++++ 8 files changed, 932 insertions(+), 145 deletions(-) create mode 100644 src/StardewModdingAPI/Events/EventArgsInput.cs create mode 100644 src/StardewModdingAPI/Events/InputEvents.cs create mode 100644 src/StardewModdingAPI/Framework/CursorPosition.cs create mode 100644 src/StardewModdingAPI/ICursorPosition.cs create mode 100644 src/StardewModdingAPI/Utilities/SButton.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 2283d6d1..5d906f6c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,8 +6,10 @@ For players: * The SMAPI console is now much simpler and easier-to-read. For mod developers: -* SMAPI mods can now edit XNB images & data loaded by the game (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). -* SMAPI mods can now inject new XNB images & data (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* Added API to edit XNB images & data loaded by the game (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* Added API to inject new XNB images & data (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). +* Added `InputEvents` which unify keyboard, mouse, and controller input for much simpler input handling (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Input_events)). +* Added useful `InputEvents` metadata like the cursor position, grab tile, etc. * The `manifest.json` version can now be specified as a string. ## 1.15 diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/StardewModdingAPI/Events/EventArgsInput.cs new file mode 100644 index 00000000..1d5e6fde --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsInput.cs @@ -0,0 +1,38 @@ +#if SMAPI_2_0 +using System; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed or released. + public class EventArgsInput : EventArgs + { + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; set; } + + /// Whether the input is considered a 'click' by the game for enabling action. + public bool IsClick { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + public EventArgsInput(SButton button, ICursorPosition cursor, bool isClick) + { + this.Button = button; + this.Cursor = cursor; + this.IsClick = isClick; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/StardewModdingAPI/Events/InputEvents.cs new file mode 100644 index 00000000..285487af --- /dev/null +++ b/src/StardewModdingAPI/Events/InputEvents.cs @@ -0,0 +1,45 @@ +#if SMAPI_2_0 +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Utilities; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player uses a controller, keyboard, or mouse button. + public static class InputEvents + { + /********* + ** Events + *********/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public static event EventHandler ButtonPressed; + + /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + public static event EventHandler ButtonReleased; + + + /********* + ** Internal methods + *********/ + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + internal static void InvokeButtonPressed(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + { + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonPressed)}", InputEvents.ButtonPressed?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + } + + /// Raise a event. + /// Encapsulates monitoring and logging. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// Whether the input is considered a 'click' by the game for enabling action. + internal static void InvokeButtonReleased(IMonitor monitor, SButton button, ICursorPosition cursor, bool isClick) + { + monitor.SafelyRaiseGenericEvent($"{nameof(InputEvents)}.{nameof(InputEvents.ButtonReleased)}", InputEvents.ButtonReleased?.GetInvocationList(), null, new EventArgsInput(button, cursor, isClick)); + } + } +} +#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/StardewModdingAPI/Framework/CursorPosition.cs new file mode 100644 index 00000000..4f256da5 --- /dev/null +++ b/src/StardewModdingAPI/Framework/CursorPosition.cs @@ -0,0 +1,37 @@ +#if SMAPI_2_0 +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Framework +{ + /// Defines a position on a given map at different reference points. + internal class CursorPosition : ICursorPosition + { + /********* + ** Accessors + *********/ + /// The pixel position relative to the top-left corner of the visible screen. + public Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + public Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + public Vector2 GrabTile { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The pixel position relative to the top-left corner of the visible screen. + /// The tile position relative to the top-left corner of the map. + /// The tile position that the game considers under the cursor for purposes of clicking actions. + public CursorPosition(Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + { + this.ScreenPixels = screenPixels; + this.Tile = tile; + this.GrabTile = grabTile; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 678dcf3a..f2c5c0c9 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Locations; @@ -59,12 +60,15 @@ namespace StardewModdingAPI.Framework /**** ** Game state ****/ - /// Arrays of pressed controller buttons indexed by . - private Buttons[] PreviousPressedButtons = new Buttons[0]; + /// A record of the buttons pressed as of the previous tick. + private SButton[] PreviousPressedButtons = new SButton[0]; /// A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick. private KeyboardState PreviousKeyState; + /// A record of the controller state (i.e. the up/down state for each button) as of the previous tick. + private GamePadState PreviousControllerState; + /// A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick. private MouseState PreviousMouseState; @@ -351,64 +355,95 @@ namespace StardewModdingAPI.Framework { // get latest state KeyboardState keyState; + GamePadState controllerState; MouseState mouseState; Point mousePosition; try { keyState = Keyboard.GetState(); + controllerState = GamePad.GetState(PlayerIndex.One); mouseState = Mouse.GetState(); mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); } catch (InvalidOperationException) // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true { keyState = this.PreviousKeyState; + controllerState = this.PreviousControllerState; mouseState = this.PreviousMouseState; mousePosition = this.PreviousMousePosition; } // analyse state - Keys[] currentlyPressedKeys = keyState.GetPressedKeys(); - Keys[] previousPressedKeys = this.PreviousKeyState.GetPressedKeys(); - Keys[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); - Keys[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); - - // raise key pressed - foreach (Keys key in framePressedKeys) - ControlEvents.InvokeKeyPressed(this.Monitor, key); - - // raise key released - foreach (Keys key in frameReleasedKeys) - ControlEvents.InvokeKeyReleased(this.Monitor, key); + SButton[] currentlyPressedKeys = this.GetPressedButtons(keyState, mouseState, controllerState).ToArray(); + SButton[] previousPressedKeys = this.PreviousPressedButtons; + SButton[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); + SButton[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); + bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); + + // get cursor position +#if SMAPI_2_0 + ICursorPosition cursor; + { + // cursor position + Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); + Vector2 tile = new Vector2((Game1.viewport.X + screenPixels.X) / Game1.tileSize, (Game1.viewport.Y + screenPixels.Y) / Game1.tileSize); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + cursor = new CursorPosition(screenPixels, tile, grabTile); + } +#endif - // raise controller button pressed - foreach (Buttons button in this.GetFramePressedButtons()) + // raise button pressed + foreach (SButton button in framePressedKeys) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) +#if SMAPI_2_0 + InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); +#endif + + // legacy events + if (button.TryGetKeyboard(out Keys key)) { - var triggers = GamePad.GetState(PlayerIndex.One).Triggers; - ControlEvents.InvokeTriggerPressed(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right); + if (key != Keys.None) + ControlEvents.InvokeKeyPressed(this.Monitor, key); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + ControlEvents.InvokeTriggerPressed(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); + else + ControlEvents.InvokeButtonPressed(this.Monitor, controllerButton); } - else - ControlEvents.InvokeButtonPressed(this.Monitor, button); } - // raise controller button released - foreach (Buttons button in this.GetFrameReleasedButtons()) + // raise button released + foreach (SButton button in frameReleasedKeys) { - if (button == Buttons.LeftTrigger || button == Buttons.RightTrigger) +#if SMAPI_2_0 + bool wasClick = + (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click + || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); + InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); +#endif + + // legacy events + if (button.TryGetKeyboard(out Keys key)) { - var triggers = GamePad.GetState(PlayerIndex.One).Triggers; - ControlEvents.InvokeTriggerReleased(this.Monitor, button, button == Buttons.LeftTrigger ? triggers.Left : triggers.Right); + if (key != Keys.None) + ControlEvents.InvokeKeyReleased(this.Monitor, key); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + ControlEvents.InvokeTriggerReleased(this.Monitor, controllerButton, controllerButton == Buttons.LeftTrigger ? controllerState.Triggers.Left : controllerState.Triggers.Right); + else + ControlEvents.InvokeButtonReleased(this.Monitor, controllerButton); } - else - ControlEvents.InvokeButtonReleased(this.Monitor, button); } - // raise keyboard state changed + // raise legacy state-changed events if (keyState != this.PreviousKeyState) ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousKeyState, keyState); - - // raise mouse state changed if (mouseState != this.PreviousMouseState) ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousMouseState, mouseState, this.PreviousMousePosition, mousePosition); @@ -416,7 +451,8 @@ namespace StardewModdingAPI.Framework this.PreviousMouseState = mouseState; this.PreviousMousePosition = mousePosition; this.PreviousKeyState = keyState; - this.PreviousPressedButtons = this.GetButtonsDown(); + this.PreviousControllerState = controllerState; + this.PreviousPressedButtons = currentlyPressedKeys; } /********* @@ -1308,120 +1344,66 @@ namespace StardewModdingAPI.Framework this.PreviousSaveID = 0; } - /// Get the controller buttons which are currently pressed. - private Buttons[] GetButtonsDown() + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) { - var state = GamePad.GetState(PlayerIndex.One); - var buttons = new List(); - if (state.IsConnected) + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) { - if (state.Buttons.A == ButtonState.Pressed) buttons.Add(Buttons.A); - if (state.Buttons.B == ButtonState.Pressed) buttons.Add(Buttons.B); - if (state.Buttons.Back == ButtonState.Pressed) buttons.Add(Buttons.Back); - if (state.Buttons.BigButton == ButtonState.Pressed) buttons.Add(Buttons.BigButton); - if (state.Buttons.LeftShoulder == ButtonState.Pressed) buttons.Add(Buttons.LeftShoulder); - if (state.Buttons.LeftStick == ButtonState.Pressed) buttons.Add(Buttons.LeftStick); - if (state.Buttons.RightShoulder == ButtonState.Pressed) buttons.Add(Buttons.RightShoulder); - if (state.Buttons.RightStick == ButtonState.Pressed) buttons.Add(Buttons.RightStick); - if (state.Buttons.Start == ButtonState.Pressed) buttons.Add(Buttons.Start); - if (state.Buttons.X == ButtonState.Pressed) buttons.Add(Buttons.X); - if (state.Buttons.Y == ButtonState.Pressed) buttons.Add(Buttons.Y); - if (state.DPad.Up == ButtonState.Pressed) buttons.Add(Buttons.DPadUp); - if (state.DPad.Down == ButtonState.Pressed) buttons.Add(Buttons.DPadDown); - if (state.DPad.Left == ButtonState.Pressed) buttons.Add(Buttons.DPadLeft); - if (state.DPad.Right == ButtonState.Pressed) buttons.Add(Buttons.DPadRight); - if (state.Triggers.Left > 0.2f) buttons.Add(Buttons.LeftTrigger); - if (state.Triggers.Right > 0.2f) buttons.Add(Buttons.RightTrigger); + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; } - return buttons.ToArray(); - } - - /// Get the controller buttons which were pressed after the last update. - private Buttons[] GetFramePressedButtons() - { - var state = GamePad.GetState(PlayerIndex.One); - var buttons = new List(); - if (state.IsConnected) - { - if (this.WasButtonJustPressed(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A); - if (this.WasButtonJustPressed(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B); - if (this.WasButtonJustPressed(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back); - if (this.WasButtonJustPressed(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton); - if (this.WasButtonJustPressed(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder); - if (this.WasButtonJustPressed(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick); - if (this.WasButtonJustPressed(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder); - if (this.WasButtonJustPressed(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick); - if (this.WasButtonJustPressed(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start); - if (this.WasButtonJustPressed(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X); - if (this.WasButtonJustPressed(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y); - if (this.WasButtonJustPressed(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp); - if (this.WasButtonJustPressed(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown); - if (this.WasButtonJustPressed(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft); - if (this.WasButtonJustPressed(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight); - if (this.WasButtonJustPressed(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger); - if (this.WasButtonJustPressed(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger); - } - return buttons.ToArray(); - } - - /// Get the controller buttons which were released after the last update. - private Buttons[] GetFrameReleasedButtons() - { - var state = GamePad.GetState(PlayerIndex.One); - var buttons = new List(); - if (state.IsConnected) - { - if (this.WasButtonJustReleased(Buttons.A, state.Buttons.A)) buttons.Add(Buttons.A); - if (this.WasButtonJustReleased(Buttons.B, state.Buttons.B)) buttons.Add(Buttons.B); - if (this.WasButtonJustReleased(Buttons.Back, state.Buttons.Back)) buttons.Add(Buttons.Back); - if (this.WasButtonJustReleased(Buttons.BigButton, state.Buttons.BigButton)) buttons.Add(Buttons.BigButton); - if (this.WasButtonJustReleased(Buttons.LeftShoulder, state.Buttons.LeftShoulder)) buttons.Add(Buttons.LeftShoulder); - if (this.WasButtonJustReleased(Buttons.LeftStick, state.Buttons.LeftStick)) buttons.Add(Buttons.LeftStick); - if (this.WasButtonJustReleased(Buttons.RightShoulder, state.Buttons.RightShoulder)) buttons.Add(Buttons.RightShoulder); - if (this.WasButtonJustReleased(Buttons.RightStick, state.Buttons.RightStick)) buttons.Add(Buttons.RightStick); - if (this.WasButtonJustReleased(Buttons.Start, state.Buttons.Start)) buttons.Add(Buttons.Start); - if (this.WasButtonJustReleased(Buttons.X, state.Buttons.X)) buttons.Add(Buttons.X); - if (this.WasButtonJustReleased(Buttons.Y, state.Buttons.Y)) buttons.Add(Buttons.Y); - if (this.WasButtonJustReleased(Buttons.DPadUp, state.DPad.Up)) buttons.Add(Buttons.DPadUp); - if (this.WasButtonJustReleased(Buttons.DPadDown, state.DPad.Down)) buttons.Add(Buttons.DPadDown); - if (this.WasButtonJustReleased(Buttons.DPadLeft, state.DPad.Left)) buttons.Add(Buttons.DPadLeft); - if (this.WasButtonJustReleased(Buttons.DPadRight, state.DPad.Right)) buttons.Add(Buttons.DPadRight); - if (this.WasButtonJustReleased(Buttons.LeftTrigger, state.Triggers.Left)) buttons.Add(Buttons.LeftTrigger); - if (this.WasButtonJustReleased(Buttons.RightTrigger, state.Triggers.Right)) buttons.Add(Buttons.RightTrigger); - } - return buttons.ToArray(); - } - - /// Get whether a controller button was pressed since the last check. - /// The controller button to check. - /// The last known state. - private bool WasButtonJustPressed(Buttons button, ButtonState buttonState) - { - return buttonState == ButtonState.Pressed && !this.PreviousPressedButtons.Contains(button); - } - - /// Get whether a controller button was released since the last check. - /// The controller button to check. - /// The last known state. - private bool WasButtonJustReleased(Buttons button, ButtonState buttonState) - { - return buttonState == ButtonState.Released && this.PreviousPressedButtons.Contains(button); - } - - /// Get whether an analogue controller button was pressed since the last check. - /// The controller button to check. - /// The last known value. - private bool WasButtonJustPressed(Buttons button, float value) - { - return this.WasButtonJustPressed(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released); - } - - /// Get whether an analogue controller button was released since the last check. - /// The controller button to check. - /// The last known value. - private bool WasButtonJustReleased(Buttons button, float value) - { - return this.WasButtonJustReleased(button, value > 0.2f ? ButtonState.Pressed : ButtonState.Released); } /// Get the player inventory changes between two states. diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/StardewModdingAPI/ICursorPosition.cs new file mode 100644 index 00000000..d03cda71 --- /dev/null +++ b/src/StardewModdingAPI/ICursorPosition.cs @@ -0,0 +1,19 @@ +#if SMAPI_2_0 +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI +{ + /// Represents a cursor position in the different coordinate systems. + public interface ICursorPosition + { + /// The pixel position relative to the top-left corner of the visible screen. + Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + Vector2 GrabTile { get; } + } +} +#endif diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index bf1c43d1..c442cc8a 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -93,7 +93,9 @@ + + @@ -207,7 +209,10 @@ + + + diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/StardewModdingAPI/Utilities/SButton.cs new file mode 100644 index 00000000..f4fccfff --- /dev/null +++ b/src/StardewModdingAPI/Utilities/SButton.cs @@ -0,0 +1,659 @@ +using System; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Utilities +{ + /// A unified button constant which includes all controller, keyboard, and mouse buttons. + /// Derived from , , and . +#if SMAPI_2_0 + public +#else + internal +#endif + enum SButton + { + /// No valid key. + None = 0, + + /********* + ** Mouse + *********/ + /// The left mouse button. + MouseLeft = 1000, + + /// The right mouse button. + MouseRight = 1001, + + /// The middle mouse button. + MouseMiddle = 1002, + + /// The first mouse XButton. + MouseX1 = 1003, + + /// The second mouse XButton. + MouseX2 = 1004, + + /********* + ** Controller + *********/ + /// The 'A' button on a controller. + ControllerA = SButtonExtensions.ControllerOffset + Buttons.A, + + /// The 'B' button on a controller. + ControllerB = SButtonExtensions.ControllerOffset + Buttons.B, + + /// The 'X' button on a controller. + ControllerX = SButtonExtensions.ControllerOffset + Buttons.X, + + /// The 'Y' button on a controller. + ControllerY = SButtonExtensions.ControllerOffset + Buttons.Y, + + /// The back button on a controller. + ControllerBack = SButtonExtensions.ControllerOffset + Buttons.Back, + + /// The start button on a controller. + ControllerStart = SButtonExtensions.ControllerOffset + Buttons.Start, + + /// The up button on the directional pad of a controller. + DPadUp = SButtonExtensions.ControllerOffset + Buttons.DPadUp, + + /// The down button on the directional pad of a controller. + DPadDown = SButtonExtensions.ControllerOffset + Buttons.DPadDown, + + /// The left button on the directional pad of a controller. + DPadLeft = SButtonExtensions.ControllerOffset + Buttons.DPadLeft, + + /// The right button on the directional pad of a controller. + DPadRight = SButtonExtensions.ControllerOffset + Buttons.DPadRight, + + /// The left bumper (shoulder) button on a controller. + LeftShoulder = SButtonExtensions.ControllerOffset + Buttons.LeftShoulder, + + /// The right bumper (shoulder) button on a controller. + RightShoulder = SButtonExtensions.ControllerOffset + Buttons.RightShoulder, + + /// The left trigger on a controller. + LeftTrigger = SButtonExtensions.ControllerOffset + Buttons.LeftTrigger, + + /// The right trigger on a controller. + RightTrigger = SButtonExtensions.ControllerOffset + Buttons.RightTrigger, + + /// The left analog stick on a controller (when pressed). + LeftStick = SButtonExtensions.ControllerOffset + Buttons.LeftStick, + + /// The right analog stick on a controller (when pressed). + RightStick = SButtonExtensions.ControllerOffset + Buttons.RightStick, + + /// The 'big button' on a controller. + BigButton = SButtonExtensions.ControllerOffset + Buttons.BigButton, + + /// The left analog stick on a controller (when pushed left). + LeftThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickLeft, + + /// The left analog stick on a controller (when pushed right). + LeftThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickRight, + + /// The left analog stick on a controller (when pushed down). + LeftThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickDown, + + /// The left analog stick on a controller (when pushed up). + LeftThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickUp, + + /// The right analog stick on a controller (when pushed left). + RightThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickLeft, + + /// The right analog stick on a controller (when pushed right). + RightThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickRight, + + /// The right analog stick on a controller (when pushed down). + RightThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickDown, + + /// The right analog stick on a controller (when pushed up). + RightThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickUp, + + /********* + ** Keyboard + *********/ + /// The A button on a keyboard. + A = Keys.A, + + /// The Add button on a keyboard. + Add = Keys.Add, + + /// The Applications button on a keyboard. + Apps = Keys.Apps, + + /// The Attn button on a keyboard. + Attn = Keys.Attn, + + /// The B button on a keyboard. + B = Keys.B, + + /// The Backspace button on a keyboard. + Back = Keys.Back, + + /// The Browser Back button on a keyboard in Windows 2000/XP. + BrowserBack = Keys.BrowserBack, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserFavorites = Keys.BrowserFavorites, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserForward = Keys.BrowserForward, + + /// The Browser Home button on a keyboard in Windows 2000/XP. + BrowserHome = Keys.BrowserHome, + + /// The Browser Refresh button on a keyboard in Windows 2000/XP. + BrowserRefresh = Keys.BrowserRefresh, + + /// The Browser Search button on a keyboard in Windows 2000/XP. + BrowserSearch = Keys.BrowserSearch, + + /// The Browser Stop button on a keyboard in Windows 2000/XP. + BrowserStop = Keys.BrowserStop, + + /// The C button on a keyboard. + C = Keys.C, + + /// The Caps Lock button on a keyboard. + CapsLock = Keys.CapsLock, + + /// The Green ChatPad button on a keyboard. + ChatPadGreen = Keys.ChatPadGreen, + + /// The Orange ChatPad button on a keyboard. + ChatPadOrange = Keys.ChatPadOrange, + + /// The CrSel button on a keyboard. + Crsel = Keys.Crsel, + + /// The D button on a keyboard. + D = Keys.D, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D0 = Keys.D0, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D1 = Keys.D1, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D2 = Keys.D2, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D3 = Keys.D3, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D4 = Keys.D4, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D5 = Keys.D5, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D6 = Keys.D6, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D7 = Keys.D7, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D8 = Keys.D8, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D9 = Keys.D9, + + /// The Decimal button on a keyboard. + Decimal = Keys.Decimal, + + /// The Delete button on a keyboard. + Delete = Keys.Delete, + + /// The Divide button on a keyboard. + Divide = Keys.Divide, + + /// The Down arrow button on a keyboard. + Down = Keys.Down, + + /// The E button on a keyboard. + E = Keys.E, + + /// The End button on a keyboard. + End = Keys.End, + + /// The Enter button on a keyboard. + Enter = Keys.Enter, + + /// The Erase EOF button on a keyboard. + EraseEof = Keys.EraseEof, + + /// The Escape button on a keyboard. + Escape = Keys.Escape, + + /// The Execute button on a keyboard. + Execute = Keys.Execute, + + /// The ExSel button on a keyboard. + Exsel = Keys.Exsel, + + /// The F button on a keyboard. + F = Keys.F, + + /// The F1 button on a keyboard. + F1 = Keys.F1, + + /// The F10 button on a keyboard. + F10 = Keys.F10, + + /// The F11 button on a keyboard. + F11 = Keys.F11, + + /// The F12 button on a keyboard. + F12 = Keys.F12, + + /// The F13 button on a keyboard. + F13 = Keys.F13, + + /// The F14 button on a keyboard. + F14 = Keys.F14, + + /// The F15 button on a keyboard. + F15 = Keys.F15, + + /// The F16 button on a keyboard. + F16 = Keys.F16, + + /// The F17 button on a keyboard. + F17 = Keys.F17, + + /// The F18 button on a keyboard. + F18 = Keys.F18, + + /// The F19 button on a keyboard. + F19 = Keys.F19, + + /// The F2 button on a keyboard. + F2 = Keys.F2, + + /// The F20 button on a keyboard. + F20 = Keys.F20, + + /// The F21 button on a keyboard. + F21 = Keys.F21, + + /// The F22 button on a keyboard. + F22 = Keys.F22, + + /// The F23 button on a keyboard. + F23 = Keys.F23, + + /// The F24 button on a keyboard. + F24 = Keys.F24, + + /// The F3 button on a keyboard. + F3 = Keys.F3, + + /// The F4 button on a keyboard. + F4 = Keys.F4, + + /// The F5 button on a keyboard. + F5 = Keys.F5, + + /// The F6 button on a keyboard. + F6 = Keys.F6, + + /// The F7 button on a keyboard. + F7 = Keys.F7, + + /// The F8 button on a keyboard. + F8 = Keys.F8, + + /// The F9 button on a keyboard. + F9 = Keys.F9, + + /// The G button on a keyboard. + G = Keys.G, + + /// The H button on a keyboard. + H = Keys.H, + + /// The Help button on a keyboard. + Help = Keys.Help, + + /// The Home button on a keyboard. + Home = Keys.Home, + + /// The I button on a keyboard. + I = Keys.I, + + /// The IME Convert button on a keyboard. + ImeConvert = Keys.ImeConvert, + + /// The IME NoConvert button on a keyboard. + ImeNoConvert = Keys.ImeNoConvert, + + /// The INS button on a keyboard. + Insert = Keys.Insert, + + /// The J button on a keyboard. + J = Keys.J, + + /// The K button on a keyboard. + K = Keys.K, + + /// The Kana button on a Japanese keyboard. + Kana = Keys.Kana, + + /// The Kanji button on a Japanese keyboard. + Kanji = Keys.Kanji, + + /// The L button on a keyboard. + L = Keys.L, + + /// The Start Applications 1 button on a keyboard in Windows 2000/XP. + LaunchApplication1 = Keys.LaunchApplication1, + + /// The Start Applications 2 button on a keyboard in Windows 2000/XP. + LaunchApplication2 = Keys.LaunchApplication2, + + /// The Start Mail button on a keyboard in Windows 2000/XP. + LaunchMail = Keys.LaunchMail, + + /// The Left arrow button on a keyboard. + Left = Keys.Left, + + /// The Left Alt button on a keyboard. + LeftAlt = Keys.LeftAlt, + + /// The Left Control button on a keyboard. + LeftControl = Keys.LeftControl, + + /// The Left Shift button on a keyboard. + LeftShift = Keys.LeftShift, + + /// The Left Windows button on a keyboard. + LeftWindows = Keys.LeftWindows, + + /// The M button on a keyboard. + M = Keys.M, + + /// The MediaNextTrack button on a keyboard in Windows 2000/XP. + MediaNextTrack = Keys.MediaNextTrack, + + /// The MediaPlayPause button on a keyboard in Windows 2000/XP. + MediaPlayPause = Keys.MediaPlayPause, + + /// The MediaPreviousTrack button on a keyboard in Windows 2000/XP. + MediaPreviousTrack = Keys.MediaPreviousTrack, + + /// The MediaStop button on a keyboard in Windows 2000/XP. + MediaStop = Keys.MediaStop, + + /// The Multiply button on a keyboard. + Multiply = Keys.Multiply, + + /// The N button on a keyboard. + N = Keys.N, + + /// The Num Lock button on a keyboard. + NumLock = Keys.NumLock, + + /// The Numeric keypad 0 button on a keyboard. + NumPad0 = Keys.NumPad0, + + /// The Numeric keypad 1 button on a keyboard. + NumPad1 = Keys.NumPad1, + + /// The Numeric keypad 2 button on a keyboard. + NumPad2 = Keys.NumPad2, + + /// The Numeric keypad 3 button on a keyboard. + NumPad3 = Keys.NumPad3, + + /// The Numeric keypad 4 button on a keyboard. + NumPad4 = Keys.NumPad4, + + /// The Numeric keypad 5 button on a keyboard. + NumPad5 = Keys.NumPad5, + + /// The Numeric keypad 6 button on a keyboard. + NumPad6 = Keys.NumPad6, + + /// The Numeric keypad 7 button on a keyboard. + NumPad7 = Keys.NumPad7, + + /// The Numeric keypad 8 button on a keyboard. + NumPad8 = Keys.NumPad8, + + /// The Numeric keypad 9 button on a keyboard. + NumPad9 = Keys.NumPad9, + + /// The O button on a keyboard. + O = Keys.O, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + Oem8 = Keys.Oem8, + + /// The OEM Auto button on a keyboard. + OemAuto = Keys.OemAuto, + + /// The OEM Angle Bracket or Backslash button on the RT 102 keyboard in Windows 2000/XP. + OemBackslash = Keys.OemBackslash, + + /// The Clear button on a keyboard. + OemClear = Keys.OemClear, + + /// The OEM Close Bracket button on a US standard keyboard in Windows 2000/XP. + OemCloseBrackets = Keys.OemCloseBrackets, + + /// The ',' button on a keyboard in any country/region in Windows 2000/XP. + OemComma = Keys.OemComma, + + /// The OEM Copy button on a keyboard. + OemCopy = Keys.OemCopy, + + /// The OEM Enlarge Window button on a keyboard. + OemEnlW = Keys.OemEnlW, + + /// The '-' button on a keyboard in any country/region in Windows 2000/XP. + OemMinus = Keys.OemMinus, + + /// The OEM Open Bracket button on a US standard keyboard in Windows 2000/XP. + OemOpenBrackets = Keys.OemOpenBrackets, + + /// The '.' button on a keyboard in any country/region. + OemPeriod = Keys.OemPeriod, + + /// The OEM Pipe button on a US standard keyboard. + OemPipe = Keys.OemPipe, + + /// The '+' button on a keyboard in Windows 2000/XP. + OemPlus = Keys.OemPlus, + + /// The OEM Question Mark button on a US standard keyboard. + OemQuestion = Keys.OemQuestion, + + /// The OEM Single/Double Quote button on a US standard keyboard. + OemQuotes = Keys.OemQuotes, + + /// The OEM Semicolon button on a US standard keyboard. + OemSemicolon = Keys.OemSemicolon, + + /// The OEM Tilde button on a US standard keyboard. + OemTilde = Keys.OemTilde, + + /// The P button on a keyboard. + P = Keys.P, + + /// The PA1 button on a keyboard. + Pa1 = Keys.Pa1, + + /// The Page Down button on a keyboard. + PageDown = Keys.PageDown, + + /// The Page Up button on a keyboard. + PageUp = Keys.PageUp, + + /// The Pause button on a keyboard. + Pause = Keys.Pause, + + /// The Play button on a keyboard. + Play = Keys.Play, + + /// The Print button on a keyboard. + Print = Keys.Print, + + /// The Print Screen button on a keyboard. + PrintScreen = Keys.PrintScreen, + + /// The IME Process button on a keyboard in Windows 95/98/ME/NT 4.0/2000/XP. + ProcessKey = Keys.ProcessKey, + + /// The Q button on a keyboard. + Q = Keys.Q, + + /// The R button on a keyboard. + R = Keys.R, + + /// The Right Arrow button on a keyboard. + Right = Keys.Right, + + /// The Right Alt button on a keyboard. + RightAlt = Keys.RightAlt, + + /// The Right Control button on a keyboard. + RightControl = Keys.RightControl, + + /// The Right Shift button on a keyboard. + RightShift = Keys.RightShift, + + /// The Right Windows button on a keyboard. + RightWindows = Keys.RightWindows, + + /// The S button on a keyboard. + S = Keys.S, + + /// The Scroll Lock button on a keyboard. + Scroll = Keys.Scroll, + + /// The Select button on a keyboard. + Select = Keys.Select, + + /// The Select Media button on a keyboard in Windows 2000/XP. + SelectMedia = Keys.SelectMedia, + + /// The Separator button on a keyboard. + Separator = Keys.Separator, + + /// The Computer Sleep button on a keyboard. + Sleep = Keys.Sleep, + + /// The Space bar on a keyboard. + Space = Keys.Space, + + /// The Subtract button on a keyboard. + Subtract = Keys.Subtract, + + /// The T button on a keyboard. + T = Keys.T, + + /// The Tab button on a keyboard. + Tab = Keys.Tab, + + /// The U button on a keyboard. + U = Keys.U, + + /// The Up Arrow button on a keyboard. + Up = Keys.Up, + + /// The V button on a keyboard. + V = Keys.V, + + /// The Volume Down button on a keyboard in Windows 2000/XP. + VolumeDown = Keys.VolumeDown, + + /// The Volume Mute button on a keyboard in Windows 2000/XP. + VolumeMute = Keys.VolumeMute, + + /// The Volume Up button on a keyboard in Windows 2000/XP. + VolumeUp = Keys.VolumeUp, + + /// The W button on a keyboard. + W = Keys.W, + + /// The X button on a keyboard. + X = Keys.X, + + /// The Y button on a keyboard. + Y = Keys.Y, + + /// The Z button on a keyboard. + Z = Keys.Z, + + /// The Zoom button on a keyboard. + Zoom = Keys.Zoom + } + + /// Provides extension methods for . +#if SMAPI_2_0 + public +#else + internal +#endif + static class SButtonExtensions + { + /********* + ** Accessors + *********/ + /// The offset added to values when converting them to to avoid collisions with values. + internal const int ControllerOffset = 2000; + + + /********* + ** Public methods + *********/ + /// Get the equivalent for the given button. + /// The keyboard button to convert. + internal static SButton ToSButton(this Keys key) + { + return (SButton)key; + } + + /// Get the equivalent for the given button. + /// The controller button to convert. + internal static SButton ToSButton(this Buttons key) + { + return (SButton)(SButtonExtensions.ControllerOffset + key); + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The keyboard equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetKeyboard(this SButton input, out Keys key) + { + if (Enum.IsDefined(typeof(Keys), (int)input)) + { + key = (Keys)input; + return true; + } + + key = Keys.None; + return false; + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The controller equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetController(this SButton input, out Buttons button) + { + if (Enum.IsDefined(typeof(Keys), (int)input - SButtonExtensions.ControllerOffset)) + { + button = (Buttons)(input - SButtonExtensions.ControllerOffset); + return true; + } + + button = 0; + return false; + } + } +} -- cgit From e2b9a4bab3e078851a289ad0a19b555dde09308e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Jul 2017 15:17:47 -0400 Subject: serialise SButtons as string in config.json (#316) --- src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs index 6431394c..3193aa3c 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; +using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.Serialisation { @@ -19,7 +20,7 @@ namespace StardewModdingAPI.Framework.Serialisation ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded Converters = new List { - new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys)) + new SelectiveStringEnumConverter(typeof(Buttons), typeof(Keys), typeof(SButton)) } }; -- cgit From d928bf188e9ab171223bc07d7209d2887d954642 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 6 Jul 2017 17:46:04 -0400 Subject: add optional mod dependencies in SMAPI 2.0 (#287) --- release-notes.md | 4 ++- .../Core/ModResolverTests.cs | 34 ++++++++++++++++++++++ .../Framework/ModLoading/ModResolver.cs | 25 ++++++++++++---- .../Framework/Models/ManifestDependency.cs | 14 ++++++++- .../Serialisation/ManifestFieldConverter.cs | 5 ++++ src/StardewModdingAPI/IManifestDependency.cs | 5 ++++ 6 files changed, 80 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index dcc21aaf..ca0e00f7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,7 +11,9 @@ For mod developers: * Added `InputEvents` which unify keyboard, mouse, and controller input for much simpler input handling (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Input_events)). * Added useful `InputEvents` metadata like the cursor position, grab tile, etc. * Added ability to prevent the game from handling a button press via `InputEvents`. -* The `manifest.json` version can now be specified as a string. +* In `manifest.json`: + * Dependencies can now be optional. + * The version can now be a string like `"1.0-alpha"` instead of a structure. * Removed all deprecated code. ## 1.15 diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 36cc3495..b451465e 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -411,6 +411,40 @@ namespace StardewModdingAPI.Tests.Core Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); } +#if SMAPI_2_0 + [Test(Description = "Assert that optional dependencies are sorted correctly if present.")] + public void ProcessDependencies_IfOptional() + { + // arrange + // A ◀── B + Mock modA = this.GetMetadata(this.GetManifest("Mod A", "1.0")); + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object, modA.Object }).ToArray(); + + // assert + Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); + } + + [Test(Description = "Assert that optional dependencies are accepted if they're missing.")] + public void ProcessDependencies_IfOptional_SucceedsIfMissing() + { + // arrange + // A ◀── B where A doesn't exist + Mock modB = this.GetMetadata(this.GetManifest("Mod B", "1.0", new ManifestDependency("Mod A", "1.0", required: false)), allowStatusChange: false); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modB.Object }).ToArray(); + + // assert + Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod."); + } +#endif + /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 9c56aaa4..38dddce7 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -228,13 +228,24 @@ namespace StardewModdingAPI.Framework.ModLoading from entry in mod.Manifest.Dependencies let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) orderby entry.UniqueID - select new { ID = entry.UniqueID, MinVersion = entry.MinimumVersion, Mod = dependencyMod } + select new + { + ID = entry.UniqueID, + MinVersion = entry.MinimumVersion, + Mod = dependencyMod, + IsRequired = +#if SMAPI_2_0 + entry.IsRequired +#else + true +#endif + } ) .ToArray(); // missing required dependencies, mark failed { - string[] failedIDs = (from entry in dependencies where entry.Mod == null select entry.ID).ToArray(); + string[] failedIDs = (from entry in dependencies where entry.IsRequired && entry.Mod == null select entry.ID).ToArray(); if (failedIDs.Any()) { sortedMods.Push(mod); @@ -248,7 +259,7 @@ namespace StardewModdingAPI.Framework.ModLoading string[] failedLabels = ( from entry in dependencies - where entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) + where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" ) .ToArray(); @@ -265,11 +276,15 @@ namespace StardewModdingAPI.Framework.ModLoading states[mod] = ModDependencyStatus.Checking; // recursively sort dependencies - IModMetadata[] modsToLoadFirst = dependencies.Select(p => p.Mod).ToArray(); - foreach (IModMetadata requiredMod in modsToLoadFirst) + foreach (var dependency in dependencies) { + IModMetadata requiredMod = dependency.Mod; var subchain = new List(currentChain) { mod }; + // ignore missing optional dependency + if (!dependency.IsRequired && requiredMod == null) + continue; + // detect dependency loop if (states[requiredMod] == ModDependencyStatus.Checking) { diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs index a0ff0c90..25d92a29 100644 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs @@ -12,6 +12,10 @@ /// The minimum required version (if any). public ISemanticVersion MinimumVersion { get; set; } +#if SMAPI_2_0 + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } +#endif /********* ** Public methods @@ -19,12 +23,20 @@ /// Construct an instance. /// The unique mod ID to require. /// The minimum required version (if any). - public ManifestDependency(string uniqueID, string minimumVersion) + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion +#if SMAPI_2_0 + , bool required = true +#endif + ) { this.UniqueID = uniqueID; this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) ? new SemanticVersion(minimumVersion) : null; +#if SMAPI_2_0 + this.IsRequired = required; +#endif } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index e6d62d50..5be0f0b6 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -73,7 +73,12 @@ namespace StardewModdingAPI.Framework.Serialisation { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); +#if SMAPI_2_0 + bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); +#else result.Add(new ManifestDependency(uniqueID, minVersion)); +#endif } return result.ToArray(); } diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs index ebb1140e..027c1d59 100644 --- a/src/StardewModdingAPI/IManifestDependency.cs +++ b/src/StardewModdingAPI/IManifestDependency.cs @@ -11,5 +11,10 @@ /// The minimum required version (if any). ISemanticVersion MinimumVersion { get; } + +#if SMAPI_2_0 + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } +#endif } } -- cgit From 3b8d1e49f025e56e0442604c2e6956d77d673b0d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 01:54:53 -0400 Subject: make deprecation warnings a bit less scary until we finish first-pass SMAPI 2.0 migration --- src/StardewModdingAPI/Framework/DeprecationManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs index cc6754b5..8750824c 100644 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -54,10 +54,10 @@ namespace StardewModdingAPI.Framework // show SMAPI 2.0 meta-warning if(this.MarkWarned("SMAPI", "SMAPI 2.0 meta-warning", "2.0")) - this.Monitor.Log("Some of your mods will break in the upcoming SMAPI 2.0 release because they use obsolete APIs. Please check for a newer version of any mod showing 'may break' warnings, or let the author know about this message. For more information, see http://community.playstarbound.com/threads/135000.", LogLevel.Warn); + this.Monitor.Log("Some of your mods use deprecated code that will stop working in a future SMAPI release. Try updating mods with 'deprecated code' warnings or let the mod authors know about this message.", LogLevel.Warn); // build message - string message = $"{source ?? "An unknown mod"} may break in the upcoming SMAPI 2.0 release (detected {nounPhrase})."; + string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; if (source == null) message += $"{Environment.NewLine}{Environment.StackTrace}"; -- cgit From c5e106801e9137078decfd6b6e3761240b47f94e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 11:29:17 -0400 Subject: split reflection logic out of mod helper (#318) --- .../Framework/InternalExtensions.cs | 5 +- .../Framework/Reflection/ReflectionHelper.cs | 316 --------------------- .../Framework/Reflection/Reflector.cs | 276 ++++++++++++++++++ src/StardewModdingAPI/Framework/SContentManager.cs | 2 +- src/StardewModdingAPI/Framework/SGame.cs | 5 +- src/StardewModdingAPI/Program.cs | 5 +- src/StardewModdingAPI/ReflectionHelper.cs | 158 +++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 3 +- 8 files changed, 446 insertions(+), 324 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/Reflector.cs create mode 100644 src/StardewModdingAPI/ReflectionHelper.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index b99d3798..2842bc83 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; using StardewValley; namespace StardewModdingAPI.Framework @@ -99,7 +100,7 @@ namespace StardewModdingAPI.Framework /// Get whether the sprite batch is between a begin and end pair. /// The sprite batch to check. /// The reflection helper with which to access private fields. - public static bool IsOpen(this SpriteBatch spriteBatch, IReflectionHelper reflection) + public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection) { // get field name const string fieldName = @@ -110,7 +111,7 @@ namespace StardewModdingAPI.Framework #endif // get result - return reflection.GetPrivateValue(Game1.spriteBatch, fieldName); + return reflection.GetPrivateField(Game1.spriteBatch, fieldName).GetValue(); } } } diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs deleted file mode 100644 index 7a5789dc..00000000 --- a/src/StardewModdingAPI/Framework/Reflection/ReflectionHelper.cs +++ /dev/null @@ -1,316 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Runtime.Caching; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// Provides helper methods for accessing private game code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). - internal class ReflectionHelper : IReflectionHelper - { - /********* - ** Properties - *********/ - /// The cached fields and methods found via reflection. - private readonly MemoryCache Cache = new MemoryCache(typeof(ReflectionHelper).FullName); - - /// The sliding cache expiration time. - private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); - - - /********* - ** Public methods - *********/ - /**** - ** Fields - ****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field wrapper, or null if the field doesn't exist and is false. - public IPrivateField GetPrivateField(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); - - // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && field == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); - return field; - } - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) - { - // get field from hierarchy - IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && field == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); - return field; - } - - /**** - ** Properties - ****/ - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); - - // get property from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && property == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); - return property; - } - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) - { - // get field from hierarchy - IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && property == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); - return property; - } - - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(object obj, string name, bool required = true) - { - IPrivateField field = this.GetPrivateField(obj, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(Type type, string name, bool required = true) - { - IPrivateField field = this.GetPrivateField(type, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /**** - ** Methods - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) - { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); - - // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); - if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); - return method; - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) - { - // get method from hierarchy - IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); - if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); - return method; - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - // validate parent - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); - - // get method from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); - if (required && method == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); - return method; - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - // get field from hierarchy - PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); - if (required && method == null) - throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); - return method; - } - - - /********* - ** Private methods - *********/ - /// Get a field from the type hierarchy. - /// The expected field type. - /// The type which has the field. - /// The object which has the field. - /// The field name. - /// The reflection binding which flags which indicates what type of field to find. - private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => - { - FieldInfo fieldInfo = null; - for (; type != null && fieldInfo == null; type = type.BaseType) - fieldInfo = type.GetField(name, bindingFlags); - return fieldInfo; - }); - - return field != null - ? new PrivateField(type, obj, field, isStatic) - : null; - } - - /// Get a property from the type hierarchy. - /// The expected property type. - /// The type which has the property. - /// The object which has the property. - /// The property name. - /// The reflection binding which flags which indicates what type of property to find. - private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => - { - PropertyInfo propertyInfo = null; - for (; type != null && propertyInfo == null; type = type.BaseType) - propertyInfo = type.GetProperty(name, bindingFlags); - return propertyInfo; - }); - - return property != null - ? new PrivateProperty(type, obj, property, isStatic) - : null; - } - - /// Get a method from the type hierarchy. - /// The type which has the method. - /// The object which has the method. - /// The method name. - /// The reflection binding which flags which indicates what type of method to find. - private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => - { - MethodInfo methodInfo = null; - for (; type != null && methodInfo == null; type = type.BaseType) - methodInfo = type.GetMethod(name, bindingFlags); - return methodInfo; - }); - - return method != null - ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) - : null; - } - - /// Get a method from the type hierarchy. - /// The type which has the method. - /// The object which has the method. - /// The method name. - /// The reflection binding which flags which indicates what type of method to find. - /// The argument types of the method signature to find. - private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) - { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => - { - MethodInfo methodInfo = null; - for (; type != null && methodInfo == null; type = type.BaseType) - methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); - return methodInfo; - }); - return method != null - ? new PrivateMethod(type, obj, method, isStatic) - : null; - } - - /// Get a method or field through the cache. - /// The expected type. - /// The cache key. - /// Fetches a new value to cache. - private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo - { - // get from cache - if (this.Cache.Contains(key)) - { - CacheEntry entry = (CacheEntry)this.Cache[key]; - return entry.IsValid - ? (TMemberInfo)entry.MemberInfo - : default(TMemberInfo); - } - - // fetch & cache new value - TMemberInfo result = fetch(); - CacheEntry cacheEntry = new CacheEntry(result != null, result); - this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); - return result; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs new file mode 100644 index 00000000..5c2d90fa --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Caching; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class Reflector + { + /********* + ** Properties + *********/ + /// The cached fields and methods found via reflection. + private readonly MemoryCache Cache = new MemoryCache(typeof(Reflector).FullName); + + /// The sliding cache expiration time. + private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + + + /********* + ** Public methods + *********/ + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance field from a null object."); + + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && field == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance field."); + return field; + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateField field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && field == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static field."); + return field; + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance property from a null object."); + + // get property from hierarchy + IPrivateProperty property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && property == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance property."); + return property; + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + // get field from hierarchy + IPrivateProperty property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && property == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static property."); + return property; + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + // get method from hierarchy + IPrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method."); + return method; + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + // validate parent + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a private instance method from a null object."); + + // get method from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a private '{name}' instance method with that signature."); + return method; + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + // get field from hierarchy + PrivateMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static, argumentTypes); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a private '{name}' static method with that signature."); + return method; + } + + + /********* + ** Private methods + *********/ + /// Get a field from the type hierarchy. + /// The expected field type. + /// The type which has the field. + /// The object which has the field. + /// The field name. + /// The reflection binding which flags which indicates what type of field to find. + private IPrivateField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + FieldInfo field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () => + { + FieldInfo fieldInfo = null; + for (; type != null && fieldInfo == null; type = type.BaseType) + fieldInfo = type.GetField(name, bindingFlags); + return fieldInfo; + }); + + return field != null + ? new PrivateField(type, obj, field, isStatic) + : null; + } + + /// Get a property from the type hierarchy. + /// The expected property type. + /// The type which has the property. + /// The object which has the property. + /// The property name. + /// The reflection binding which flags which indicates what type of property to find. + private IPrivateProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => + { + PropertyInfo propertyInfo = null; + for (; type != null && propertyInfo == null; type = type.BaseType) + propertyInfo = type.GetProperty(name, bindingFlags); + return propertyInfo; + }); + + return property != null + ? new PrivateProperty(type, obj, property, isStatic) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + private IPrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags); + return methodInfo; + }); + + return method != null + ? new PrivateMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + /// The argument types of the method signature to find. + private PrivateMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () => + { + MethodInfo methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null); + return methodInfo; + }); + return method != null + ? new PrivateMethod(type, obj, method, isStatic) + : null; + } + + /// Get a method or field through the cache. + /// The expected type. + /// The cache key. + /// Fetches a new value to cache. + private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo + { + // get from cache + if (this.Cache.Contains(key)) + { + CacheEntry entry = (CacheEntry)this.Cache[key]; + return entry.IsValid + ? (TMemberInfo)entry.MemberInfo + : default(TMemberInfo); + } + + // fetch & cache new value + TMemberInfo result = fetch(); + CacheEntry cacheEntry = new CacheEntry(result != null, result); + this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry }); + return result; + } + } +} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 42c3b0e6..669b0e7a 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Framework throw new ArgumentNullException(nameof(monitor)); // initialise - IReflectionHelper reflection = new ReflectionHelper(); + var reflection = new Reflector(); this.Monitor = monitor; // get underlying fields for interception diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f2c5c0c9..c7784c60 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -10,6 +10,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -150,7 +151,7 @@ namespace StardewModdingAPI.Framework ** Private wrappers ****/ /// Simplifies access to private game code. - private static IReflectionHelper Reflection; + private static Reflector Reflection; // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. @@ -184,7 +185,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging. /// Simplifies access to private game code. - internal SGame(IMonitor monitor, IReflectionHelper reflection) + internal SGame(IMonitor monitor, Reflector reflection) { // initialise this.Monitor = monitor; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 4b50e4fb..3b3f99b3 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); /// Simplifies access to private game code. - private readonly IReflectionHelper Reflection = new ReflectionHelper(); + private readonly Reflector Reflection = new Reflector(); /// The underlying game instance. private SGame GameInstance; @@ -702,7 +702,8 @@ namespace StardewModdingAPI // inject data mod.ModManifest = manifest; - mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); + var reflectionHelper = new ReflectionHelper(this.Reflection); + mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, reflectionHelper); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); #if !SMAPI_2_0 mod.PathOnDisk = metadata.DirectoryPath; diff --git a/src/StardewModdingAPI/ReflectionHelper.cs b/src/StardewModdingAPI/ReflectionHelper.cs new file mode 100644 index 00000000..56754cb4 --- /dev/null +++ b/src/StardewModdingAPI/ReflectionHelper.cs @@ -0,0 +1,158 @@ +using System; +using StardewModdingAPI.Framework.Reflection; + +namespace StardewModdingAPI +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class ReflectionHelper : IReflectionHelper + { + /********* + ** Properties + *********/ + /// The underlying reflection helper. + private readonly Reflector Reflector; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying reflection helper. + public ReflectionHelper(Reflector reflector) + { + this.Reflector = reflector; + } + + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateField(obj, name, required); + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateField(type, name, required); + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateProperty(obj, name, required); + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateProperty(type, name, required); + } + + /**** + ** Field values + ** (shorthand since this is the most common case) + ****/ + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(object obj, string name, bool required = true) + { + IPrivateField field = this.GetPrivateField(obj, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(Type type, string name, bool required = true) + { + IPrivateField field = this.GetPrivateField(type, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateMethod(obj, name, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateMethod(type, name, required); + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index c442cc8a..efef87b1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -149,6 +149,7 @@ + @@ -180,7 +181,7 @@ - + -- cgit From f033b5a2f72b96168f6e20e96fa50742e70b01d6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 11:39:09 -0400 Subject: group mod helpers (#318) --- .../Core/TranslationTests.cs | 1 + src/StardewModdingAPI/Framework/CommandHelper.cs | 53 ---- src/StardewModdingAPI/Framework/ContentHelper.cs | 351 --------------------- src/StardewModdingAPI/Framework/ModHelper.cs | 131 -------- .../Framework/ModHelpers/CommandHelper.cs | 52 +++ .../Framework/ModHelpers/ContentHelper.cs | 351 +++++++++++++++++++++ .../Framework/ModHelpers/ModHelper.cs | 131 ++++++++ .../Framework/ModHelpers/ReflectionHelper.cs | 158 ++++++++++ .../Framework/ModHelpers/TranslationHelper.cs | 138 ++++++++ .../Framework/TranslationHelper.cs | 138 -------- src/StardewModdingAPI/Program.cs | 1 + src/StardewModdingAPI/ReflectionHelper.cs | 158 ---------- src/StardewModdingAPI/StardewModdingAPI.csproj | 12 +- 13 files changed, 838 insertions(+), 837 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/CommandHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ContentHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/ModHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/TranslationHelper.cs delete mode 100644 src/StardewModdingAPI/ReflectionHelper.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs index ce3431e4..fceef0a3 100644 --- a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.ModHelpers; using StardewValley; namespace StardewModdingAPI.Tests.Core diff --git a/src/StardewModdingAPI/Framework/CommandHelper.cs b/src/StardewModdingAPI/Framework/CommandHelper.cs deleted file mode 100644 index 86734fc5..00000000 --- a/src/StardewModdingAPI/Framework/CommandHelper.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework -{ - /// Provides an API for managing console commands. - internal class CommandHelper : ICommandHelper - { - /********* - ** Accessors - *********/ - /// The friendly mod name for this instance. - private readonly string ModName; - - /// Manages console commands. - private readonly CommandManager CommandManager; - - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The friendly mod name for this instance. - /// Manages console commands. - public CommandHelper(string modName, CommandManager commandManager) - { - this.ModName = modName; - this.CommandManager = commandManager; - } - - /// Add a console command. - /// The command name, which the user must type to trigger it. - /// The human-readable documentation shown when the player runs the built-in 'help' command. - /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. - /// The or is null or empty. - /// The is not a valid format. - /// There's already a command with that name. - public ICommandHelper Add(string name, string documentation, Action callback) - { - this.CommandManager.Add(this.ModName, name, documentation, callback); - return this; - } - - /// Trigger a command. - /// The command name. - /// The command arguments. - /// Returns whether a matching command was triggered. - public bool Trigger(string name, string[] arguments) - { - return this.CommandManager.Trigger(name, arguments); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs deleted file mode 100644 index 0c09fe94..00000000 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework.Exceptions; -using StardewValley; -using xTile; -using xTile.Format; -using xTile.Tiles; - -namespace StardewModdingAPI.Framework -{ - /// Provides an API for loading content assets. - internal class ContentHelper : IContentHelper - { - /********* - ** Properties - *********/ - /// SMAPI's underlying content manager. - private readonly SContentManager ContentManager; - - /// The absolute path to the mod folder. - private readonly string ModFolderPath; - - /// The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName"). - private readonly string ModFolderPathFromContent; - - /// The friendly mod name for use in errors. - private readonly string ModName; - - - /********* - ** Accessors - *********/ - /// The observable implementation of . - internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); - - /// The observable implementation of . - internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); - - /// Interceptors which provide the initial versions of matching content assets. - internal IList AssetLoaders => this.ObservableAssetLoaders; - - /// Interceptors which edit matching content assets after they're loaded. - internal IList AssetEditors => this.ObservableAssetEditors; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// SMAPI's underlying content manager. - /// The absolute path to the mod folder. - /// The friendly mod name for use in errors. - public ContentHelper(SContentManager contentManager, string modFolderPath, string modName) - { - this.ContentManager = contentManager; - this.ModFolderPath = modFolderPath; - this.ModName = modName; - this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); - } - - /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. - /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - /// The content asset couldn't be loaded (e.g. because it doesn't exist). - public T Load(string key, ContentSource source = ContentSource.ModFolder) - { - SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); - - this.AssertValidAssetKeyFormat(key); - try - { - switch (source) - { - case ContentSource.GameContent: - return this.ContentManager.Load(key); - - case ContentSource.ModFolder: - // get file - FileInfo file = this.GetModFile(key); - if (!file.Exists) - throw GetContentError($"there's no matching file at path '{file.FullName}'."); - - // get asset path - string assetPath = this.GetModAssetPath(key, file.FullName); - - // try cache - if (this.ContentManager.IsLoaded(assetPath)) - return this.ContentManager.Load(assetPath); - - // load content - switch (file.Extension.ToLower()) - { - // XNB file - case ".xnb": - { - T asset = this.ContentManager.Load(assetPath); - if (asset is Map) - this.FixLocalMapTilesheets(asset as Map, key); - return asset; - } - - // unpacked map - case ".tbin": - { - // validate - if (typeof(T) != typeof(Map)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); - - // fetch & cache - FormatManager formatManager = FormatManager.Instance; - Map map = formatManager.LoadMap(file.FullName); - this.FixLocalMapTilesheets(map, key); - - // inject map - this.ContentManager.Inject(assetPath, map); - return (T)(object)map; - } - - // unpacked image - case ".png": - // validate - if (typeof(T) != typeof(Texture2D)) - throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - - // fetch & cache - using (FileStream stream = File.OpenRead(file.FullName)) - { - Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); - texture = this.PremultiplyTransparency(texture); - this.ContentManager.Inject(assetPath, texture); - return (T)(object)texture; - } - - default: - throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); - } - - default: - throw GetContentError($"unknown content source '{source}'."); - } - } - catch (Exception ex) when (!(ex is SContentLoadException)) - { - throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); - } - } - - /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. - /// The is empty or contains invalid characters. - public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) - { - switch (source) - { - case ContentSource.GameContent: - return this.ContentManager.NormaliseAssetName(key); - - case ContentSource.ModFolder: - FileInfo file = this.GetModFile(key); - return this.ContentManager.NormaliseAssetName(this.GetModAssetPath(key, file.FullName)); - - default: - throw new NotSupportedException($"Unknown content source '{source}'."); - } - } - - - /********* - ** Private methods - *********/ - /// Fix the tilesheets for a map loaded from the mod folder. - /// The map whose tilesheets to fix. - /// The map asset key within the mod folder. - /// The map tilesheets could not be loaded. - private void FixLocalMapTilesheets(Map map, string mapKey) - { - if (!map.TileSheets.Any()) - return; - - string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder - foreach (TileSheet tilesheet in map.TileSheets) - { - // check for tilesheet relative to map - { - string localKey = Path.Combine(relativeMapFolder, tilesheet.ImageSource); - FileInfo localFile = this.GetModFile(localKey); - if (localFile.Exists) - { - try - { - this.Load(localKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); - } - tilesheet.ImageSource = this.GetActualAssetKey(localKey); - continue; - } - } - - // fallback to game content - { - string contentKey = tilesheet.ImageSource; - if (contentKey.EndsWith(".png")) - contentKey = contentKey.Substring(0, contentKey.Length - 4); - try - { - this.ContentManager.Load(contentKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The '{tilesheet.ImageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); - } - tilesheet.ImageSource = contentKey; - } - } - } - - /// Assert that the given key has a valid format. - /// The asset key to check. - /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertValidAssetKeyFormat(string key) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("The asset key or local path is empty."); - if (key.Intersect(Path.GetInvalidPathChars()).Any()) - throw new ArgumentException("The asset key or local path contains invalid characters."); - } - - /// Get a file from the mod folder. - /// The asset path relative to the mod folder. - private FileInfo GetModFile(string path) - { - // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); - FileInfo file = new FileInfo(path); - - // try with default extension - if (!file.Exists && file.Extension.ToLower() != ".xnb") - { - FileInfo result = new FileInfo(path + ".xnb"); - if (result.Exists) - file = result; - } - - return file; - } - - /// Get the asset path which loads a mod folder through a content manager. - /// The file path relative to the mod's folder. - /// The absolute file path. - private string GetModAssetPath(string localPath, string absolutePath) - { -#if SMAPI_FOR_WINDOWS - // XNA doesn't allow absolute asset paths, so get a path relative to the content folder - return Path.Combine(this.ModFolderPathFromContent, localPath); -#else - // MonoGame is weird about relative paths on Mac, but allows absolute paths - return absolutePath; -#endif - } - - /// Get a directory path relative to a given root. - /// The root path from which the path should be relative. - /// The target file path. - private string GetRelativePath(string rootPath, string targetPath) - { - // convert to URIs - Uri from = new Uri(rootPath + "/"); - Uri to = new Uri(targetPath + "/"); - if (from.Scheme != to.Scheme) - throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); - - // get relative path - return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) - .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform - } - - /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by Layoric. - private Texture2D PremultiplyTransparency(Texture2D texture) - { - // validate - if (Context.IsInDrawLoop) - throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); - - // process texture - SpriteBatch spriteBatch = Game1.spriteBatch; - GraphicsDevice gpu = Game1.graphics.GraphicsDevice; - using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) - { - // create blank render target to premultiply - gpu.SetRenderTarget(renderTarget); - gpu.Clear(Color.Black); - - // multiply each color by the source alpha, and write just the color values into the final texture - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorDestinationBlend = Blend.Zero, - ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, - AlphaDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.SourceAlpha, - ColorSourceBlend = Blend.SourceAlpha - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // copy the alpha values from the source texture into the final one without multiplying them - spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState - { - ColorWriteChannels = ColorWriteChannels.Alpha, - AlphaDestinationBlend = Blend.Zero, - ColorDestinationBlend = Blend.Zero, - AlphaSourceBlend = Blend.One, - ColorSourceBlend = Blend.One - }); - spriteBatch.Draw(texture, texture.Bounds, Color.White); - spriteBatch.End(); - - // release GPU - gpu.SetRenderTarget(null); - - // extract premultiplied data - Color[] data = new Color[texture.Width * texture.Height]; - renderTarget.GetData(data); - - // unset texture from GPU to regain control - gpu.Textures[0] = null; - - // update texture with premultiplied data - texture.SetData(data); - } - - return texture; - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs deleted file mode 100644 index 5a8ce459..00000000 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.IO; -using StardewModdingAPI.Framework.Serialisation; - -namespace StardewModdingAPI.Framework -{ - /// Provides simplified APIs for writing mods. - internal class ModHelper : IModHelper, IDisposable - { - /********* - ** Properties - *********/ - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper; - - - /********* - ** Accessors - *********/ - /// The full path to the mod's folder. - public string DirectoryPath { get; } - - /// An API for loading content assets. - public IContentHelper Content { get; } - - /// Simplifies access to private game code. - public IReflectionHelper Reflection { get; } - - /// Metadata about loaded mods. - public IModRegistry ModRegistry { get; } - - /// An API for managing console commands. - public ICommandHelper ConsoleCommands { get; } - - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - public ITranslationHelper Translation { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's display name. - /// The full path to the mod's folder. - /// Encapsulate SMAPI's JSON parsing. - /// Metadata about loaded mods. - /// Manages console commands. - /// The content manager which loads content assets. - /// Simplifies access to private game code. - /// An argument is null or empty. - /// The path does not exist on disk. - public ModHelper(string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) - { - // validate - if (string.IsNullOrWhiteSpace(modDirectory)) - throw new ArgumentNullException(nameof(modDirectory)); - if (jsonHelper == null) - throw new ArgumentNullException(nameof(jsonHelper)); - if (modRegistry == null) - throw new ArgumentNullException(nameof(modRegistry)); - if (!Directory.Exists(modDirectory)) - throw new InvalidOperationException("The specified mod directory does not exist."); - - // initialise - this.DirectoryPath = modDirectory; - this.JsonHelper = jsonHelper; - this.Content = new ContentHelper(contentManager, modDirectory, displayName); - this.ModRegistry = modRegistry; - this.ConsoleCommands = new CommandHelper(displayName, commandManager); - this.Reflection = reflection; - this.Translation = new TranslationHelper(displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); - } - - /**** - ** Mod config file - ****/ - /// Read the mod's configuration file (and create it if needed). - /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. - public TConfig ReadConfig() - where TConfig : class, new() - { - TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); - this.WriteConfig(config); // create file or fill in missing fields - return config; - } - - /// Save to the mod's configuration file. - /// The config class type. - /// The config settings to save. - public void WriteConfig(TConfig config) - where TConfig : class, new() - { - this.WriteJsonFile("config.json", config); - } - - /**** - ** Generic JSON files - ****/ - /// Read a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - public TModel ReadJsonFile(string path) - where TModel : class - { - path = Path.Combine(this.DirectoryPath, path); - return this.JsonHelper.ReadJsonFile(path); - } - - /// Save to a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// The model to save. - public void WriteJsonFile(string path, TModel model) - where TModel : class - { - path = Path.Combine(this.DirectoryPath, path); - this.JsonHelper.WriteJsonFile(path, model); - } - - - /**** - ** Disposal - ****/ - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - // nothing to dispose yet - } - } -} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs new file mode 100644 index 00000000..5fd56fdf --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs @@ -0,0 +1,52 @@ +using System; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for managing console commands. + internal class CommandHelper : ICommandHelper + { + /********* + ** Accessors + *********/ + /// The friendly mod name for this instance. + private readonly string ModName; + + /// Manages console commands. + private readonly CommandManager CommandManager; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The friendly mod name for this instance. + /// Manages console commands. + public CommandHelper(string modName, CommandManager commandManager) + { + this.ModName = modName; + this.CommandManager = commandManager; + } + + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public ICommandHelper Add(string name, string documentation, Action callback) + { + this.CommandManager.Add(this.ModName, name, documentation, callback); + return this; + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + return this.CommandManager.Trigger(name, arguments); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs new file mode 100644 index 00000000..4fc46dd0 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Exceptions; +using StardewValley; +using xTile; +using xTile.Format; +using xTile.Tiles; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for loading content assets. + internal class ContentHelper : IContentHelper + { + /********* + ** Properties + *********/ + /// SMAPI's underlying content manager. + private readonly SContentManager ContentManager; + + /// The absolute path to the mod folder. + private readonly string ModFolderPath; + + /// The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName"). + private readonly string ModFolderPathFromContent; + + /// The friendly mod name for use in errors. + private readonly string ModName; + + + /********* + ** Accessors + *********/ + /// The observable implementation of . + internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); + + /// The observable implementation of . + internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); + + /// Interceptors which provide the initial versions of matching content assets. + internal IList AssetLoaders => this.ObservableAssetLoaders; + + /// Interceptors which edit matching content assets after they're loaded. + internal IList AssetEditors => this.ObservableAssetEditors; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's underlying content manager. + /// The absolute path to the mod folder. + /// The friendly mod name for use in errors. + public ContentHelper(SContentManager contentManager, string modFolderPath, string modName) + { + this.ContentManager = contentManager; + this.ModFolderPath = modFolderPath; + this.ModName = modName; + this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + } + + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T Load(string key, ContentSource source = ContentSource.ModFolder) + { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); + + this.AssertValidAssetKeyFormat(key); + try + { + switch (source) + { + case ContentSource.GameContent: + return this.ContentManager.Load(key); + + case ContentSource.ModFolder: + // get file + FileInfo file = this.GetModFile(key); + if (!file.Exists) + throw GetContentError($"there's no matching file at path '{file.FullName}'."); + + // get asset path + string assetPath = this.GetModAssetPath(key, file.FullName); + + // try cache + if (this.ContentManager.IsLoaded(assetPath)) + return this.ContentManager.Load(assetPath); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + { + T asset = this.ContentManager.Load(assetPath); + if (asset is Map) + this.FixLocalMapTilesheets(asset as Map, key); + return asset; + } + + // unpacked map + case ".tbin": + { + // validate + if (typeof(T) != typeof(Map)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + this.FixLocalMapTilesheets(map, key); + + // inject map + this.ContentManager.Inject(assetPath, map); + return (T)(object)map; + } + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.ContentManager.Inject(assetPath, texture); + return (T)(object)texture; + } + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + + default: + throw GetContentError($"unknown content source '{source}'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); + } + } + + /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) + { + switch (source) + { + case ContentSource.GameContent: + return this.ContentManager.NormaliseAssetName(key); + + case ContentSource.ModFolder: + FileInfo file = this.GetModFile(key); + return this.ContentManager.NormaliseAssetName(this.GetModAssetPath(key, file.FullName)); + + default: + throw new NotSupportedException($"Unknown content source '{source}'."); + } + } + + + /********* + ** Private methods + *********/ + /// Fix the tilesheets for a map loaded from the mod folder. + /// The map whose tilesheets to fix. + /// The map asset key within the mod folder. + /// The map tilesheets could not be loaded. + private void FixLocalMapTilesheets(Map map, string mapKey) + { + if (!map.TileSheets.Any()) + return; + + string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder + foreach (TileSheet tilesheet in map.TileSheets) + { + // check for tilesheet relative to map + { + string localKey = Path.Combine(relativeMapFolder, tilesheet.ImageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + { + try + { + this.Load(localKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"The local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); + } + tilesheet.ImageSource = this.GetActualAssetKey(localKey); + continue; + } + } + + // fallback to game content + { + string contentKey = tilesheet.ImageSource; + if (contentKey.EndsWith(".png")) + contentKey = contentKey.Substring(0, contentKey.Length - 4); + try + { + this.ContentManager.Load(contentKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"The '{tilesheet.ImageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); + } + tilesheet.ImageSource = contentKey; + } + } + } + + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")] + private void AssertValidAssetKeyFormat(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + } + + /// Get a file from the mod folder. + /// The asset path relative to the mod folder. + private FileInfo GetModFile(string path) + { + // try exact match + path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); + FileInfo file = new FileInfo(path); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(path + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Get the asset path which loads a mod folder through a content manager. + /// The file path relative to the mod's folder. + /// The absolute file path. + private string GetModAssetPath(string localPath, string absolutePath) + { +#if SMAPI_FOR_WINDOWS + // XNA doesn't allow absolute asset paths, so get a path relative to the content folder + return Path.Combine(this.ModFolderPathFromContent, localPath); +#else + // MonoGame is weird about relative paths on Mac, but allows absolute paths + return absolutePath; +#endif + } + + /// Get a directory path relative to a given root. + /// The root path from which the path should be relative. + /// The target file path. + private string GetRelativePath(string rootPath, string targetPath) + { + // convert to URIs + Uri from = new Uri(rootPath + "/"); + Uri to = new Uri(targetPath + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); + + // get relative path + return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) + .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by Layoric. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // validate + if (Context.IsInDrawLoop) + throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop."); + + // process texture + SpriteBatch spriteBatch = Game1.spriteBatch; + GraphicsDevice gpu = Game1.graphics.GraphicsDevice; + using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height)) + { + // create blank render target to premultiply + gpu.SetRenderTarget(renderTarget); + gpu.Clear(Color.Black); + + // multiply each color by the source alpha, and write just the color values into the final texture + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorDestinationBlend = Blend.Zero, + ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue, + AlphaDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.SourceAlpha, + ColorSourceBlend = Blend.SourceAlpha + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // copy the alpha values from the source texture into the final one without multiplying them + spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState + { + ColorWriteChannels = ColorWriteChannels.Alpha, + AlphaDestinationBlend = Blend.Zero, + ColorDestinationBlend = Blend.Zero, + AlphaSourceBlend = Blend.One, + ColorSourceBlend = Blend.One + }); + spriteBatch.Draw(texture, texture.Bounds, Color.White); + spriteBatch.End(); + + // release GPU + gpu.SetRenderTarget(null); + + // extract premultiplied data + Color[] data = new Color[texture.Width * texture.Height]; + renderTarget.GetData(data); + + // unset texture from GPU to regain control + gpu.Textures[0] = null; + + // update texture with premultiplied data + texture.SetData(data); + } + + return texture; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs new file mode 100644 index 00000000..965a940a --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using StardewModdingAPI.Framework.Serialisation; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides simplified APIs for writing mods. + internal class ModHelper : IModHelper, IDisposable + { + /********* + ** Properties + *********/ + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + + /********* + ** Accessors + *********/ + /// The full path to the mod's folder. + public string DirectoryPath { get; } + + /// An API for loading content assets. + public IContentHelper Content { get; } + + /// Simplifies access to private game code. + public IReflectionHelper Reflection { get; } + + /// Metadata about loaded mods. + public IModRegistry ModRegistry { get; } + + /// An API for managing console commands. + public ICommandHelper ConsoleCommands { get; } + + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public ITranslationHelper Translation { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's display name. + /// The full path to the mod's folder. + /// Encapsulate SMAPI's JSON parsing. + /// Metadata about loaded mods. + /// Manages console commands. + /// The content manager which loads content assets. + /// Simplifies access to private game code. + /// An argument is null or empty. + /// The path does not exist on disk. + public ModHelper(string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) + { + // validate + if (string.IsNullOrWhiteSpace(modDirectory)) + throw new ArgumentNullException(nameof(modDirectory)); + if (jsonHelper == null) + throw new ArgumentNullException(nameof(jsonHelper)); + if (modRegistry == null) + throw new ArgumentNullException(nameof(modRegistry)); + if (!Directory.Exists(modDirectory)) + throw new InvalidOperationException("The specified mod directory does not exist."); + + // initialise + this.DirectoryPath = modDirectory; + this.JsonHelper = jsonHelper; + this.Content = new ContentHelper(contentManager, modDirectory, displayName); + this.ModRegistry = modRegistry; + this.ConsoleCommands = new CommandHelper(displayName, commandManager); + this.Reflection = reflection; + this.Translation = new TranslationHelper(displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + } + + /**** + ** Mod config file + ****/ + /// Read the mod's configuration file (and create it if needed). + /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. + public TConfig ReadConfig() + where TConfig : class, new() + { + TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); + this.WriteConfig(config); // create file or fill in missing fields + return config; + } + + /// Save to the mod's configuration file. + /// The config class type. + /// The config settings to save. + public void WriteConfig(TConfig config) + where TConfig : class, new() + { + this.WriteJsonFile("config.json", config); + } + + /**** + ** Generic JSON files + ****/ + /// Read a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + public TModel ReadJsonFile(string path) + where TModel : class + { + path = Path.Combine(this.DirectoryPath, path); + return this.JsonHelper.ReadJsonFile(path); + } + + /// Save to a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// The model to save. + public void WriteJsonFile(string path, TModel model) + where TModel : class + { + path = Path.Combine(this.DirectoryPath, path); + this.JsonHelper.WriteJsonFile(path, model); + } + + + /**** + ** Disposal + ****/ + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // nothing to dispose yet + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs new file mode 100644 index 00000000..5a21d999 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -0,0 +1,158 @@ +using System; +using StardewModdingAPI.Framework.Reflection; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class ReflectionHelper : IReflectionHelper + { + /********* + ** Properties + *********/ + /// The underlying reflection helper. + private readonly Reflector Reflector; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying reflection helper. + public ReflectionHelper(Reflector reflector) + { + this.Reflector = reflector; + } + + /**** + ** Fields + ****/ + /// Get a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IPrivateField GetPrivateField(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateField(obj, name, required); + } + + /// Get a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateField GetPrivateField(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateField(type, name, required); + } + + /**** + ** Properties + ****/ + /// Get a private instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateProperty(obj, name, required); + } + + /// Get a private static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the private property is not found. + public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateProperty(type, name, required); + } + + /**** + ** Field values + ** (shorthand since this is the most common case) + ****/ + /// Get the value of a private instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(object obj, string name, bool required = true) + { + IPrivateField field = this.GetPrivateField(obj, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /// Get the value of a private static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the private field is not found. + /// Returns the field value, or the default value for if the field wasn't found and is false. + /// + /// This is a shortcut for followed by . + /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. + /// + public TValue GetPrivateValue(Type type, string name, bool required = true) + { + IPrivateField field = this.GetPrivateField(type, name, required); + return field != null + ? field.GetValue() + : default(TValue); + } + + /**** + ** Methods + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) + { + return this.Reflector.GetPrivateMethod(obj, name, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) + { + return this.Reflector.GetPrivateMethod(type, name, required); + } + + /**** + ** Methods by signature + ****/ + /// Get a private instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) + { + return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); + } + + /// Get a private static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the private field is not found. + public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) + { + return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs new file mode 100644 index 00000000..86737f85 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + internal class TranslationHelper : ITranslationHelper + { + /********* + ** Properties + *********/ + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The translations for each locale. + private readonly IDictionary> All = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + /// The translations for the current locale, with locale fallback taken into account. + private IDictionary ForLocale; + + + /********* + ** Accessors + *********/ + /// The current locale. + public string Locale { get; private set; } + + /// The game's current language code. + public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The name of the relevant mod for error messages. + /// The initial locale. + /// The game's current language code. + public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode) + { + // save data + this.ModName = modName; + + // set locale + this.SetLocale(locale, languageCode); + } + + /// Get all translations for the current locale. + public IEnumerable GetTranslations() + { + return this.ForLocale.Values.ToArray(); + } + + /// Get a translation for the current locale. + /// The translation key. + public Translation Get(string key) + { + this.ForLocale.TryGetValue(key, out Translation translation); + return translation ?? new Translation(this.ModName, this.Locale, key, null); + } + + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + public Translation Get(string key, object tokens) + { + return this.Get(key).Tokens(tokens); + } + + /// Set the translations to use. + /// The translations to use. + internal TranslationHelper SetTranslations(IDictionary> translations) + { + // reset translations + this.All.Clear(); + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // rebuild cache + this.SetLocale(this.Locale, this.LocaleEnum); + + return this; + } + + /// Set the current locale and precache translations. + /// The current locale. + /// The game's current language code. + internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) + { + this.Locale = locale.ToLower().Trim(); + this.LocaleEnum = localeEnum; + + this.ForLocale = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (string next in this.GetRelevantLocales(this.Locale)) + { + // skip if locale not defined + if (!this.All.TryGetValue(next, out IDictionary translations)) + continue; + + // add missing translations + foreach (var pair in translations) + { + if (!this.ForLocale.ContainsKey(pair.Key)) + this.ForLocale.Add(pair.Key, new Translation(this.ModName, this.Locale, pair.Key, pair.Value)); + } + } + } + + + /********* + ** Private methods + *********/ + /// Get the locales which can provide translations for the given locale, in precedence order. + /// The locale for which to find valid locales. + private IEnumerable GetRelevantLocales(string locale) + { + // given locale + yield return locale; + + // broader locales (like pt-BR => pt) + while (true) + { + int dashIndex = locale.LastIndexOf('-'); + if (dashIndex <= 0) + break; + + locale = locale.Substring(0, dashIndex); + yield return locale; + } + + // default + if (locale != "default") + yield return "default"; + } + } +} diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs deleted file mode 100644 index fe387789..00000000 --- a/src/StardewModdingAPI/Framework/TranslationHelper.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Framework -{ - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - internal class TranslationHelper : ITranslationHelper - { - /********* - ** Properties - *********/ - /// The name of the relevant mod for error messages. - private readonly string ModName; - - /// The translations for each locale. - private readonly IDictionary> All = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - - /// The translations for the current locale, with locale fallback taken into account. - private IDictionary ForLocale; - - - /********* - ** Accessors - *********/ - /// The current locale. - public string Locale { get; private set; } - - /// The game's current language code. - public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The name of the relevant mod for error messages. - /// The initial locale. - /// The game's current language code. - public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode) - { - // save data - this.ModName = modName; - - // set locale - this.SetLocale(locale, languageCode); - } - - /// Get all translations for the current locale. - public IEnumerable GetTranslations() - { - return this.ForLocale.Values.ToArray(); - } - - /// Get a translation for the current locale. - /// The translation key. - public Translation Get(string key) - { - this.ForLocale.TryGetValue(key, out Translation translation); - return translation ?? new Translation(this.ModName, this.Locale, key, null); - } - - /// Get a translation for the current locale. - /// The translation key. - /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. - public Translation Get(string key, object tokens) - { - return this.Get(key).Tokens(tokens); - } - - /// Set the translations to use. - /// The translations to use. - internal TranslationHelper SetTranslations(IDictionary> translations) - { - // reset translations - this.All.Clear(); - foreach (var pair in translations) - this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); - - // rebuild cache - this.SetLocale(this.Locale, this.LocaleEnum); - - return this; - } - - /// Set the current locale and precache translations. - /// The current locale. - /// The game's current language code. - internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) - { - this.Locale = locale.ToLower().Trim(); - this.LocaleEnum = localeEnum; - - this.ForLocale = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (string next in this.GetRelevantLocales(this.Locale)) - { - // skip if locale not defined - if (!this.All.TryGetValue(next, out IDictionary translations)) - continue; - - // add missing translations - foreach (var pair in translations) - { - if (!this.ForLocale.ContainsKey(pair.Key)) - this.ForLocale.Add(pair.Key, new Translation(this.ModName, this.Locale, pair.Key, pair.Value)); - } - } - } - - - /********* - ** Private methods - *********/ - /// Get the locales which can provide translations for the given locale, in precedence order. - /// The locale for which to find valid locales. - private IEnumerable GetRelevantLocales(string locale) - { - // given locale - yield return locale; - - // broader locales (like pt-BR => pt) - while (true) - { - int dashIndex = locale.LastIndexOf('-'); - if (dashIndex <= 0) - break; - - locale = locale.Substring(0, dashIndex); - yield return locale; - } - - // default - if (locale != "default") - yield return "default"; - } - } -} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3b3f99b3..97bc0256 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -17,6 +17,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; diff --git a/src/StardewModdingAPI/ReflectionHelper.cs b/src/StardewModdingAPI/ReflectionHelper.cs deleted file mode 100644 index 56754cb4..00000000 --- a/src/StardewModdingAPI/ReflectionHelper.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using StardewModdingAPI.Framework.Reflection; - -namespace StardewModdingAPI -{ - /// Provides helper methods for accessing private game code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). - internal class ReflectionHelper : IReflectionHelper - { - /********* - ** Properties - *********/ - /// The underlying reflection helper. - private readonly Reflector Reflector; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying reflection helper. - public ReflectionHelper(Reflector reflector) - { - this.Reflector = reflector; - } - - /**** - ** Fields - ****/ - /// Get a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field wrapper, or null if the field doesn't exist and is false. - public IPrivateField GetPrivateField(object obj, string name, bool required = true) - { - return this.Reflector.GetPrivateField(obj, name, required); - } - - /// Get a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateField GetPrivateField(Type type, string name, bool required = true) - { - return this.Reflector.GetPrivateField(type, name, required); - } - - /**** - ** Properties - ****/ - /// Get a private instance property. - /// The property type. - /// The object which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) - { - return this.Reflector.GetPrivateProperty(obj, name, required); - } - - /// Get a private static property. - /// The property type. - /// The type which has the property. - /// The property name. - /// Whether to throw an exception if the private property is not found. - public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) - { - return this.Reflector.GetPrivateProperty(type, name, required); - } - - /**** - ** Field values - ** (shorthand since this is the most common case) - ****/ - /// Get the value of a private instance field. - /// The field type. - /// The object which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(object obj, string name, bool required = true) - { - IPrivateField field = this.GetPrivateField(obj, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /// Get the value of a private static field. - /// The field type. - /// The type which has the field. - /// The field name. - /// Whether to throw an exception if the private field is not found. - /// Returns the field value, or the default value for if the field wasn't found and is false. - /// - /// This is a shortcut for followed by . - /// When is false, this will return the default value if reflection fails. If you need to check whether the field exists, use instead. - /// - public TValue GetPrivateValue(Type type, string name, bool required = true) - { - IPrivateField field = this.GetPrivateField(type, name, required); - return field != null - ? field.GetValue() - : default(TValue); - } - - /**** - ** Methods - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) - { - return this.Reflector.GetPrivateMethod(obj, name, required); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) - { - return this.Reflector.GetPrivateMethod(type, name, required); - } - - /**** - ** Methods by signature - ****/ - /// Get a private instance method. - /// The object which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) - { - return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); - } - - /// Get a private static method. - /// The type which has the method. - /// The field name. - /// The argument types of the method signature to find. - /// Whether to throw an exception if the private field is not found. - public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) - { - return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); - } - } -} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index efef87b1..da058fb0 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -128,6 +128,11 @@ + + + + + @@ -135,7 +140,6 @@ - @@ -143,20 +147,17 @@ - - - @@ -200,7 +201,6 @@ - @@ -282,7 +282,7 @@ $(GamePath)\StardewModdingAPI.exe $(GamePath) - + -- cgit From 053c0577eccef3db3397a935863af79b30a0282f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 11:44:18 -0400 Subject: add mod ID to mod helpers (#318) --- .../Core/TranslationTests.cs | 6 +++--- .../Framework/ModHelpers/BaseHelper.cs | 23 ++++++++++++++++++++++ .../Framework/ModHelpers/CommandHelper.cs | 6 ++++-- .../Framework/ModHelpers/ContentHelper.cs | 6 ++++-- .../Framework/ModHelpers/ModHelper.cs | 12 ++++++----- .../Framework/ModHelpers/ReflectionHelper.cs | 6 ++++-- .../Framework/ModHelpers/TranslationHelper.cs | 6 ++++-- src/StardewModdingAPI/ICommandHelper.cs | 2 +- src/StardewModdingAPI/IContentHelper.cs | 2 +- src/StardewModdingAPI/IModLinked.cs | 12 +++++++++++ src/StardewModdingAPI/IReflectionHelper.cs | 2 +- src/StardewModdingAPI/ITranslationHelper.cs | 2 +- src/StardewModdingAPI/Program.cs | 4 ++-- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 ++ 14 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs create mode 100644 src/StardewModdingAPI/IModLinked.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs index fceef0a3..8511e765 100644 --- a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Tests.Core var data = new Dictionary>(); // act - ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); + ITranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); Translation translation = helper.Get("key"); Translation[] translationList = helper.GetTranslations()?.ToArray(); @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Tests.Core // act var actual = new Dictionary(); - TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); + TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); foreach (string locale in expected.Keys) { this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); @@ -79,7 +79,7 @@ namespace StardewModdingAPI.Tests.Core // act var actual = new Dictionary(); - TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); + TranslationHelper helper = new TranslationHelper("ModID", "ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); foreach (string locale in expected.Keys) { this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); diff --git a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs new file mode 100644 index 00000000..16032da1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs @@ -0,0 +1,23 @@ +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// The common base class for mod helpers. + internal abstract class BaseHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod for which the helper was created. + public string ModID { get; } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + protected BaseHelper(string modID) + { + this.ModID = modID; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs index 5fd56fdf..bdedb07c 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides an API for managing console commands. - internal class CommandHelper : ICommandHelper + internal class CommandHelper : BaseHelper, ICommandHelper { /********* ** Accessors @@ -19,9 +19,11 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// Construct an instance. + /// The unique ID of the relevant mod. /// The friendly mod name for this instance. /// Manages console commands. - public CommandHelper(string modName, CommandManager commandManager) + public CommandHelper(string modID, string modName, CommandManager commandManager) + : base(modID) { this.ModName = modName; this.CommandManager = commandManager; diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 4fc46dd0..5f72176e 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -16,7 +16,7 @@ using xTile.Tiles; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides an API for loading content assets. - internal class ContentHelper : IContentHelper + internal class ContentHelper : BaseHelper, IContentHelper { /********* ** Properties @@ -56,8 +56,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// SMAPI's underlying content manager. /// The absolute path to the mod folder. + /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. - public ContentHelper(SContentManager contentManager, string modFolderPath, string modName) + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) + : base(modID) { this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs index 965a940a..20d891a1 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides simplified APIs for writing mods. - internal class ModHelper : IModHelper, IDisposable + internal class ModHelper : BaseHelper, IModHelper, IDisposable { /********* ** Properties @@ -40,6 +40,7 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// Construct an instance. + /// The mod's unique ID. /// The mod's display name. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. @@ -49,7 +50,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Simplifies access to private game code. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) + public ModHelper(string modID, string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) + : base(modID) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -64,11 +66,11 @@ namespace StardewModdingAPI.Framework.ModHelpers // initialise this.DirectoryPath = modDirectory; this.JsonHelper = jsonHelper; - this.Content = new ContentHelper(contentManager, modDirectory, displayName); + this.Content = new ContentHelper(contentManager, modDirectory, modID, displayName); this.ModRegistry = modRegistry; - this.ConsoleCommands = new CommandHelper(displayName, commandManager); + this.ConsoleCommands = new CommandHelper(modID, displayName, commandManager); this.Reflection = reflection; - this.Translation = new TranslationHelper(displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + this.Translation = new TranslationHelper(modID, displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); } /**** diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs index 5a21d999..9411a97a 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { /// Provides helper methods for accessing private game code. /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). - internal class ReflectionHelper : IReflectionHelper + internal class ReflectionHelper : BaseHelper, IReflectionHelper { /********* ** Properties @@ -18,8 +18,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// Construct an instance. + /// The unique ID of the relevant mod. /// The underlying reflection helper. - public ReflectionHelper(Reflector reflector) + public ReflectionHelper(string modID, Reflector reflector) + : base(modID) { this.Reflector = reflector; } diff --git a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs index 86737f85..bbe3a81a 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs @@ -6,7 +6,7 @@ using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - internal class TranslationHelper : ITranslationHelper + internal class TranslationHelper : BaseHelper, ITranslationHelper { /********* ** Properties @@ -35,10 +35,12 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// Construct an instance. + /// The unique ID of the relevant mod. /// The name of the relevant mod for error messages. /// The initial locale. /// The game's current language code. - public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode) + public TranslationHelper(string modID, string modName, string locale, LocalizedContentManager.LanguageCode languageCode) + : base(modID) { // save data this.ModName = modName; diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/StardewModdingAPI/ICommandHelper.cs index 3a51ffb4..fb562e32 100644 --- a/src/StardewModdingAPI/ICommandHelper.cs +++ b/src/StardewModdingAPI/ICommandHelper.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI { /// Provides an API for managing console commands. - public interface ICommandHelper + public interface ICommandHelper : IModLinked { /********* ** Public methods diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index 1d520135..32a9ff19 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -5,7 +5,7 @@ using Microsoft.Xna.Framework.Graphics; namespace StardewModdingAPI { /// Provides an API for loading content assets. - public interface IContentHelper + public interface IContentHelper : IModLinked { /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. diff --git a/src/StardewModdingAPI/IModLinked.cs b/src/StardewModdingAPI/IModLinked.cs new file mode 100644 index 00000000..172ee30c --- /dev/null +++ b/src/StardewModdingAPI/IModLinked.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// An instance linked to a mod. + public interface IModLinked + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod for which the instance was created. + string ModID { get; } + } +} diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/StardewModdingAPI/IReflectionHelper.cs index 77943c6c..f66e3a31 100644 --- a/src/StardewModdingAPI/IReflectionHelper.cs +++ b/src/StardewModdingAPI/IReflectionHelper.cs @@ -3,7 +3,7 @@ namespace StardewModdingAPI { /// Simplifies access to private game code. - public interface IReflectionHelper + public interface IReflectionHelper : IModLinked { /********* ** Public methods diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs index dac83025..c4b72444 100644 --- a/src/StardewModdingAPI/ITranslationHelper.cs +++ b/src/StardewModdingAPI/ITranslationHelper.cs @@ -4,7 +4,7 @@ using StardewValley; namespace StardewModdingAPI { /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). - public interface ITranslationHelper + public interface ITranslationHelper : IModLinked { /********* ** Accessors diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 97bc0256..66ed0a85 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -703,8 +703,8 @@ namespace StardewModdingAPI // inject data mod.ModManifest = manifest; - var reflectionHelper = new ReflectionHelper(this.Reflection); - mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, reflectionHelper); + var reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); + mod.Helper = new ModHelper(manifest.UniqueID, metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, reflectionHelper); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); #if !SMAPI_2_0 mod.PathOnDisk = metadata.DirectoryPath; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index da058fb0..93d55b0a 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -128,6 +128,7 @@ + @@ -186,6 +187,7 @@ + -- cgit From 5583e707b217eb36e71ccae2fe894efbd599a8db Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 12:17:22 -0400 Subject: split helper out of mod registry, add mod ID, refactor ModHelper constructor (#318) --- .../Framework/ModHelpers/ModHelper.cs | 36 ++++++++-------- .../Framework/ModHelpers/ModRegistryHelper.cs | 48 ++++++++++++++++++++++ src/StardewModdingAPI/Framework/ModRegistry.cs | 6 +-- src/StardewModdingAPI/IModRegistry.cs | 4 +- src/StardewModdingAPI/IReflectionHelper.cs | 2 +- src/StardewModdingAPI/Program.cs | 21 ++++++---- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs index 20d891a1..665b9cf4 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs @@ -23,16 +23,16 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for loading content assets. public IContentHelper Content { get; } - /// Simplifies access to private game code. + /// An API for accessing private game code. public IReflectionHelper Reflection { get; } - /// Metadata about loaded mods. + /// an API for fetching metadata about loaded mods. public IModRegistry ModRegistry { get; } /// An API for managing console commands. public ICommandHelper ConsoleCommands { get; } - /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). public ITranslationHelper Translation { get; } @@ -41,36 +41,32 @@ namespace StardewModdingAPI.Framework.ModHelpers *********/ /// Construct an instance. /// The mod's unique ID. - /// The mod's display name. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. - /// Metadata about loaded mods. - /// Manages console commands. - /// The content manager which loads content assets. - /// Simplifies access to private game code. + /// An API for loading content assets. + /// An API for managing console commands. + /// an API for fetching metadata about loaded mods. + /// An API for accessing private game code. + /// An API for reading translations stored in the mod's i18n folder. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, IContentHelper contentHelper, ICommandHelper commandHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper) : base(modID) { - // validate + // validate directory if (string.IsNullOrWhiteSpace(modDirectory)) throw new ArgumentNullException(nameof(modDirectory)); - if (jsonHelper == null) - throw new ArgumentNullException(nameof(jsonHelper)); - if (modRegistry == null) - throw new ArgumentNullException(nameof(modRegistry)); if (!Directory.Exists(modDirectory)) throw new InvalidOperationException("The specified mod directory does not exist."); // initialise this.DirectoryPath = modDirectory; - this.JsonHelper = jsonHelper; - this.Content = new ContentHelper(contentManager, modDirectory, modID, displayName); - this.ModRegistry = modRegistry; - this.ConsoleCommands = new CommandHelper(modID, displayName, commandManager); - this.Reflection = reflection; - this.Translation = new TranslationHelper(modID, displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); + this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); + this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); + this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); + this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); } /**** diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs new file mode 100644 index 00000000..9e824694 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides metadata about installed mods. + internal class ModRegistryHelper : BaseHelper, IModRegistry + { + /********* + ** Properties + *********/ + /// The underlying mod registry. + private readonly ModRegistry Registry; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The underlying mod registry. + public ModRegistryHelper(string modID, ModRegistry registry) + : base(modID) + { + this.Registry = registry; + } + + /// Get metadata for all loaded mods. + public IEnumerable GetAll() + { + return this.Registry.GetAll(); + } + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + public IManifest Get(string uniqueID) + { + return this.Registry.Get(uniqueID); + } + + /// Get whether a mod has been loaded. + /// The mod's unique ID. + public bool IsLoaded(string uniqueID) + { + return this.Registry.IsLoaded(uniqueID); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index f9d3cfbf..a427bdb7 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -7,7 +7,7 @@ using System.Reflection; namespace StardewModdingAPI.Framework { /// Tracks the installed mods. - internal class ModRegistry : IModRegistry + internal class ModRegistry { /********* ** Properties @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /**** - ** IModRegistry + ** Basic metadata ****/ /// Get metadata for all loaded mods. public IEnumerable GetAll() @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework } /**** - ** Internal methods + ** Mod data ****/ /// Register a mod as a possible source of deprecation warnings. /// The mod metadata. diff --git a/src/StardewModdingAPI/IModRegistry.cs b/src/StardewModdingAPI/IModRegistry.cs index 676c9734..5ef3fd65 100644 --- a/src/StardewModdingAPI/IModRegistry.cs +++ b/src/StardewModdingAPI/IModRegistry.cs @@ -2,8 +2,8 @@ namespace StardewModdingAPI { - /// Provides metadata about loaded mods. - public interface IModRegistry + /// Provides an API for fetching metadata about loaded mods. + public interface IModRegistry : IModLinked { /// Get metadata for all loaded mods. IEnumerable GetAll(); diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/StardewModdingAPI/IReflectionHelper.cs index f66e3a31..fb2c7861 100644 --- a/src/StardewModdingAPI/IReflectionHelper.cs +++ b/src/StardewModdingAPI/IReflectionHelper.cs @@ -2,7 +2,7 @@ namespace StardewModdingAPI { - /// Simplifies access to private game code. + /// Provides an API for accessing private game code. public interface IReflectionHelper : IModLinked { /********* diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 66ed0a85..97e18322 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -422,7 +422,7 @@ namespace StardewModdingAPI mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); } } - } + } #endif // process dependencies @@ -604,7 +604,7 @@ namespace StardewModdingAPI /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. #if !SMAPI_2_0 -/// A list to populate with any deprecation warnings. + /// A list to populate with any deprecation warnings. private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) #else private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) @@ -702,13 +702,20 @@ namespace StardewModdingAPI #endif // inject data - mod.ModManifest = manifest; - var reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); - mod.Helper = new ModHelper(manifest.UniqueID, metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, reflectionHelper); - mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); + { + ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + + mod.ModManifest = manifest; + mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); + mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); #if !SMAPI_2_0 - mod.PathOnDisk = metadata.DirectoryPath; + mod.PathOnDisk = metadata.DirectoryPath; #endif + } // track mod metadata.SetMod(mod); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 93d55b0a..03f810d1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -132,6 +132,7 @@ + -- cgit From 8743c4115aa142113d791f2d2cd9ba811dcada2c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Jul 2017 12:53:12 -0400 Subject: tweak deprecation meta-warning --- src/StardewModdingAPI/Framework/DeprecationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs index 8750824c..153d8829 100644 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -54,7 +54,7 @@ namespace StardewModdingAPI.Framework // show SMAPI 2.0 meta-warning if(this.MarkWarned("SMAPI", "SMAPI 2.0 meta-warning", "2.0")) - this.Monitor.Log("Some of your mods use deprecated code that will stop working in a future SMAPI release. Try updating mods with 'deprecated code' warnings or let the mod authors know about this message.", LogLevel.Warn); + this.Monitor.Log("Some mods may stop working in SMAPI 2.0 (but they'll work fine for now). Try updating mods with 'deprecated code' warnings; if that doesn't remove the warnings, let the mod authors know about this message or see http://community.playstarbound.com/threads/135000 for details.", LogLevel.Warn); // build message string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; -- cgit