diff options
Diffstat (limited to 'src/SMAPI/Events')
-rw-r--r-- | src/SMAPI/Events/AssetEditPriority.cs | 16 | ||||
-rw-r--r-- | src/SMAPI/Events/AssetLoadPriority.cs | 19 | ||||
-rw-r--r-- | src/SMAPI/Events/AssetReadyEventArgs.cs | 31 | ||||
-rw-r--r-- | src/SMAPI/Events/AssetRequestedEventArgs.cs | 131 | ||||
-rw-r--r-- | src/SMAPI/Events/AssetsInvalidatedEventArgs.cs | 33 | ||||
-rw-r--r-- | src/SMAPI/Events/ButtonsChangedEventArgs.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Events/ChangeType.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/Events/IContentEvents.cs | 27 | ||||
-rw-r--r-- | src/SMAPI/Events/IModEvents.cs | 3 | ||||
-rw-r--r-- | src/SMAPI/Events/LocaleChangedEventArgs.cs | 45 | ||||
-rw-r--r-- | src/SMAPI/Events/MenuChangedEventArgs.cs | 14 | ||||
-rw-r--r-- | src/SMAPI/Events/ModMessageReceivedEventArgs.cs | 4 |
12 files changed, 316 insertions, 24 deletions
diff --git a/src/SMAPI/Events/AssetEditPriority.cs b/src/SMAPI/Events/AssetEditPriority.cs new file mode 100644 index 00000000..d41dfd7d --- /dev/null +++ b/src/SMAPI/Events/AssetEditPriority.cs @@ -0,0 +1,16 @@ +namespace StardewModdingAPI.Events +{ + /// <summary>The priority for an asset edit when multiple apply for the same asset.</summary> + /// <remarks>You can also specify arbitrary intermediate values, like <c>AssetLoadPriority.Low + 5</c>.</remarks> + public enum AssetEditPriority + { + /// <summary>This edit should be applied before (i.e. 'under') <see cref="Default"/> edits.</summary> + Early = -1000, + + /// <summary>The default priority.</summary> + Default = 0, + + /// <summary>This edit should be applied after (i.e. 'on top of') <see cref="Default"/> edits.</summary> + Late = 1000 + } +} diff --git a/src/SMAPI/Events/AssetLoadPriority.cs b/src/SMAPI/Events/AssetLoadPriority.cs new file mode 100644 index 00000000..e07b5a40 --- /dev/null +++ b/src/SMAPI/Events/AssetLoadPriority.cs @@ -0,0 +1,19 @@ +namespace StardewModdingAPI.Events +{ + /// <summary>The priority for an asset load when multiple apply for the same asset.</summary> + /// <remarks>If multiple non-<see cref="Exclusive"/> loads have the same priority, the one registered first will be selected. You can also specify arbitrary intermediate values, like <c>AssetLoadPriority.Low + 5</c>.</remarks> + public enum AssetLoadPriority + { + /// <summary>This load is optional and can safely be skipped if there are higher-priority loads.</summary> + Low = -1000, + + /// <summary>The load is optional and can safely be skipped if there are higher-priority loads, but it should still be preferred over any <see cref="Low"/>-priority loads.</summary> + Medium = 0, + + /// <summary>The load is optional and can safely be skipped if there are higher-priority loads, but it should still be preferred over any <see cref="Low"/>- or <see cref="Medium"/>-priority loads.</summary> + High = 1000, + + /// <summary>The load is not optional. If more than one loader has <see cref="Exclusive"/> priority, SMAPI will log an error and ignore all of them.</summary> + Exclusive = int.MaxValue + } +} diff --git a/src/SMAPI/Events/AssetReadyEventArgs.cs b/src/SMAPI/Events/AssetReadyEventArgs.cs new file mode 100644 index 00000000..2c308f18 --- /dev/null +++ b/src/SMAPI/Events/AssetReadyEventArgs.cs @@ -0,0 +1,31 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments for an <see cref="IContentEvents.AssetReady"/> event.</summary> + public class AssetReadyEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// <summary>The name of the asset being requested.</summary> + public IAssetName Name { get; } + + /// <summary>The <see cref="Name"/> with any locale codes stripped.</summary> + /// <remarks>For example, if <see cref="Name"/> contains a locale like <c>Data/Bundles.fr-FR</c>, this will be the name without locale like <c>Data/Bundles</c>. If the name has no locale, this field is equivalent.</remarks> + public IAssetName NameWithoutLocale { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="name">The name of the asset being requested.</param> + /// <param name="nameWithoutLocale">The <paramref name="name"/> with any locale codes stripped.</param> + internal AssetReadyEventArgs(IAssetName name, IAssetName nameWithoutLocale) + { + this.Name = name; + this.NameWithoutLocale = nameWithoutLocale; + } + } +} diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs new file mode 100644 index 00000000..3bcf83b9 --- /dev/null +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Content; +using xTile; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments for an <see cref="IContentEvents.AssetRequested"/> event.</summary> + public class AssetRequestedEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// <summary>The mod handling the event.</summary> + private readonly IModMetadata Mod; + + /// <summary>Get the mod metadata for a content pack, if it's a valid content pack for the mod.</summary> + private readonly Func<IModMetadata, string?, string, IModMetadata?> GetOnBehalfOf; + + + /********* + ** Accessors + *********/ + /// <summary>The name of the asset being requested.</summary> + public IAssetName Name { get; } + + /// <summary>The <see cref="Name"/> with any locale codes stripped.</summary> + /// <remarks>For example, if <see cref="Name"/> contains a locale like <c>Data/Bundles.fr-FR</c>, this will be the name without locale like <c>Data/Bundles</c>. If the name has no locale, this field is equivalent.</remarks> + public IAssetName NameWithoutLocale { get; } + + /// <summary>The requested data type.</summary> + public Type DataType { get; } + + /// <summary>The load operations requested by the event handler.</summary> + internal IList<AssetLoadOperation> LoadOperations { get; } = new List<AssetLoadOperation>(); + + /// <summary>The edit operations requested by the event handler.</summary> + internal IList<AssetEditOperation> EditOperations { get; } = new List<AssetEditOperation>(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="mod">The mod handling the event.</param> + /// <param name="name">The name of the asset being requested.</param> + /// <param name="dataType">The requested data type.</param> + /// <param name="nameWithoutLocale">The <paramref name="name"/> with any locale codes stripped.</param> + /// <param name="getOnBehalfOf">Get the mod metadata for a content pack, if it's a valid content pack for the mod.</param> + internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, IAssetName nameWithoutLocale, Type dataType, Func<IModMetadata, string?, string, IModMetadata?> getOnBehalfOf) + { + this.Mod = mod; + this.Name = name; + this.NameWithoutLocale = nameWithoutLocale; + this.DataType = dataType; + this.GetOnBehalfOf = getOnBehalfOf; + } + + /// <summary>Provide the initial instance for the asset, instead of trying to load it from the game's <c>Content</c> folder.</summary> + /// <param name="load">Get the initial instance of an asset.</param> + /// <param name="priority">If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</param> + /// <param name="onBehalfOf">The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod.</param> + /// <remarks> + /// Usage notes: + /// <list type="bullet"> + /// <item>The asset doesn't need to exist in the game's <c>Content</c> folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder.</item> + /// <item>Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the <paramref name="priority"/> parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use <see cref="Edit"/> instead to avoid those limitations and improve mod compatibility.</item> + /// </list> + /// </remarks> + public void LoadFrom(Func<object> load, AssetLoadPriority priority, string? onBehalfOf = null) + { + this.LoadOperations.Add( + new AssetLoadOperation( + mod: this.Mod, + priority: priority, + onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), + getData: _ => load() + ) + ); + } + + /// <summary>Provide the initial instance for the asset from a file in your mod folder, instead of trying to load it from the game's <c>Content</c> folder.</summary> + /// <typeparam name="TAsset">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, dictionaries, and lists; other types may be supported by the game's content pipeline.</typeparam> + /// <param name="relativePath">The relative path to the file in your mod folder.</param> + /// <param name="priority">If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</param> + /// <remarks> + /// Usage notes: + /// <list type="bullet"> + /// <item>The asset doesn't need to exist in the game's <c>Content</c> folder. If any mod loads the asset, the game will see it as an existing asset as if it was in that folder.</item> + /// <item>Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will raise an error and ignore all of them. If you're making changes to the existing asset instead of replacing it, you should use <see cref="Edit"/> instead to avoid those limitations and improve mod compatibility.</item> + /// </list> + /// </remarks> + public void LoadFromModFile<TAsset>(string relativePath, AssetLoadPriority priority) + where TAsset : notnull + { + this.LoadOperations.Add( + new AssetLoadOperation( + mod: this.Mod, + priority: priority, + onBehalfOf: null, + _ => this.Mod.Mod!.Helper.ModContent.Load<TAsset>(relativePath) + ) + ); + } + + /// <summary>Edit the asset after it's loaded.</summary> + /// <param name="apply">Apply changes to the asset.</param> + /// <param name="priority">If there are multiple edits that apply to the same asset, the priority with which this one should be applied.</param> + /// <param name="onBehalfOf">The content pack ID on whose behalf you're applying the change. This is only valid for content packs for your mod.</param> + /// <remarks> + /// Usage notes: + /// <list type="bullet"> + /// <item>Editing an asset which doesn't exist has no effect. This is applied after the asset is loaded from the game's <c>Content</c> folder, or from any mod's <see cref="LoadFrom"/> or <see cref="LoadFromModFile{TAsset}"/>.</item> + /// <item>You can apply any number of edits to the asset. Each edit will be applied on top of the previous one (i.e. it'll see the merged asset from all previous edits as its input).</item> + /// </list> + /// </remarks> + public void Edit(Action<IAssetData> apply, AssetEditPriority priority = AssetEditPriority.Default, string? onBehalfOf = null) + { + this.EditOperations.Add( + new AssetEditOperation( + mod: this.Mod, + priority: priority, + onBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"), + apply + ) + ); + } + } +} diff --git a/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs b/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs new file mode 100644 index 00000000..614cdf49 --- /dev/null +++ b/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments for an <see cref="IContentEvents.AssetsInvalidated"/> event.</summary> + public class AssetsInvalidatedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// <summary>The asset names that were invalidated.</summary> + public IReadOnlySet<IAssetName> Names { get; } + + /// <summary>The <see cref="Names"/> with any locale codes stripped.</summary> + /// <remarks>For example, if <see cref="Names"/> contains a locale like <c>Data/Bundles.fr-FR</c>, this will have the name without locale like <c>Data/Bundles</c>. If the name has no locale, this field is equivalent.</remarks> + public IReadOnlySet<IAssetName> NamesWithoutLocale { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="names">The asset names that were invalidated.</param> + /// <param name="namesWithoutLocale">The <paramref name="names"/> with any locale codes stripped.</param> + internal AssetsInvalidatedEventArgs(IEnumerable<IAssetName> names, IEnumerable<IAssetName> namesWithoutLocale) + { + this.Names = names.ToImmutableHashSet(); + this.NamesWithoutLocale = namesWithoutLocale.ToImmutableHashSet(); + } + } +} diff --git a/src/SMAPI/Events/ButtonsChangedEventArgs.cs b/src/SMAPI/Events/ButtonsChangedEventArgs.cs index dda41692..a5e87735 100644 --- a/src/SMAPI/Events/ButtonsChangedEventArgs.cs +++ b/src/SMAPI/Events/ButtonsChangedEventArgs.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Events foreach (var state in new[] { SButtonState.Pressed, SButtonState.Held, SButtonState.Released }) { if (!lookup.ContainsKey(state)) - lookup[state] = new SButton[0]; + lookup[state] = Array.Empty<SButton>(); } return lookup; diff --git a/src/SMAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs deleted file mode 100644 index 0fc717df..00000000 --- a/src/SMAPI/Events/ChangeType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Events -{ - /// <summary>Indicates how an inventory item changed.</summary> - public enum ChangeType - { - /// <summary>The entire stack was removed.</summary> - Removed, - - /// <summary>The entire stack was added.</summary> - Added, - - /// <summary>The stack size changed.</summary> - StackChange - } -} diff --git a/src/SMAPI/Events/IContentEvents.cs b/src/SMAPI/Events/IContentEvents.cs new file mode 100644 index 00000000..d537db70 --- /dev/null +++ b/src/SMAPI/Events/IContentEvents.cs @@ -0,0 +1,27 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// <summary>Events related to assets loaded from the content pipeline (including data, maps, and textures).</summary> + public interface IContentEvents + { + /// <summary>Raised when an asset is being requested from the content pipeline.</summary> + /// <remarks> + /// The asset isn't necessarily being loaded yet (e.g. the game may be checking if it exists). Mods can register the changes they want to apply using methods on the event arguments. These will be applied when the asset is actually loaded. + /// + /// If the asset is requested multiple times in the same tick (e.g. once to check if it exists and once to load it), SMAPI might only raise the event once and reuse the cached result. + /// </remarks> + event EventHandler<AssetRequestedEventArgs> AssetRequested; + + /// <summary>Raised after one or more assets were invalidated from the content cache by a mod, so they'll be reloaded next time they're requested. If the assets will be reloaded or propagated automatically, this event is raised before that happens.</summary> + event EventHandler<AssetsInvalidatedEventArgs> AssetsInvalidated; + + /// <summary>Raised after an asset is loaded by the content pipeline, after all mod edits specified via <see cref="AssetRequested"/> have been applied.</summary> + /// <remarks>This event is only raised if something requested the asset from the content pipeline. Invalidating an asset from the content cache won't necessarily reload it automatically.</remarks> + event EventHandler<AssetReadyEventArgs> AssetReady; + + /// <summary>Raised after the game language changes.</summary> + /// <remarks>For non-English players, this may be raised during startup when the game switches to the previously selected language.</remarks> + event EventHandler<LocaleChangedEventArgs> LocaleChanged; + } +} diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index 1f892b31..2603961b 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -3,6 +3,9 @@ namespace StardewModdingAPI.Events /// <summary>Manages access to events raised by SMAPI.</summary> public interface IModEvents { + /// <summary>Events related to assets loaded from the content pipeline (including data, maps, and textures).</summary> + IContentEvents Content { get; } + /// <summary>Events related to UI and drawing to the screen.</summary> IDisplayEvents Display { get; } diff --git a/src/SMAPI/Events/LocaleChangedEventArgs.cs b/src/SMAPI/Events/LocaleChangedEventArgs.cs new file mode 100644 index 00000000..09d3f6e5 --- /dev/null +++ b/src/SMAPI/Events/LocaleChangedEventArgs.cs @@ -0,0 +1,45 @@ +using System; +using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode; + +namespace StardewModdingAPI.Events +{ + /// <summary>Event arguments for an <see cref="IContentEvents.LocaleChanged"/> event.</summary> + public class LocaleChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// <summary>The previous language enum value.</summary> + /// <remarks>For a custom language, this is always <see cref="LanguageCode.mod"/>.</remarks> + public LanguageCode OldLanguage { get; } + + /// <summary>The previous locale code.</summary> + /// <remarks>This is the locale code as it appears in asset names, like <c>fr-FR</c> in <c>Maps/springobjects.fr-FR</c>. The locale code for English is an empty string.</remarks> + public string OldLocale { get; } + + /// <summary>The new language enum value.</summary> + /// <remarks><inheritdoc cref="OldLanguage" select="remarks" /></remarks> + public LanguageCode NewLanguage { get; } + + /// <summary>The new locale code.</summary> + /// <remarks><inheritdoc cref="OldLocale" select="remarks" /></remarks> + public string NewLocale { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="oldLanguage">The previous language enum value.</param> + /// <param name="oldLocale">The previous locale code.</param> + /// <param name="newLanguage">The new language enum value.</param> + /// <param name="newLocale">The new locale code.</param> + internal LocaleChangedEventArgs(LanguageCode oldLanguage, string oldLocale, LanguageCode newLanguage, string newLocale) + { + this.OldLanguage = oldLanguage; + this.OldLocale = oldLocale; + this.NewLanguage = newLanguage; + this.NewLocale = newLocale; + } + } +} diff --git a/src/SMAPI/Events/MenuChangedEventArgs.cs b/src/SMAPI/Events/MenuChangedEventArgs.cs index 977ba38b..c37fd216 100644 --- a/src/SMAPI/Events/MenuChangedEventArgs.cs +++ b/src/SMAPI/Events/MenuChangedEventArgs.cs @@ -9,20 +9,20 @@ namespace StardewModdingAPI.Events /********* ** Accessors *********/ - /// <summary>The previous menu.</summary> - public IClickableMenu OldMenu { get; } + /// <summary>The previous menu, if any.</summary> + public IClickableMenu? OldMenu { get; } - /// <summary>The current menu.</summary> - public IClickableMenu NewMenu { get; } + /// <summary>The current menu, if any.</summary> + public IClickableMenu? NewMenu { get; } /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="oldMenu">The previous menu.</param> - /// <param name="newMenu">The current menu.</param> - internal MenuChangedEventArgs(IClickableMenu oldMenu, IClickableMenu newMenu) + /// <param name="oldMenu">The previous menu, if any.</param> + /// <param name="newMenu">The current menu, if any.</param> + internal MenuChangedEventArgs(IClickableMenu? oldMenu, IClickableMenu? newMenu) { this.OldMenu = oldMenu; this.NewMenu = newMenu; diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs index d75a7540..84a27d18 100644 --- a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs +++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs @@ -45,8 +45,10 @@ namespace StardewModdingAPI.Events /// <summary>Read the message data into the given model type.</summary> /// <typeparam name="TModel">The message model type.</typeparam> public TModel ReadAs<TModel>() + where TModel : notnull { - return this.Message.Data.ToObject<TModel>(this.JsonHelper.GetSerializer()); + return this.Message.Data.ToObject<TModel>(this.JsonHelper.GetSerializer()) + ?? throw new InvalidOperationException($"Can't read empty mod message data as a {typeof(TModel).FullName} value."); } } } |