diff options
21 files changed, 211 insertions, 83 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 0687b888..7379cba3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -45,6 +45,7 @@ the C# mod that loads them is updated. _This adds support for many previously unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more. Existing mod APIs should work fine as-is._ * Mod files loaded through SMAPI APIs (including `helper.Content.Load`) are now case-insensitive, even on Linux. * Other improvements: + * Added `IAssetDataForImage.ExtendMap` to resize maps in asset editors. * Added [command-line arguments](technical/smapi.md#command-line-arguments) to toggle developer mode (thanks to Tondorian!). * Added `IContentPack.ModContent` property. * Added `Constants.ContentPath`. diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index e1b56559..b8649a7d 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -145,7 +145,7 @@ namespace SMAPI.Tests.Core { // arrange Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields + this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields(this.GetModDataRecord()) { Status = ModStatus.AssumeBroken }); @@ -216,9 +216,9 @@ namespace SMAPI.Tests.Core File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll!), ""); // arrange - Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict); + Mock<IModMetadata> mock = new(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mock.Setup(p => p.DataRecord).Returns(() => null); + mock.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields()); mock.Setup(p => p.Manifest).Returns(manifest); mock.Setup(p => p.DirectoryPath).Returns(modFolder); @@ -265,7 +265,7 @@ namespace SMAPI.Tests.Core public void ProcessDependencies_Skips_Failed() { // arrange - Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict); + Mock<IModMetadata> mock = new(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act @@ -380,7 +380,7 @@ namespace SMAPI.Tests.Core Mock<IModMetadata> modA = this.GetMetadata("Mod A"); Mock<IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock<IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); - Mock<IModMetadata> modD = new Mock<IModMetadata>(MockBehavior.Strict); + Mock<IModMetadata> modD = new(MockBehavior.Strict); modD.Setup(p => p.Manifest).Returns<IManifest>(null); modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); @@ -516,8 +516,8 @@ namespace SMAPI.Tests.Core /// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param> private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false) { - Mock<IModMetadata> mod = new Mock<IModMetadata>(MockBehavior.Strict); - mod.Setup(p => p.DataRecord).Returns(() => null); + Mock<IModMetadata> mod = new(MockBehavior.Strict); + mod.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields()); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); mod.Setup(p => p.Manifest).Returns(manifest); @@ -538,11 +538,22 @@ namespace SMAPI.Tests.Core private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields? modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.DataRecord).Returns(() => null); mod.Setup(p => p.Manifest).Returns(this.GetManifest()); mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); - mod.Setup(p => p.DataRecord).Returns(modRecord); + mod.Setup(p => p.DataRecord).Returns(modRecord ?? this.GetModDataRecordVersionedFields()); mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>()); } + + /// <summary>Generate a default mod data record.</summary> + private ModDataRecord GetModDataRecord() + { + return new("Default Display Name", new ModDataModel("Sample ID", null, ModWarning.None)); + } + + /// <summary>Generate a default mod data versioned fields instance.</summary> + private ModDataRecordVersionedFields GetModDataRecordVersionedFields() + { + return new ModDataRecordVersionedFields(this.GetModDataRecord()); + } } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs index 3fa70615..da678ac9 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; namespace StardewModdingAPI.Toolkit.Framework.ModData @@ -11,6 +9,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Accessors ********/ /// <summary>Extra metadata about mods.</summary> - public IDictionary<string, ModDataModel> ModData { get; set; } + public IDictionary<string, ModDataModel> ModData { get; } = new Dictionary<string, ModDataModel>(); } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs index 46cb81e1..9674d283 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.ModData @@ -20,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData public bool IsDefault { get; } /// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary> - public ISemanticVersion LowerVersion { get; } + public ISemanticVersion? LowerVersion { get; } /// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary> - public ISemanticVersion UpperVersion { get; } + public ISemanticVersion? UpperVersion { get; } /********* @@ -35,7 +33,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <param name="isDefault">Whether this field should only be applied if it's not already set.</param> /// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param> /// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param> - public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion? lowerVersion, ISemanticVersion? upperVersion) { this.Key = key; this.Value = value; @@ -46,7 +44,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get whether this data field applies for the given manifest.</summary> /// <param name="manifest">The mod manifest.</param> - public bool IsMatch(IManifest manifest) + public bool IsMatch(IManifest? manifest) { return manifest?.Version != null // ignore invalid manifest diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index 4d96a555..5912fb87 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -16,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Accessors *********/ /// <summary>The mod's current unique ID.</summary> - public string ID { get; set; } + public string ID { get; } /// <summary>The former mod IDs (if any).</summary> /// <remarks> @@ -25,14 +23,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// ID, if any. If the mod's ID changed over time, multiple variants can be separated by the /// <c>|</c> character. /// </remarks> - public string FormerIDs { get; set; } + public string? FormerIDs { get; } /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> - public ModWarning SuppressWarnings { get; set; } + public ModWarning SuppressWarnings { get; } /// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary> [JsonExtensionData] - public IDictionary<string, JToken> ExtensionData { get; set; } + public IDictionary<string, JToken> ExtensionData { get; } = new Dictionary<string, JToken>(); /// <summary>The versioned field data.</summary> /// <remarks> @@ -52,6 +50,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /********* ** Public methods *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The mod's current unique ID.</param> + /// <param name="formerIds">The former mod IDs (if any).</param> + /// <param name="suppressWarnings">The mod warnings to suppress, even if they'd normally be shown.</param> + public ModDataModel(string id, string? formerIds, ModWarning suppressWarnings) + { + this.ID = id; + this.FormerIDs = formerIds; + this.SuppressWarnings = suppressWarnings; + } + /// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary> public IEnumerable<ModDataField> GetFields() { @@ -61,8 +70,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData string packedKey = pair.Key; string value = pair.Value; bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; + ISemanticVersion? lowerVersion = null; + ISemanticVersion? upperVersion = null; // parse string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); @@ -113,11 +122,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData [OnDeserialized] private void OnDeserialized(StreamingContext context) { - if (this.ExtensionData != null) - { - this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); - this.ExtensionData = null; - } + this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString()); + this.ExtensionData.Clear(); } } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 4c09e1ba..e9ece387 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -22,7 +20,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData public string[] FormerIDs { get; } /// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary> - public ModWarning SuppressWarnings { get; set; } + public ModWarning SuppressWarnings { get; } /// <summary>The versioned field data.</summary> public ModDataField[] Fields { get; } @@ -72,9 +70,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData } /// <summary>Get the default update key for this mod, if any.</summary> - public string GetDefaultUpdateKey() + public string? GetDefaultUpdateKey() { - string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + string? updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; return !string.IsNullOrWhiteSpace(updateKey) ? updateKey : null; @@ -84,7 +82,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <param name="manifest">The manifest to match.</param> public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) { - ModDataRecordVersionedFields parsed = new() { DisplayName = this.DisplayName, DataRecord = this }; + ModDataRecordVersionedFields parsed = new(this); foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) { switch (field.Key) diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index b599b343..65fa424e 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Toolkit.Framework.ModData { /// <summary>The versioned fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary> @@ -9,24 +7,32 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Accessors *********/ /// <summary>The underlying data record.</summary> - public ModDataRecord DataRecord { get; set; } - - /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary> - public string DisplayName { get; set; } + public ModDataRecord DataRecord { get; } - /// <summary>The update key to apply.</summary> - public string UpdateKey { get; set; } + /// <summary>The update key to apply (if any).</summary> + public string? UpdateKey { get; set; } /// <summary>The predefined compatibility status.</summary> public ModStatus Status { get; set; } = ModStatus.None; /// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary> - public string StatusReasonPhrase { get; set; } + public string? StatusReasonPhrase { get; set; } /// <summary>Technical details shown in TRACE logs for the <see cref="Status"/>, or <c>null</c> to omit it.</summary> - public string StatusReasonDetails { get; set; } + public string? StatusReasonDetails { get; set; } /// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary> - public ISemanticVersion StatusUpperVersion { get; set; } + public ISemanticVersion? StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="dataRecord">The underlying data record.</param> + public ModDataRecordVersionedFields(ModDataRecord dataRecord) + { + this.DataRecord = dataRecord; + } } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs index a5237334..168b8aac 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -16,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData private readonly ModDataRecord[] Records; /// <summary>Get an update URL for an update key (if valid).</summary> - private readonly Func<string, string> GetUpdateUrl; + private readonly Func<string, string?> GetUpdateUrl; /********* @@ -29,7 +27,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Construct an instance.</summary> /// <param name="records">The underlying mod data records indexed by default display name.</param> /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> - public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string> getUpdateUrl) + public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string?> getUpdateUrl) { this.Records = records.ToArray(); this.GetUpdateUrl = getUpdateUrl; @@ -43,7 +41,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get a mod data record.</summary> /// <param name="modID">The unique mod ID.</param> - public ModDataRecord Get(string modID) + public ModDataRecord? Get(string? modID) { return !string.IsNullOrWhiteSpace(modID) ? this.Records.FirstOrDefault(p => p.HasID(modID)) @@ -52,11 +50,11 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// <summary>Get the mod page URL for a mod (if available).</summary> /// <param name="id">The unique mod ID.</param> - public string GetModPageUrlFor(string id) + public string? GetModPageUrlFor(string? id) { // get update key - ModDataRecord record = this.Get(id); - ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + ModDataRecord? record = this.Get(id); + ModDataField? updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); if (updateKeyField == null) return null; diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs index eede4562..6f5dffbe 100644 --- a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs @@ -2,4 +2,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("StardewModdingAPI")] [assembly: InternalsVisibleTo("SMAPI.Installer")] +[assembly: InternalsVisibleTo("SMAPI.Tests")] [assembly: InternalsVisibleTo("SMAPI.Web")] diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 01010602..da3ad608 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -46,7 +46,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models /// <summary>Any manifest fields which didn't match a valid field.</summary> [JsonExtensionData] - public IDictionary<string, object> ExtraFields { get; set; } = new Dictionary<string, object>(); + public IDictionary<string, object> ExtraFields { get; } = new Dictionary<string, object>(); /********* diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index 0425e195..93148277 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using xTile; +using xTile.Dimensions; using xTile.Layers; using xTile.Tiles; +using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace StardewModdingAPI.Framework.Content { @@ -16,6 +19,13 @@ namespace StardewModdingAPI.Framework.Content internal class AssetDataForMap : AssetData<Map>, IAssetDataForMap { /********* + ** Fields + *********/ + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -24,8 +34,12 @@ namespace StardewModdingAPI.Framework.Content /// <param name="data">The content data being read.</param> /// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param> /// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param> - public AssetDataForMap(string locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced) - : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } + /// <param name="reflection">Simplifies access to private code.</param> + public AssetDataForMap(string locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced, Reflector reflection) + : base(locale, assetName, data, getNormalizedPath, onDataReplaced) + { + this.Reflection = reflection; + } /// <inheritdoc /> /// <remarks>Derived from <see cref="GameLocation.ApplyMapOverride(Map,string,Rectangle?,Rectangle?)"/> with a few changes: @@ -137,6 +151,42 @@ namespace StardewModdingAPI.Framework.Content } } + /// <inheritdoc /> + public bool ExtendMap(Map map, int minWidth, int minHeight) + { + bool resized = false; + + // resize layers + foreach (Layer layer in map.Layers) + { + // check if resize needed + if (layer.LayerWidth >= minWidth && layer.LayerHeight >= minHeight) + continue; + resized = true; + + // build new tile matrix + int width = Math.Max(minWidth, layer.LayerWidth); + int height = Math.Max(minHeight, layer.LayerHeight); + Tile[,] tiles = new Tile[width, height]; + for (int x = 0; x < layer.LayerWidth; x++) + { + for (int y = 0; y < layer.LayerHeight; y++) + tiles[x, y] = layer.Tiles[x, y]; + } + + // update fields + this.Reflection.GetField<Tile[,]>(layer, "m_tiles").SetValue(tiles); + this.Reflection.GetField<TileArray>(layer, "m_tileArray").SetValue(new TileArray(layer, tiles)); + this.Reflection.GetField<Size>(layer, "m_layerSize").SetValue(new Size(width, height)); + } + + // resize map + if (resized) + this.Reflection.GetMethod(map, "UpdateDisplaySize").Invoke(); + + return resized; + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs index 4a6df64b..bb3966b9 100644 --- a/src/SMAPI/Framework/Content/AssetDataForObject.cs +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; using xTile; namespace StardewModdingAPI.Framework.Content @@ -11,6 +12,13 @@ namespace StardewModdingAPI.Framework.Content internal class AssetDataForObject : AssetData<object>, IAssetData { /********* + ** Fields + *********/ + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -18,15 +26,20 @@ namespace StardewModdingAPI.Framework.Content /// <param name="assetName">The asset name being read.</param> /// <param name="data">The content data being read.</param> /// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param> - public AssetDataForObject(string locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath) - : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { } + /// <param name="reflection">Simplifies access to private code.</param> + public AssetDataForObject(string locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath, Reflector reflection) + : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) + { + this.Reflection = reflection; + } /// <summary>Construct an instance.</summary> /// <param name="info">The asset metadata.</param> /// <param name="data">The content data being read.</param> /// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param> - public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath) - : this(info.Locale, info.Name, data, getNormalizedPath) { } + /// <param name="reflection">Simplifies access to private code.</param> + public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath, Reflector reflection) + : this(info.Locale, info.Name, data, getNormalizedPath, reflection) { } /// <inheritdoc /> public IAssetDataForDictionary<TKey, TValue> AsDictionary<TKey, TValue>() @@ -43,7 +56,7 @@ namespace StardewModdingAPI.Framework.Content /// <inheritdoc /> public IAssetDataForMap AsMap() { - return new AssetDataForMap(this.Locale, this.Name, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith); + return new AssetDataForMap(this.Locale, this.Name, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith, this.Reflection); } /// <inheritdoc /> diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 81820b05..4e48b08c 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -727,7 +727,8 @@ namespace StardewModdingAPI.Framework locale: null, assetName: legacyName, data: asset.Data, - getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName + getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName, + reflection: this.Reflection ); } diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 4594d235..f1ccab48 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -32,6 +32,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Encapsulates monitoring and logging.</summary> protected readonly IMonitor Monitor; + /// <summary>Simplifies access to private code.</summary> + protected readonly Reflector Reflection; + /// <summary>Whether to enable more aggressive memory optimizations.</summary> protected readonly bool AggressiveMemoryOptimizations; @@ -90,6 +93,7 @@ namespace StardewModdingAPI.Framework.ContentManagers this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); this.Cache = new ContentCache(this, reflection); this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Reflection = reflection; this.OnDisposing = onDisposing; this.IsNamespaced = isNamespaced; this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations; diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index f4e1bda4..e494559d 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -124,7 +124,7 @@ namespace StardewModdingAPI.Framework.ContentManagers IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName); IAssetData asset = this.ApplyLoader<T>(info) - ?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName); + ?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection); asset = this.ApplyEditors<T>(info, asset); return (T)asset.Data; }); @@ -187,7 +187,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // return matched asset return this.TryFixAndValidateLoadedAsset(info, data, loader) - ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName) + ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName, this.Reflection) : null; } @@ -197,7 +197,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <param name="asset">The loaded asset.</param> private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset) { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName, this.Reflection); // special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead. { diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index e72e397e..12ef0439 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -10,6 +10,7 @@ using System.Linq; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers @@ -36,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + /********* ** Accessors @@ -94,7 +98,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The friendly mod name for use in errors.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor) + /// <param name="reflection">Simplifies access to private code.</param> + public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor, Reflector reflection) : base(modID) { string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID); @@ -104,6 +109,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager); this.ModName = modName; this.Monitor = monitor; + this.Reflection = reflection; } /// <inheritdoc /> @@ -185,7 +191,13 @@ namespace StardewModdingAPI.Framework.ModHelpers assetName ??= $"temp/{Guid.NewGuid():N}"; - return new AssetDataForObject(this.CurrentLocale, this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/), data, this.NormalizeAssetName); + return new AssetDataForObject( + locale: this.CurrentLocale, + assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/), + data: data, + getNormalizedPath: this.NormalizeAssetName, + reflection: this.Reflection + ); } diff --git a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs index 956bac7f..6d0c2f5f 100644 --- a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs @@ -5,6 +5,7 @@ using System.Linq; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers @@ -27,6 +28,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + /********* ** Accessors @@ -46,7 +50,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The friendly mod name for use in errors.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public GameContentHelper(ContentCoordinator contentCore, string modID, string modName, IMonitor monitor) + /// <param name="reflection">Simplifies access to private code.</param> + public GameContentHelper(ContentCoordinator contentCore, string modID, string modName, IMonitor monitor, Reflector reflection) : base(modID) { string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID); @@ -55,6 +60,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content"); this.ModName = modName; this.Monitor = monitor; + this.Reflection = reflection; } /// <inheritdoc /> @@ -119,7 +125,13 @@ namespace StardewModdingAPI.Framework.ModHelpers assetName ??= $"temp/{Guid.NewGuid():N}"; - return new AssetDataForObject(this.CurrentLocale, this.ContentCore.ParseAssetName(assetName, allowLocales: true), data, key => this.ParseAssetName(key).Name); + return new AssetDataForObject( + locale: this.CurrentLocale, + assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true), + data: data, + getNormalizedPath: key => this.ParseAssetName(key).Name, + reflection: this.Reflection + ); } /// <summary>Get the underlying game content manager.</summary> diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs index 90064354..b149ed82 100644 --- a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Utilities; namespace StardewModdingAPI.Framework.ModHelpers @@ -27,6 +28,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary> private readonly CaseInsensitivePathCache RelativePathCache; + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + /********* ** Public methods @@ -38,7 +42,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modName">The friendly mod name for use in errors.</param> /// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param> /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="relativePathCache"/>.</param> - public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IContentManager gameContentManager, CaseInsensitivePathCache relativePathCache) + /// <param name="reflection">Simplifies access to private code.</param> + public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IContentManager gameContentManager, CaseInsensitivePathCache relativePathCache, Reflector reflection) : base(modID) { string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID); @@ -47,6 +52,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, gameContentManager); this.ModName = modName; this.RelativePathCache = relativePathCache; + this.Reflection = reflection; } /// <inheritdoc /> @@ -83,7 +89,13 @@ namespace StardewModdingAPI.Framework.ModHelpers ? this.RelativePathCache.GetAssetName(relativePath) : $"temp/{Guid.NewGuid():N}"; - return new AssetDataForObject(this.ContentCore.GetLocale(), this.ContentCore.ParseAssetName(relativePath, allowLocales: false), data, key => this.ContentCore.ParseAssetName(key, allowLocales: false).Name); + return new AssetDataForObject( + locale: this.ContentCore.GetLocale(), + assetName: this.ContentCore.ParseAssetName(relativePath, allowLocales: false), + data: data, + getNormalizedPath: key => this.ContentCore.ParseAssetName(key, allowLocales: false).Name, + reflection: this.Reflection + ); } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1a58d84b..364a7632 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1754,8 +1754,8 @@ namespace StardewModdingAPI.Framework IManifest manifest = mod.Manifest; IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(mod.DirectoryPath); - GameContentHelper gameContentHelper = new(this.ContentCore, manifest.UniqueID, mod.DisplayName, monitor); - IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache); + GameContentHelper gameContentHelper = new(this.ContentCore, manifest.UniqueID, mod.DisplayName, monitor, this.Reflection); + IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection); TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, modContentHelper, translationHelper, jsonHelper, relativePathCache); mod.SetMod(contentPack, monitor, translationHelper); @@ -1838,8 +1838,8 @@ namespace StardewModdingAPI.Framework CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(packDirPath); - GameContentHelper gameContentHelper = new(contentCore, packManifest.UniqueID, packManifest.Name, packMonitor); - IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), relativePathCache); + GameContentHelper gameContentHelper = new(contentCore, packManifest.UniqueID, packManifest.Name, packMonitor, this.Reflection); + IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection); TranslationHelper packTranslationHelper = new(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper, relativePathCache); @@ -1852,10 +1852,10 @@ namespace StardewModdingAPI.Framework ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager); CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(mod.DirectoryPath); #pragma warning disable CS0612 // deprecated code - ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); + ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor, this.Reflection); #pragma warning restore CS0612 - GameContentHelper gameContentHelper = new(contentCore, manifest.UniqueID, mod.DisplayName, monitor); - IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache); + GameContentHelper gameContentHelper = new(contentCore, manifest.UniqueID, mod.DisplayName, monitor, this.Reflection); + IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection); IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack); IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection); diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs index 388caa68..1416592e 100644 --- a/src/SMAPI/IAssetDataForImage.cs +++ b/src/SMAPI/IAssetDataForImage.cs @@ -23,8 +23,8 @@ namespace StardewModdingAPI void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); /// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary> - /// <param name="minWidth">The minimum texture width.</param> - /// <param name="minHeight">The minimum texture height.</param> + /// <param name="minWidth">The minimum texture width in pixels.</param> + /// <param name="minHeight">The minimum texture height in pixels.</param> /// <returns>Whether the texture was resized.</returns> bool ExtendImage(int minWidth, int minHeight); } diff --git a/src/SMAPI/IAssetDataForMap.cs b/src/SMAPI/IAssetDataForMap.cs index 89ee28f2..0b637baf 100644 --- a/src/SMAPI/IAssetDataForMap.cs +++ b/src/SMAPI/IAssetDataForMap.cs @@ -17,5 +17,12 @@ namespace StardewModdingAPI /// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param> /// <param name="patchMode">Indicates how the map should be patched.</param> void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay); + + /// <summary>Extend the map if needed to fit the given size. Note that this is an expensive operation and resizes the map in-place.</summary> + /// <param name="map">The map to resize.</param> + /// <param name="minWidth">The minimum map width in tiles.</param> + /// <param name="minHeight">The minimum map height in tiles.</param> + /// <returns>Whether the map was resized.</returns> + bool ExtendMap(Map map, int minWidth, int minHeight); } } |