Merge branch 'develop' into stable
This commit is contained in:
commit
a9cadc7f32
|
@ -1,7 +1,7 @@
|
|||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!--set general build properties -->
|
||||
<Version>3.14.2</Version>
|
||||
<Version>3.14.3</Version>
|
||||
<Product>SMAPI</Product>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
← [README](README.md)
|
||||
|
||||
# Release notes
|
||||
## 3.14.3
|
||||
Released 12 May 2022 for Stardew Valley 1.5.6 or later.
|
||||
|
||||
* For players:
|
||||
* Reduced in-game performance impact.
|
||||
|
||||
* For mod authors:
|
||||
* Refactored how event handling works under the hood, particularly the new content API. This should have no effect on mod usage.
|
||||
* Verbose mode now logs the in-game time.
|
||||
* Fixed error when loading a `.xnb` file through the old content API without the file extension.
|
||||
* Fixed asset propagation for player sprites not fully updating recolor masks in some cases.
|
||||
|
||||
* For the web UI:
|
||||
* Updated the JSON validator/schema for Content Patcher 1.26.0.
|
||||
|
||||
## 3.14.2
|
||||
Released 08 May 2022 for Stardew Valley 1.5.6 or later.
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.14.2",
|
||||
"Version": "3.14.3",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.14.2"
|
||||
"MinimumApiVersion": "3.14.3"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Error Handler",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.14.2",
|
||||
"Version": "3.14.3",
|
||||
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
||||
"UniqueID": "SMAPI.ErrorHandler",
|
||||
"EntryDll": "ErrorHandler.dll",
|
||||
"MinimumApiVersion": "3.14.2"
|
||||
"MinimumApiVersion": "3.14.3"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.14.2",
|
||||
"Version": "3.14.3",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.14.2"
|
||||
"MinimumApiVersion": "3.14.3"
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
"title": "Format version",
|
||||
"description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.",
|
||||
"type": "string",
|
||||
"const": "1.25.0",
|
||||
"const": "1.26.0",
|
||||
"@errorMessages": {
|
||||
"const": "Incorrect value '@value'. You should always use the latest format version (currently 1.25.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
|
||||
"const": "Incorrect value '@value'. You should always use the latest format version (currently 1.26.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
|
||||
}
|
||||
},
|
||||
"ConfigSchema": {
|
||||
|
@ -51,6 +51,11 @@
|
|||
"description": "An optional explanation of the config field for players, shown in UIs like Generic Mod Config Menu.",
|
||||
"type": "string"
|
||||
},
|
||||
"Section": {
|
||||
"title": "Section",
|
||||
"description": "An optional section key to group related fields on config UIs. This can be the literal text to show, or you can add a translation with the key 'config.section.<section value>.name' and '.description' to add a translated name & tooltip.",
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
|
|
@ -50,7 +50,7 @@ namespace StardewModdingAPI
|
|||
internal static int? LogScreenId { get; set; }
|
||||
|
||||
/// <summary>SMAPI's current raw semantic version.</summary>
|
||||
internal static string RawApiVersion = "3.14.2";
|
||||
internal static string RawApiVersion = "3.14.3";
|
||||
}
|
||||
|
||||
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace StardewModdingAPI.Events
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>The mod handling the event.</summary>
|
||||
private readonly IModMetadata Mod;
|
||||
private 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;
|
||||
|
@ -37,26 +37,31 @@ namespace StardewModdingAPI.Events
|
|||
public Type DataType => this.AssetInfo.DataType;
|
||||
|
||||
/// <summary>The load operations requested by the event handler.</summary>
|
||||
internal IList<AssetLoadOperation> LoadOperations { get; } = new List<AssetLoadOperation>();
|
||||
internal List<AssetLoadOperation> LoadOperations { get; } = new();
|
||||
|
||||
/// <summary>The edit operations requested by the event handler.</summary>
|
||||
internal IList<AssetEditOperation> EditOperations { get; } = new List<AssetEditOperation>();
|
||||
internal List<AssetEditOperation> EditOperations { get; } = new();
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="mod">The mod handling the event.</param>
|
||||
/// <param name="assetInfo">The asset info being requested.</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, IAssetInfo assetInfo, Func<IModMetadata, string?, string, IModMetadata?> getOnBehalfOf)
|
||||
internal AssetRequestedEventArgs(IAssetInfo assetInfo, Func<IModMetadata, string?, string, IModMetadata?> getOnBehalfOf)
|
||||
{
|
||||
this.Mod = mod;
|
||||
this.AssetInfo = assetInfo;
|
||||
this.GetOnBehalfOf = getOnBehalfOf;
|
||||
}
|
||||
|
||||
/// <summary>Set the mod handling the event.</summary>
|
||||
/// <param name="mod">The mod handling the event.</param>
|
||||
internal void SetMod(IModMetadata mod)
|
||||
{
|
||||
this.Mod = mod;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
@ -70,10 +75,11 @@ namespace StardewModdingAPI.Events
|
|||
/// </remarks>
|
||||
public void LoadFrom(Func<object> load, AssetLoadPriority priority, string? onBehalfOf = null)
|
||||
{
|
||||
IModMetadata mod = this.GetMod();
|
||||
this.LoadOperations.Add(
|
||||
new AssetLoadOperation(
|
||||
Mod: this.Mod,
|
||||
OnBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"),
|
||||
Mod: mod,
|
||||
OnBehalfOf: this.GetOnBehalfOf(mod, onBehalfOf, "load assets"),
|
||||
Priority: priority,
|
||||
GetData: _ => load()
|
||||
)
|
||||
|
@ -94,12 +100,13 @@ namespace StardewModdingAPI.Events
|
|||
public void LoadFromModFile<TAsset>(string relativePath, AssetLoadPriority priority)
|
||||
where TAsset : notnull
|
||||
{
|
||||
IModMetadata mod = this.GetMod();
|
||||
this.LoadOperations.Add(
|
||||
new AssetLoadOperation(
|
||||
Mod: this.Mod,
|
||||
Mod: mod,
|
||||
OnBehalfOf: null,
|
||||
Priority: priority,
|
||||
GetData: _ => this.Mod.Mod!.Helper.ModContent.Load<TAsset>(relativePath)
|
||||
GetData: _ => mod.Mod!.Helper.ModContent.Load<TAsset>(relativePath)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -117,14 +124,26 @@ namespace StardewModdingAPI.Events
|
|||
/// </remarks>
|
||||
public void Edit(Action<IAssetData> apply, AssetEditPriority priority = AssetEditPriority.Default, string? onBehalfOf = null)
|
||||
{
|
||||
IModMetadata mod = this.GetMod();
|
||||
this.EditOperations.Add(
|
||||
new AssetEditOperation(
|
||||
Mod: this.Mod,
|
||||
Mod: mod,
|
||||
Priority: priority,
|
||||
OnBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"),
|
||||
OnBehalfOf: this.GetOnBehalfOf(mod, onBehalfOf, "edit assets"),
|
||||
ApplyEdit: apply
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the mod handling the event.</summary>
|
||||
/// <exception cref="InvalidOperationException">This instance hasn't been initialized with the mod metadata yet.</exception>
|
||||
private IModMetadata GetMod()
|
||||
{
|
||||
return this.Mod ?? throw new InvalidOperationException($"This {nameof(AssetRequestedEventArgs)} instance hasn't been initialized yet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Content
|
||||
{
|
||||
/// <summary>A set of operations to apply to an asset for a given <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/> implementation.</summary>
|
||||
/// <param name="Mod">The mod applying the changes.</param>
|
||||
/// <summary>A set of operations to apply to an asset.</summary>
|
||||
/// <param name="LoadOperations">The load operations to apply.</param>
|
||||
/// <param name="EditOperations">The edit operations to apply.</param>
|
||||
internal record AssetOperationGroup(IModMetadata Mod, AssetLoadOperation[] LoadOperations, AssetEditOperation[] EditOperations);
|
||||
internal record AssetOperationGroup(List<AssetLoadOperation> LoadOperations, List<AssetEditOperation> EditOperations);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework
|
|||
private readonly Action<IList<IAssetName>> OnAssetsInvalidated;
|
||||
|
||||
/// <summary>Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</summary>
|
||||
private readonly Func<IAssetInfo, IList<AssetOperationGroup>> RequestAssetOperations;
|
||||
private readonly Func<IAssetInfo, AssetOperationGroup?> RequestAssetOperations;
|
||||
|
||||
/// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
|
||||
private readonly List<IContentManager> ContentManagers = new();
|
||||
|
@ -79,15 +79,15 @@ namespace StardewModdingAPI.Framework
|
|||
private Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>> LocaleCodes;
|
||||
|
||||
/// <summary>The cached asset load/edit operations to apply, indexed by asset name.</summary>
|
||||
private readonly TickCacheDictionary<IAssetName, IList<AssetOperationGroup>> AssetOperationsByKey = new();
|
||||
private readonly TickCacheDictionary<IAssetName, AssetOperationGroup?> AssetOperationsByKey = new();
|
||||
|
||||
/// <summary>A cache of asset operation groups created for legacy <see cref="IAssetLoader"/> implementations.</summary>
|
||||
[Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")]
|
||||
private readonly Dictionary<IAssetLoader, Dictionary<Type, AssetOperationGroup>> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<IAssetLoader, Dictionary<Type, AssetLoadOperation>> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
/// <summary>A cache of asset operation groups created for legacy <see cref="IAssetEditor"/> implementations.</summary>
|
||||
[Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")]
|
||||
private readonly Dictionary<IAssetEditor, Dictionary<Type, AssetOperationGroup>> LegacyEditorCache = new(ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<IAssetEditor, Dictionary<Type, AssetEditOperation>> LegacyEditorCache = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
|
||||
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
|
||||
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
|
||||
public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, IList<AssetOperationGroup>> requestAssetOperations)
|
||||
public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFileLookup> getFileLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, AssetOperationGroup?> requestAssetOperations)
|
||||
{
|
||||
this.GetFileLookup = getFileLookup;
|
||||
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
|
||||
|
@ -449,16 +449,12 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>Get the asset load and edit operations to apply to a given asset if it's (re)loaded now.</summary>
|
||||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="info">The asset info to load or edit.</param>
|
||||
public IList<AssetOperationGroup> GetAssetOperations<T>(IAssetInfo info)
|
||||
public AssetOperationGroup? GetAssetOperations<T>(IAssetInfo info)
|
||||
where T : notnull
|
||||
{
|
||||
return this.AssetOperationsByKey.GetOrSet(
|
||||
info.Name,
|
||||
#pragma warning disable CS0612, CS0618 // deprecated code
|
||||
() => this.Editors.Count > 0 || this.Loaders.Count > 0
|
||||
? this.GetAssetOperationsIncludingLegacyWithoutCache<T>(info).ToArray()
|
||||
#pragma warning restore CS0612, CS0618
|
||||
: this.RequestAssetOperations(info)
|
||||
() => this.GetAssetOperationsWithoutCache<T>(info)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -584,41 +580,40 @@ namespace StardewModdingAPI.Framework
|
|||
/// <summary>Get the asset load and edit operations to apply to a given asset if it's (re)loaded now, ignoring the <see cref="AssetOperationsByKey"/> cache.</summary>
|
||||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="info">The asset info to load or edit.</param>
|
||||
[Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")]
|
||||
private IEnumerable<AssetOperationGroup> GetAssetOperationsIncludingLegacyWithoutCache<T>(IAssetInfo info)
|
||||
private AssetOperationGroup? GetAssetOperationsWithoutCache<T>(IAssetInfo info)
|
||||
where T : notnull
|
||||
{
|
||||
IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info);
|
||||
|
||||
// new content API
|
||||
foreach (AssetOperationGroup group in this.RequestAssetOperations(info))
|
||||
yield return group;
|
||||
AssetOperationGroup? group = this.RequestAssetOperations(info);
|
||||
|
||||
// legacy load operations
|
||||
foreach (ModLinked<IAssetLoader> loader in this.Loaders)
|
||||
#pragma warning disable CS0612, CS0618 // deprecated code
|
||||
if (this.Editors.Count > 0 || this.Loaders.Count > 0)
|
||||
{
|
||||
// check if loader applies
|
||||
try
|
||||
{
|
||||
if (!loader.Data.CanLoad<T>(legacyInfo))
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
continue;
|
||||
}
|
||||
IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info);
|
||||
|
||||
// add operation
|
||||
yield return this.GetOrCreateLegacyOperationGroup(
|
||||
cache: this.LegacyLoaderCache,
|
||||
editor: loader.Data,
|
||||
dataType: info.DataType,
|
||||
createGroup: () => new AssetOperationGroup(
|
||||
Mod: loader.Mod,
|
||||
LoadOperations: new[]
|
||||
{
|
||||
new AssetLoadOperation(
|
||||
foreach (ModLinked<IAssetLoader> loader in this.Loaders)
|
||||
{
|
||||
// check if loader applies
|
||||
try
|
||||
{
|
||||
if (!loader.Data.CanLoad<T>(legacyInfo))
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
// add operation
|
||||
group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>());
|
||||
group.LoadOperations.Add(
|
||||
this.GetOrCreateLegacyOperation(
|
||||
cache: this.LegacyLoaderCache,
|
||||
editor: loader.Data,
|
||||
dataType: info.DataType,
|
||||
create: () => new AssetLoadOperation(
|
||||
Mod: loader.Mod,
|
||||
OnBehalfOf: null,
|
||||
Priority: AssetLoadPriority.Exclusive,
|
||||
|
@ -626,59 +621,54 @@ namespace StardewModdingAPI.Framework
|
|||
this.GetLegacyAssetInfo(assetInfo)
|
||||
)
|
||||
)
|
||||
},
|
||||
EditOperations: Array.Empty<AssetEditOperation>()
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// legacy edit operations
|
||||
foreach (var editor in this.Editors)
|
||||
{
|
||||
// check if editor applies
|
||||
try
|
||||
// legacy edit operations
|
||||
foreach (var editor in this.Editors)
|
||||
{
|
||||
if (!editor.Data.CanEdit<T>(legacyInfo))
|
||||
// check if editor applies
|
||||
try
|
||||
{
|
||||
if (!editor.Data.CanEdit<T>(legacyInfo))
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// HACK
|
||||
//
|
||||
// If two editors have the same priority, they're applied in registration order (so
|
||||
// whichever was registered first is applied first). Mods often depend on this
|
||||
// behavior, like Json Assets registering its interceptors before Content Patcher.
|
||||
//
|
||||
// Unfortunately the old & new content APIs have separate lists, so new-API
|
||||
// interceptors always ran before old-API interceptors with the same priority,
|
||||
// regardless of the registration order *between* APIs. Since the new API works in
|
||||
// a fundamentally different way (i.e. loads/edits are defined on asset request
|
||||
// instead of by registering a global 'editor' or 'loader' class), there's no way
|
||||
// to track registration order between them.
|
||||
//
|
||||
// Until we drop the old content API in SMAPI 4.0.0, this sets the priority for
|
||||
// specific legacy editors to maintain compatibility.
|
||||
AssetEditPriority priority = editor.Data.GetType().FullName switch
|
||||
{
|
||||
"JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher
|
||||
_ => AssetEditPriority.Default
|
||||
};
|
||||
// HACK
|
||||
//
|
||||
// If two editors have the same priority, they're applied in registration order (so
|
||||
// whichever was registered first is applied first). Mods often depend on this
|
||||
// behavior, like Json Assets registering its interceptors before Content Patcher.
|
||||
//
|
||||
// Unfortunately the old & new content APIs have separate lists, so new-API
|
||||
// interceptors always ran before old-API interceptors with the same priority,
|
||||
// regardless of the registration order *between* APIs. Since the new API works in
|
||||
// a fundamentally different way (i.e. loads/edits are defined on asset request
|
||||
// instead of by registering a global 'editor' or 'loader' class), there's no way
|
||||
// to track registration order between them.
|
||||
//
|
||||
// Until we drop the old content API in SMAPI 4.0.0, this sets the priority for
|
||||
// specific legacy editors to maintain compatibility.
|
||||
AssetEditPriority priority = editor.Data.GetType().FullName switch
|
||||
{
|
||||
"JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher
|
||||
_ => AssetEditPriority.Default
|
||||
};
|
||||
|
||||
// add operation
|
||||
yield return this.GetOrCreateLegacyOperationGroup(
|
||||
cache: this.LegacyEditorCache,
|
||||
editor: editor.Data,
|
||||
dataType: info.DataType,
|
||||
createGroup: () => new AssetOperationGroup(
|
||||
Mod: editor.Mod,
|
||||
LoadOperations: Array.Empty<AssetLoadOperation>(),
|
||||
EditOperations: new[]
|
||||
{
|
||||
new AssetEditOperation(
|
||||
// add operation
|
||||
group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>());
|
||||
group.EditOperations.Add(
|
||||
this.GetOrCreateLegacyOperation(
|
||||
cache: this.LegacyEditorCache,
|
||||
editor: editor.Data,
|
||||
dataType: info.DataType,
|
||||
create: () => new AssetEditOperation(
|
||||
Mod: editor.Mod,
|
||||
OnBehalfOf: null,
|
||||
Priority: priority,
|
||||
|
@ -686,28 +676,32 @@ namespace StardewModdingAPI.Framework
|
|||
this.GetLegacyAssetData(assetData)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0612, CS0618
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>Get a cached asset operation group for a legacy <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/> instance, creating it if needed.</summary>
|
||||
/// <typeparam name="TInterceptor">The editor type (one of <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/>).</typeparam>
|
||||
/// <typeparam name="TOperation">The operation model type.</typeparam>
|
||||
/// <param name="cache">The cached operation groups for the interceptor type.</param>
|
||||
/// <param name="editor">The legacy asset interceptor.</param>
|
||||
/// <param name="dataType">The asset data type.</param>
|
||||
/// <param name="createGroup">Create the asset operation group if it's not cached yet.</param>
|
||||
private AssetOperationGroup GetOrCreateLegacyOperationGroup<TInterceptor>(Dictionary<TInterceptor, Dictionary<Type, AssetOperationGroup>> cache, TInterceptor editor, Type dataType, Func<AssetOperationGroup> createGroup)
|
||||
/// <param name="create">Create the asset operation group if it's not cached yet.</param>
|
||||
private TOperation GetOrCreateLegacyOperation<TInterceptor, TOperation>(Dictionary<TInterceptor, Dictionary<Type, TOperation>> cache, TInterceptor editor, Type dataType, Func<TOperation> create)
|
||||
where TInterceptor : class
|
||||
{
|
||||
if (!cache.TryGetValue(editor, out Dictionary<Type, AssetOperationGroup>? cacheByType))
|
||||
cache[editor] = cacheByType = new Dictionary<Type, AssetOperationGroup>();
|
||||
if (!cache.TryGetValue(editor, out Dictionary<Type, TOperation>? cacheByType))
|
||||
cache[editor] = cacheByType = new Dictionary<Type, TOperation>();
|
||||
|
||||
if (!cacheByType.TryGetValue(dataType, out AssetOperationGroup? group))
|
||||
cacheByType[dataType] = group = createGroup();
|
||||
if (!cacheByType.TryGetValue(dataType, out TOperation? operation))
|
||||
cacheByType[dataType] = operation = create();
|
||||
|
||||
return group;
|
||||
return operation;
|
||||
}
|
||||
|
||||
/// <summary>Get an asset info compatible with legacy <see cref="IAssetLoader"/> and <see cref="IAssetEditor"/> instances, which always expect the base name.</summary>
|
||||
|
|
|
@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
// NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid
|
||||
// throwing other types like ArgumentException here.
|
||||
if (string.IsNullOrWhiteSpace(assetName))
|
||||
throw new SContentLoadException("The asset key or local path is empty.");
|
||||
throw new SContentLoadException(ContentLoadErrorType.InvalidName, "The asset key or local path is empty.");
|
||||
if (assetName.Intersect(Path.GetInvalidPathChars()).Any())
|
||||
throw new SContentLoadException("The asset key or local path contains invalid characters.");
|
||||
throw new SContentLoadException(ContentLoadErrorType.InvalidName, "The asset key or local path contains invalid characters.");
|
||||
|
||||
return this.Cache.NormalizeKey(assetName);
|
||||
}
|
||||
|
|
|
@ -75,15 +75,19 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
// custom asset from a loader
|
||||
string locale = this.GetLocale();
|
||||
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName);
|
||||
AssetLoadOperation[] loaders = this.GetLoaders<object>(info).ToArray();
|
||||
|
||||
if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error))
|
||||
AssetOperationGroup? operations = this.Coordinator.GetAssetOperations<object>(info);
|
||||
if (operations?.LoadOperations.Count > 0)
|
||||
{
|
||||
this.Monitor.Log(error, LogLevel.Warn);
|
||||
return false;
|
||||
if (!this.AssertMaxOneRequiredLoader(info, operations.LoadOperations, out string? error))
|
||||
{
|
||||
this.Monitor.Log(error, LogLevel.Warn);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return loaders.Any();
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -121,10 +125,11 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
data = this.AssetsBeingLoaded.Track(assetName.Name, () =>
|
||||
{
|
||||
IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName);
|
||||
AssetOperationGroup? operations = this.Coordinator.GetAssetOperations<T>(info);
|
||||
IAssetData asset =
|
||||
this.ApplyLoader<T>(info)
|
||||
this.ApplyLoader<T>(info, operations?.LoadOperations)
|
||||
?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection);
|
||||
asset = this.ApplyEditors<T>(info, asset);
|
||||
asset = this.ApplyEditors<T>(info, asset, operations?.EditOperations);
|
||||
return (T)asset.Data;
|
||||
});
|
||||
}
|
||||
|
@ -149,25 +154,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
*********/
|
||||
/// <summary>Load the initial asset from the registered loaders.</summary>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
/// <param name="loadOperations">The load operations to apply to the asset.</param>
|
||||
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
|
||||
private IAssetData? ApplyLoader<T>(IAssetInfo info)
|
||||
private IAssetData? ApplyLoader<T>(IAssetInfo info, List<AssetLoadOperation>? loadOperations)
|
||||
where T : notnull
|
||||
{
|
||||
// find matching loader
|
||||
AssetLoadOperation? loader;
|
||||
AssetLoadOperation? loader = null;
|
||||
if (loadOperations?.Count > 0)
|
||||
{
|
||||
AssetLoadOperation[] loaders = this.GetLoaders<T>(info).OrderByDescending(p => p.Priority).ToArray();
|
||||
|
||||
if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error))
|
||||
if (!this.AssertMaxOneRequiredLoader(info, loadOperations, out string? error))
|
||||
{
|
||||
this.Monitor.Log(error, LogLevel.Warn);
|
||||
return null;
|
||||
}
|
||||
|
||||
loader = loaders.FirstOrDefault();
|
||||
loader = loadOperations.OrderByDescending(p => p.Priority).FirstOrDefault();
|
||||
}
|
||||
|
||||
// no loader found
|
||||
if (loader == null)
|
||||
return null;
|
||||
|
||||
|
@ -195,9 +198,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
/// <param name="asset">The loaded asset.</param>
|
||||
private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset)
|
||||
/// <param name="editOperations">The edit operations to apply to the asset.</param>
|
||||
private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset, List<AssetEditOperation>? editOperations)
|
||||
where T : notnull
|
||||
{
|
||||
if (editOperations?.Count is not > 0)
|
||||
return asset;
|
||||
|
||||
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.
|
||||
|
@ -210,12 +217,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
return (IAssetData)this.GetType()
|
||||
.GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.MakeGenericMethod(actualType)
|
||||
.Invoke(this, new object[] { info, asset })!;
|
||||
.Invoke(this, new object[] { info, asset, editOperations })!;
|
||||
}
|
||||
}
|
||||
|
||||
// edit asset
|
||||
AssetEditOperation[] editors = this.GetEditors<T>(info).OrderBy(p => p.Priority).ToArray();
|
||||
AssetEditOperation[] editors = editOperations.OrderBy(p => p.Priority).ToArray();
|
||||
foreach (AssetEditOperation editor in editors)
|
||||
{
|
||||
IModMetadata mod = editor.Mod;
|
||||
|
@ -250,34 +257,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
return asset;
|
||||
}
|
||||
|
||||
/// <summary>Get the asset loaders which handle an asset.</summary>
|
||||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info)
|
||||
where T : notnull
|
||||
{
|
||||
return this.Coordinator
|
||||
.GetAssetOperations<T>(info)
|
||||
.SelectMany(p => p.LoadOperations);
|
||||
}
|
||||
|
||||
/// <summary>Get the asset editors to apply to an asset.</summary>
|
||||
/// <typeparam name="T">The asset type.</typeparam>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info)
|
||||
where T : notnull
|
||||
{
|
||||
return this.Coordinator
|
||||
.GetAssetOperations<T>(info)
|
||||
.SelectMany(p => p.EditOperations);
|
||||
}
|
||||
|
||||
/// <summary>Assert that at most one loader will be applied to an asset.</summary>
|
||||
/// <param name="info">The basic asset metadata.</param>
|
||||
/// <param name="loaders">The asset loaders to apply.</param>
|
||||
/// <param name="error">The error message to show to the user, if the method returns false.</param>
|
||||
/// <returns>Returns true if only one loader will apply, else false.</returns>
|
||||
private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, [NotNullWhen(false)] out string? error)
|
||||
private bool AssertMaxOneRequiredLoader(IAssetInfo info, List<AssetLoadOperation> loaders, [NotNullWhen(false)] out string? error)
|
||||
{
|
||||
AssetLoadOperation[] required = loaders.Where(p => p.Priority == AssetLoadPriority.Exclusive).ToArray();
|
||||
if (required.Length <= 1)
|
||||
|
@ -295,7 +280,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
? $"Multiple mods want to provide the '{info.Name}' asset: {string.Join(", ", loaderNames)}"
|
||||
: $"The '{loaderNames[0]}' mod wants to provide the '{info.Name}' asset multiple times";
|
||||
|
||||
error = $"{errorPhrase}. An asset can't be loaded multiple times, so 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.)";
|
||||
error = $"{errorPhrase}. An asset can't be loaded multiple times, so SMAPI will use the default asset instead. Uninstall one of the mods to fix this. (Message for modders: you should avoid {nameof(AssetLoadPriority)}.{nameof(AssetLoadPriority.Exclusive)} and {nameof(IAssetLoader)} if possible to avoid conflicts.)";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
|
||||
{
|
||||
if (contentManagerID != this.Name)
|
||||
throw this.GetLoadError(assetName, "can't load a different mod's managed asset key through this mod content manager.");
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager.");
|
||||
assetName = relativePath;
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
// get file
|
||||
FileInfo file = this.GetModFile<T>(assetName.Name);
|
||||
if (!file.Exists)
|
||||
throw this.GetLoadError(assetName, "the specified path doesn't exist.");
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist.");
|
||||
|
||||
// load content
|
||||
asset = file.Extension.ToLower() switch
|
||||
|
@ -121,7 +121,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
}
|
||||
catch (Exception ex) when (ex is not SContentLoadException)
|
||||
{
|
||||
throw this.GetLoadError(assetName, "an unexpected occurred.", ex);
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected occurred.", ex);
|
||||
}
|
||||
|
||||
// track & return asset
|
||||
|
@ -157,7 +157,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
{
|
||||
// validate
|
||||
if (!typeof(T).IsAssignableFrom(typeof(XmlSource)))
|
||||
throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'.");
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'.");
|
||||
|
||||
// load
|
||||
string source = File.ReadAllText(file.FullName);
|
||||
|
@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
private T LoadDataFile<T>(IAssetName assetName, FileInfo file)
|
||||
{
|
||||
if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset))
|
||||
throw this.GetLoadError(assetName, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
{
|
||||
// validate
|
||||
if (typeof(T) != typeof(Texture2D))
|
||||
throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
|
||||
|
||||
// load
|
||||
using FileStream stream = File.OpenRead(file.FullName);
|
||||
|
@ -201,7 +201,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
{
|
||||
// validate
|
||||
if (typeof(T) != typeof(Map))
|
||||
throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
|
||||
|
||||
// load
|
||||
FormatManager formatManager = FormatManager.Instance;
|
||||
|
@ -239,16 +239,17 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
/// <param name="file">The file to load.</param>
|
||||
private T HandleUnknownFileType<T>(IAssetName assetName, FileInfo file)
|
||||
{
|
||||
throw this.GetLoadError(assetName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
|
||||
throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
|
||||
}
|
||||
|
||||
/// <summary>Get an error which indicates that an asset couldn't be loaded.</summary>
|
||||
/// <param name="errorType">Why loading an asset through the content pipeline failed.</param>
|
||||
/// <param name="assetName">The asset name that failed to load.</param>
|
||||
/// <param name="reasonPhrase">The reason the file couldn't be loaded.</param>
|
||||
/// <param name="exception">The underlying exception, if applicable.</param>
|
||||
private SContentLoadException GetLoadError(IAssetName assetName, string reasonPhrase, Exception? exception = null)
|
||||
private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null)
|
||||
{
|
||||
return new($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
|
||||
return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
|
||||
}
|
||||
|
||||
/// <summary>Get a file from the mod folder.</summary>
|
||||
|
@ -328,13 +329,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
// validate tilesheet path
|
||||
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
|
||||
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
|
||||
throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
|
||||
throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
|
||||
|
||||
// load best match
|
||||
try
|
||||
{
|
||||
if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName? assetName, out string? error))
|
||||
throw new SContentLoadException($"{errorPrefix} {error}");
|
||||
throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} {error}");
|
||||
|
||||
if (assetName is not null)
|
||||
{
|
||||
|
@ -346,7 +347,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
}
|
||||
catch (Exception ex) when (ex is not SContentLoadException)
|
||||
{
|
||||
throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex);
|
||||
throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.Events
|
|||
public EventManager(ModRegistry modRegistry)
|
||||
{
|
||||
// create shortcut initializers
|
||||
ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName, bool isPerformanceCritical = false)
|
||||
ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName)
|
||||
{
|
||||
return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry, isPerformanceCritical);
|
||||
return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry);
|
||||
}
|
||||
|
||||
// init events
|
||||
|
@ -210,21 +210,21 @@ namespace StardewModdingAPI.Framework.Events
|
|||
this.LocaleChanged = ManageEventOf<LocaleChangedEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged));
|
||||
|
||||
this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged));
|
||||
this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true);
|
||||
this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true);
|
||||
this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true);
|
||||
this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true);
|
||||
this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true);
|
||||
this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true);
|
||||
this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true);
|
||||
this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true);
|
||||
this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering));
|
||||
this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered));
|
||||
this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld));
|
||||
this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld));
|
||||
this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu));
|
||||
this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu));
|
||||
this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud));
|
||||
this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud));
|
||||
this.WindowResized = ManageEventOf<WindowResizedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized));
|
||||
|
||||
this.GameLaunched = ManageEventOf<GameLaunchedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched));
|
||||
this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true);
|
||||
this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true);
|
||||
this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true);
|
||||
this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true);
|
||||
this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking));
|
||||
this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked));
|
||||
this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking));
|
||||
this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked));
|
||||
this.SaveCreating = ManageEventOf<SaveCreatingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating));
|
||||
this.SaveCreated = ManageEventOf<SaveCreatedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated));
|
||||
this.Saving = ManageEventOf<SavingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving));
|
||||
|
@ -238,7 +238,7 @@ namespace StardewModdingAPI.Framework.Events
|
|||
this.ButtonsChanged = ManageEventOf<ButtonsChangedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonsChanged));
|
||||
this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed));
|
||||
this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased));
|
||||
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true);
|
||||
this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved));
|
||||
this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled));
|
||||
|
||||
this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived));
|
||||
|
@ -261,8 +261,8 @@ namespace StardewModdingAPI.Framework.Events
|
|||
this.FurnitureListChanged = ManageEventOf<FurnitureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.FurnitureListChanged));
|
||||
|
||||
this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged));
|
||||
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true);
|
||||
this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true);
|
||||
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking));
|
||||
this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace StardewModdingAPI.Framework.Events
|
|||
/// <summary>A human-readable name for the event.</summary>
|
||||
string EventName { get; }
|
||||
|
||||
/// <summary>Whether the event is typically called at least once per second.</summary>
|
||||
bool IsPerformanceCritical { get; }
|
||||
/// <summary>Whether any handlers are listening to the event.</summary>
|
||||
bool HasListeners { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,14 +20,17 @@ namespace StardewModdingAPI.Framework.Events
|
|||
/// <summary>The underlying event handlers.</summary>
|
||||
private readonly List<ManagedEventHandler<TEventArgs>> Handlers = new();
|
||||
|
||||
/// <summary>A cached snapshot of <see cref="Handlers"/>, or <c>null</c> to rebuild it next raise.</summary>
|
||||
/// <summary>A cached snapshot of the <see cref="Handlers"/> sorted by event priority, or <c>null</c> to rebuild it next raise.</summary>
|
||||
private ManagedEventHandler<TEventArgs>[]? CachedHandlers = Array.Empty<ManagedEventHandler<TEventArgs>>();
|
||||
|
||||
/// <summary>The total number of event handlers registered for this events, regardless of whether they're still registered.</summary>
|
||||
private int RegistrationIndex;
|
||||
|
||||
/// <summary>Whether new handlers were added since the last raise.</summary>
|
||||
private bool HasNewHandlers;
|
||||
/// <summary>Whether handlers were removed since the last raise.</summary>
|
||||
private bool HasRemovedHandlers;
|
||||
|
||||
/// <summary>Whether any of the handlers have a custom priority.</summary>
|
||||
private bool HasPriorities;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -37,7 +40,7 @@ namespace StardewModdingAPI.Framework.Events
|
|||
public string EventName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPerformanceCritical { get; }
|
||||
public bool HasListeners { get; private set; }
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -46,18 +49,10 @@ namespace StardewModdingAPI.Framework.Events
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="eventName">A human-readable name for the event.</param>
|
||||
/// <param name="modRegistry">The mod registry with which to identify mods.</param>
|
||||
/// <param name="isPerformanceCritical">Whether the event is typically called at least once per second.</param>
|
||||
public ManagedEvent(string eventName, ModRegistry modRegistry, bool isPerformanceCritical = false)
|
||||
public ManagedEvent(string eventName, ModRegistry modRegistry)
|
||||
{
|
||||
this.EventName = eventName;
|
||||
this.ModRegistry = modRegistry;
|
||||
this.IsPerformanceCritical = isPerformanceCritical;
|
||||
}
|
||||
|
||||
/// <summary>Get whether anything is listening to the event.</summary>
|
||||
public bool HasListeners()
|
||||
{
|
||||
return this.Handlers.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>Add an event handler.</summary>
|
||||
|
@ -72,7 +67,8 @@ namespace StardewModdingAPI.Framework.Events
|
|||
|
||||
this.Handlers.Add(managedHandler);
|
||||
this.CachedHandlers = null;
|
||||
this.HasNewHandlers = true;
|
||||
this.HasListeners = true;
|
||||
this.HasPriorities |= priority != EventPriority.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +86,8 @@ namespace StardewModdingAPI.Framework.Events
|
|||
|
||||
this.Handlers.RemoveAt(i);
|
||||
this.CachedHandlers = null;
|
||||
this.HasListeners = this.Handlers.Count != 0;
|
||||
this.HasRemovedHandlers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -97,46 +95,40 @@ namespace StardewModdingAPI.Framework.Events
|
|||
|
||||
/// <summary>Raise the event and notify all handlers.</summary>
|
||||
/// <param name="args">The event arguments to pass.</param>
|
||||
/// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param>
|
||||
public void Raise(TEventArgs args, Func<IModMetadata, bool>? match = null)
|
||||
{
|
||||
this.Raise((_, invoke) => invoke(args), match);
|
||||
}
|
||||
|
||||
/// <summary>Raise the event and notify all handlers.</summary>
|
||||
/// <param name="invoke">Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it.</param>
|
||||
/// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param>
|
||||
public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke, Func<IModMetadata, bool>? match = null)
|
||||
public void Raise(TEventArgs args)
|
||||
{
|
||||
// skip if no handlers
|
||||
if (this.Handlers.Count == 0)
|
||||
return;
|
||||
|
||||
// update cached data
|
||||
// (This is debounced here to avoid repeatedly sorting when handlers are added/removed,
|
||||
// and keeping a separate cached list allows changes during enumeration.)
|
||||
var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event, which will set this field to null
|
||||
if (handlers == null)
|
||||
{
|
||||
lock (this.Handlers)
|
||||
{
|
||||
if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal))
|
||||
this.Handlers.Sort();
|
||||
|
||||
this.CachedHandlers = handlers = this.Handlers.ToArray();
|
||||
this.HasNewHandlers = false;
|
||||
}
|
||||
}
|
||||
|
||||
// raise event
|
||||
foreach (ManagedEventHandler<TEventArgs> handler in handlers)
|
||||
foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
|
||||
{
|
||||
if (match != null && !match(handler.SourceMod))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
invoke(handler.SourceMod, args => handler.Handler.Invoke(null, args));
|
||||
handler.Handler(null, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(handler, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Raise the event and notify all handlers.</summary>
|
||||
/// <param name="invoke">Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it.</param>
|
||||
public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke)
|
||||
{
|
||||
// skip if no handlers
|
||||
if (this.Handlers.Count == 0)
|
||||
return;
|
||||
|
||||
// raise event
|
||||
foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
|
||||
{
|
||||
try
|
||||
{
|
||||
invoke(handler.SourceMod, args => handler.Handler(null, args));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -152,9 +144,36 @@ namespace StardewModdingAPI.Framework.Events
|
|||
/// <summary>Log an exception from an event handler.</summary>
|
||||
/// <param name="handler">The event handler instance.</param>
|
||||
/// <param name="ex">The exception that was raised.</param>
|
||||
protected void LogError(ManagedEventHandler<TEventArgs> handler, Exception ex)
|
||||
private void LogError(ManagedEventHandler<TEventArgs> handler, Exception ex)
|
||||
{
|
||||
handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
|
||||
/// <summary>Get cached copy of the sorted handlers to invoke.</summary>
|
||||
/// <remarks>This returns the handlers sorted by priority, and allows iterating the list even if a mod adds/removes handlers while handling it. This is debounced when requested to avoid repeatedly sorting when handlers are added/removed.</remarks>
|
||||
private ManagedEventHandler<TEventArgs>[] GetHandlers()
|
||||
{
|
||||
ManagedEventHandler<TEventArgs>[]? handlers = this.CachedHandlers;
|
||||
|
||||
if (handlers == null)
|
||||
{
|
||||
lock (this.Handlers)
|
||||
{
|
||||
// recheck priorities
|
||||
if (this.HasRemovedHandlers)
|
||||
this.HasPriorities = this.Handlers.Any(p => p.Priority != EventPriority.Normal);
|
||||
|
||||
// sort by priority if needed
|
||||
if (this.HasPriorities)
|
||||
this.Handlers.Sort();
|
||||
|
||||
// update cache
|
||||
this.CachedHandlers = handlers = this.Handlers.ToArray();
|
||||
this.HasRemovedHandlers = false;
|
||||
}
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
namespace StardewModdingAPI.Framework.Exceptions
|
||||
{
|
||||
/// <summary>Indicates why loading an asset through the content pipeline failed.</summary>
|
||||
internal enum ContentLoadErrorType
|
||||
{
|
||||
/// <summary>The asset name is empty or has an invalid format.</summary>
|
||||
InvalidName,
|
||||
|
||||
/// <summary>The asset doesn't exist.</summary>
|
||||
AssetDoesNotExist,
|
||||
|
||||
/// <summary>The asset is not available in the current context (e.g. an attempt to load another mod's assets).</summary>
|
||||
AccessDenied,
|
||||
|
||||
/// <summary>The asset exists, but the data could not be deserialized or it doesn't match the expected type.</summary>
|
||||
InvalidData,
|
||||
|
||||
/// <summary>An unknown error occurred.</summary>
|
||||
Other
|
||||
}
|
||||
}
|
|
@ -6,13 +6,24 @@ namespace StardewModdingAPI.Framework.Exceptions
|
|||
/// <summary>An implementation of <see cref="ContentLoadException"/> used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework.</summary>
|
||||
internal class SContentLoadException : ContentLoadException
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Why loading the asset through the content pipeline failed.</summary>
|
||||
public ContentLoadErrorType ErrorType { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="errorType">Why loading the asset through the content pipeline failed.</param>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="ex">The underlying exception, if any.</param>
|
||||
public SContentLoadException(string message, Exception? ex = null)
|
||||
: base(message, ex) { }
|
||||
public SContentLoadException(ContentLoadErrorType errorType, string message, Exception? ex = null)
|
||||
: base(message, ex)
|
||||
{
|
||||
this.ErrorType = errorType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="event">The event to raise.</param>
|
||||
public static void RaiseEmpty<TEventArgs>(this ManagedEvent<TEventArgs> @event) where TEventArgs : new()
|
||||
{
|
||||
@event.Raise(Singleton<TEventArgs>.Instance);
|
||||
if (@event.HasListeners)
|
||||
@event.Raise(Singleton<TEventArgs>.Instance);
|
||||
}
|
||||
|
||||
/****
|
||||
|
|
|
@ -132,15 +132,40 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
return this.GameContentManager.LoadLocalized<T>(assetName, this.CurrentLocaleConstant, useCache: false);
|
||||
|
||||
case ContentSource.ModFolder:
|
||||
return this.ModContentManager.LoadExact<T>(assetName, useCache: false);
|
||||
try
|
||||
{
|
||||
return this.ModContentManager.LoadExact<T>(assetName, useCache: false);
|
||||
}
|
||||
catch (SContentLoadException ex) when (ex.ErrorType == ContentLoadErrorType.AssetDoesNotExist)
|
||||
{
|
||||
// legacy behavior: you can load a .xnb file without the file extension
|
||||
try
|
||||
{
|
||||
IAssetName newName = this.ContentCore.ParseAssetName(assetName.Name + ".xnb", allowLocales: false);
|
||||
if (this.ModContentManager.DoesAssetExist<T>(newName))
|
||||
{
|
||||
T data = this.ModContentManager.LoadExact<T>(newName, useCache: false);
|
||||
SCore.DeprecationManager.Warn(
|
||||
this.Mod,
|
||||
"loading XNB files from the mod folder without the .xnb file extension",
|
||||
"3.14.0",
|
||||
DeprecationLevel.Notice
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
catch { /* legacy behavior failed, rethrow original error */ }
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
|
||||
throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not SContentLoadException)
|
||||
{
|
||||
throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex);
|
||||
throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
}
|
||||
catch (Exception ex) when (ex is not SContentLoadException)
|
||||
{
|
||||
throw new SContentLoadException($"{this.ModName} failed loading content asset '{assetName}' from the game content.", ex);
|
||||
throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.ModName} failed loading content asset '{assetName}' from the game content.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
}
|
||||
catch (Exception ex) when (ex is not SContentLoadException)
|
||||
{
|
||||
throw new SContentLoadException($"{this.ModName} failed loading content asset '{relativePath}' from its mod folder.", ex);
|
||||
throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.ModName} failed loading content asset '{relativePath}' from its mod folder.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ using StardewModdingAPI.Toolkit.Utilities.PathLookups;
|
|||
using StardewModdingAPI.Utilities;
|
||||
using StardewValley;
|
||||
using StardewValley.Menus;
|
||||
using StardewValley.Objects;
|
||||
using xTile.Display;
|
||||
using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
|
||||
using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
|
||||
|
@ -577,6 +578,7 @@ namespace StardewModdingAPI.Framework
|
|||
private void OnPlayerInstanceUpdating(SGame instance, GameTime gameTime, Action runUpdate)
|
||||
{
|
||||
EventManager events = this.EventManager;
|
||||
bool verbose = this.Monitor.IsVerbose;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -804,10 +806,11 @@ namespace StardewModdingAPI.Framework
|
|||
// since the game adds & removes its own handler on the fly.
|
||||
if (state.WindowSize.IsChanged)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}.");
|
||||
|
||||
events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New));
|
||||
if (events.WindowResized.HasListeners)
|
||||
events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New));
|
||||
}
|
||||
|
||||
/*********
|
||||
|
@ -822,40 +825,50 @@ namespace StardewModdingAPI.Framework
|
|||
ICursorPosition cursor = instance.Input.CursorPosition;
|
||||
|
||||
// raise cursor moved event
|
||||
if (state.Cursor.IsChanged)
|
||||
if (state.Cursor.IsChanged && events.CursorMoved.HasListeners)
|
||||
events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old!, state.Cursor.New!));
|
||||
|
||||
// raise mouse wheel scrolled
|
||||
if (state.MouseWheelScroll.IsChanged)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}.");
|
||||
events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New));
|
||||
|
||||
if (events.MouseWheelScrolled.HasListeners)
|
||||
events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New));
|
||||
}
|
||||
|
||||
// raise input button events
|
||||
if (inputState.ButtonStates.Count > 0)
|
||||
{
|
||||
events.ButtonsChanged.Raise(new ButtonsChangedEventArgs(cursor, inputState));
|
||||
if (events.ButtonsChanged.HasListeners)
|
||||
events.ButtonsChanged.Raise(new ButtonsChangedEventArgs(cursor, inputState));
|
||||
|
||||
foreach (var pair in inputState.ButtonStates)
|
||||
bool raisePressed = events.ButtonPressed.HasListeners;
|
||||
bool raiseReleased = events.ButtonReleased.HasListeners;
|
||||
|
||||
if (verbose || raisePressed || raiseReleased)
|
||||
{
|
||||
SButton button = pair.Key;
|
||||
SButtonState status = pair.Value;
|
||||
|
||||
if (status == SButtonState.Pressed)
|
||||
foreach ((SButton button, SButtonState status) in inputState.ButtonStates)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
this.Monitor.Log($"Events: button {button} pressed.");
|
||||
switch (status)
|
||||
{
|
||||
case SButtonState.Pressed:
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Events: button {button} pressed.");
|
||||
|
||||
events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
|
||||
}
|
||||
else if (status == SButtonState.Released)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
this.Monitor.Log($"Events: button {button} released.");
|
||||
if (raisePressed)
|
||||
events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
|
||||
break;
|
||||
|
||||
events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState));
|
||||
case SButtonState.Released:
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Events: button {button} released.");
|
||||
|
||||
if (raiseReleased)
|
||||
events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -867,14 +880,15 @@ namespace StardewModdingAPI.Framework
|
|||
*********/
|
||||
if (state.ActiveMenu.IsChanged)
|
||||
{
|
||||
var was = state.ActiveMenu.Old;
|
||||
var now = state.ActiveMenu.New;
|
||||
IClickableMenu? was = state.ActiveMenu.Old;
|
||||
IClickableMenu? now = state.ActiveMenu.New;
|
||||
|
||||
if (this.Monitor.IsVerbose)
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.");
|
||||
|
||||
// raise menu events
|
||||
events.MenuChanged.Raise(new MenuChangedEventArgs(was, now));
|
||||
if (events.MenuChanged.HasListeners)
|
||||
events.MenuChanged.Raise(new MenuChangedEventArgs(was, now));
|
||||
}
|
||||
|
||||
/*********
|
||||
|
@ -885,19 +899,20 @@ namespace StardewModdingAPI.Framework
|
|||
bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded
|
||||
|
||||
// location list changes
|
||||
if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || this.Monitor.IsVerbose))
|
||||
if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners || verbose))
|
||||
{
|
||||
var added = state.Locations.LocationList.Added.ToArray();
|
||||
var removed = state.Locations.LocationList.Removed.ToArray();
|
||||
|
||||
if (this.Monitor.IsVerbose)
|
||||
if (verbose)
|
||||
{
|
||||
string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none";
|
||||
string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none";
|
||||
this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).");
|
||||
}
|
||||
|
||||
events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed));
|
||||
if (events.LocationListChanged.HasListeners)
|
||||
events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed));
|
||||
}
|
||||
|
||||
// raise location contents changed
|
||||
|
@ -905,51 +920,54 @@ namespace StardewModdingAPI.Framework
|
|||
{
|
||||
foreach (LocationSnapshot locState in state.Locations.Locations)
|
||||
{
|
||||
var location = locState.Location;
|
||||
GameLocation location = locState.Location;
|
||||
|
||||
// buildings changed
|
||||
if (locState.Buildings.IsChanged)
|
||||
if (locState.Buildings.IsChanged && events.BuildingListChanged.HasListeners)
|
||||
events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, locState.Buildings.Added, locState.Buildings.Removed));
|
||||
|
||||
// debris changed
|
||||
if (locState.Debris.IsChanged)
|
||||
if (locState.Debris.IsChanged && events.DebrisListChanged.HasListeners)
|
||||
events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, locState.Debris.Added, locState.Debris.Removed));
|
||||
|
||||
// large terrain features changed
|
||||
if (locState.LargeTerrainFeatures.IsChanged)
|
||||
if (locState.LargeTerrainFeatures.IsChanged && events.LargeTerrainFeatureListChanged.HasListeners)
|
||||
events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, locState.LargeTerrainFeatures.Added, locState.LargeTerrainFeatures.Removed));
|
||||
|
||||
// NPCs changed
|
||||
if (locState.Npcs.IsChanged)
|
||||
if (locState.Npcs.IsChanged && events.NpcListChanged.HasListeners)
|
||||
events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, locState.Npcs.Added, locState.Npcs.Removed));
|
||||
|
||||
// objects changed
|
||||
if (locState.Objects.IsChanged)
|
||||
if (locState.Objects.IsChanged && events.ObjectListChanged.HasListeners)
|
||||
events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed));
|
||||
|
||||
// chest items changed
|
||||
if (events.ChestInventoryChanged.HasListeners())
|
||||
if (events.ChestInventoryChanged.HasListeners)
|
||||
{
|
||||
foreach (var pair in locState.ChestItems)
|
||||
{
|
||||
SnapshotItemListDiff diff = pair.Value;
|
||||
events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged));
|
||||
}
|
||||
foreach ((Chest chest, SnapshotItemListDiff diff) in locState.ChestItems)
|
||||
events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(chest, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged));
|
||||
}
|
||||
|
||||
// terrain features changed
|
||||
if (locState.TerrainFeatures.IsChanged)
|
||||
if (locState.TerrainFeatures.IsChanged && events.TerrainFeatureListChanged.HasListeners)
|
||||
events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed));
|
||||
|
||||
// furniture changed
|
||||
if (locState.Furniture.IsChanged)
|
||||
if (locState.Furniture.IsChanged && events.FurnitureListChanged.HasListeners)
|
||||
events.FurnitureListChanged.Raise(new FurnitureListChangedEventArgs(location, locState.Furniture.Added, locState.Furniture.Removed));
|
||||
}
|
||||
}
|
||||
|
||||
// raise time changed
|
||||
if (raiseWorldEvents && state.Time.IsChanged)
|
||||
events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New));
|
||||
{
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Context: time changed to {state.Time.New}.");
|
||||
|
||||
if (events.TimeChanged.HasListeners)
|
||||
events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New));
|
||||
}
|
||||
|
||||
// raise player events
|
||||
if (raiseWorldEvents)
|
||||
|
@ -960,32 +978,41 @@ namespace StardewModdingAPI.Framework
|
|||
// raise current location changed
|
||||
if (playerState.Location.IsChanged)
|
||||
{
|
||||
if (this.Monitor.IsVerbose)
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Context: set location to {playerState.Location.New}.");
|
||||
|
||||
events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old!, playerState.Location.New!));
|
||||
if (events.Warped.HasListeners)
|
||||
events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old!, playerState.Location.New!));
|
||||
}
|
||||
|
||||
// raise player leveled up a skill
|
||||
foreach ((SkillType skill, var value) in playerState.Skills)
|
||||
bool raiseLevelChanged = events.LevelChanged.HasListeners;
|
||||
if (verbose || raiseLevelChanged)
|
||||
{
|
||||
if (!value.IsChanged)
|
||||
continue;
|
||||
foreach ((SkillType skill, var value) in playerState.Skills)
|
||||
{
|
||||
if (!value.IsChanged)
|
||||
continue;
|
||||
|
||||
if (this.Monitor.IsVerbose)
|
||||
this.Monitor.Log($"Events: player skill '{skill}' changed from {value.Old} to {value.New}.");
|
||||
if (verbose)
|
||||
this.Monitor.Log($"Events: player skill '{skill}' changed from {value.Old} to {value.New}.");
|
||||
|
||||
events.LevelChanged.Raise(new LevelChangedEventArgs(player, skill, value.Old, value.New));
|
||||
if (raiseLevelChanged)
|
||||
events.LevelChanged.Raise(new LevelChangedEventArgs(player, skill, value.Old, value.New));
|
||||
}
|
||||
}
|
||||
|
||||
// raise player inventory changed
|
||||
if (playerState.Inventory.IsChanged)
|
||||
{
|
||||
SnapshotItemListDiff inventory = playerState.Inventory;
|
||||
|
||||
if (this.Monitor.IsVerbose)
|
||||
if (verbose)
|
||||
this.Monitor.Log("Events: player inventory changed.");
|
||||
events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged));
|
||||
|
||||
if (events.InventoryChanged.HasListeners)
|
||||
{
|
||||
SnapshotItemListDiff inventory = playerState.Inventory;
|
||||
events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -997,7 +1024,9 @@ namespace StardewModdingAPI.Framework
|
|||
if (instance.IsFirstTick && !Context.IsGameLaunched)
|
||||
{
|
||||
Context.IsGameLaunched = true;
|
||||
events.GameLaunched.Raise(new GameLaunchedEventArgs());
|
||||
|
||||
if (events.GameLaunched.HasListeners)
|
||||
events.GameLaunched.Raise(new GameLaunchedEventArgs());
|
||||
}
|
||||
|
||||
// preloaded
|
||||
|
@ -1076,7 +1105,7 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
|
||||
// raise event
|
||||
if (this.EventManager.LocaleChanged.HasListeners())
|
||||
if (this.EventManager.LocaleChanged.HasListeners)
|
||||
{
|
||||
this.EventManager.LocaleChanged.Raise(
|
||||
new LocaleChangedEventArgs(
|
||||
|
@ -1123,9 +1152,11 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
|
||||
// raise events
|
||||
this.EventManager.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage));
|
||||
EventManager events = this.EventManager;
|
||||
if (events.LoadStageChanged.HasListeners)
|
||||
events.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage));
|
||||
if (newStage == LoadStage.None)
|
||||
this.EventManager.ReturnedToTitle.RaiseEmpty();
|
||||
events.ReturnedToTitle.RaiseEmpty();
|
||||
}
|
||||
|
||||
/// <summary>A callback invoked before <see cref="Game1.newDayAfterFade"/> runs.</summary>
|
||||
|
@ -1139,7 +1170,7 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="assetName">The asset name that was loaded.</param>
|
||||
private void OnAssetLoaded(IContentManager contentManager, IAssetName assetName)
|
||||
{
|
||||
if (this.EventManager.AssetReady.HasListeners())
|
||||
if (this.EventManager.AssetReady.HasListeners)
|
||||
this.EventManager.AssetReady.Raise(new AssetReadyEventArgs(assetName, assetName.GetBaseAssetName()));
|
||||
}
|
||||
|
||||
|
@ -1147,33 +1178,33 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="assetNames">The invalidated asset names.</param>
|
||||
private void OnAssetsInvalidated(IList<IAssetName> assetNames)
|
||||
{
|
||||
if (this.EventManager.AssetsInvalidated.HasListeners())
|
||||
if (this.EventManager.AssetsInvalidated.HasListeners)
|
||||
this.EventManager.AssetsInvalidated.Raise(new AssetsInvalidatedEventArgs(assetNames, assetNames.Select(p => p.GetBaseAssetName())));
|
||||
}
|
||||
|
||||
/// <summary>Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</summary>
|
||||
/// <param name="asset">The asset info being requested.</param>
|
||||
private IList<AssetOperationGroup> RequestAssetOperations(IAssetInfo asset)
|
||||
private AssetOperationGroup? RequestAssetOperations(IAssetInfo asset)
|
||||
{
|
||||
List<AssetOperationGroup> operations = new();
|
||||
// get event
|
||||
var requestedEvent = this.EventManager.AssetRequested;
|
||||
if (!requestedEvent.HasListeners)
|
||||
return null;
|
||||
|
||||
this.EventManager.AssetRequested.Raise(
|
||||
// raise event
|
||||
AssetRequestedEventArgs args = new(asset, this.GetOnBehalfOfContentPack);
|
||||
requestedEvent.Raise(
|
||||
invoke: (mod, invoke) =>
|
||||
{
|
||||
AssetRequestedEventArgs args = new(mod, asset, this.GetOnBehalfOfContentPack);
|
||||
|
||||
args.SetMod(mod);
|
||||
invoke(args);
|
||||
|
||||
if (args.LoadOperations.Any() || args.EditOperations.Any())
|
||||
{
|
||||
operations.Add(
|
||||
new AssetOperationGroup(mod, args.LoadOperations.ToArray(), args.EditOperations.ToArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return operations;
|
||||
// collect operations
|
||||
return args.LoadOperations.Count != 0 || args.EditOperations.Count != 0
|
||||
? new AssetOperationGroup(args.LoadOperations, args.EditOperations)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Get the mod metadata for a content pack whose ID matches <paramref name="id"/>, if it's a valid content pack for the given <paramref name="mod"/>.</summary>
|
||||
|
@ -1225,13 +1256,26 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="message">The message to deliver to applicable mods.</param>
|
||||
private void OnModMessageReceived(ModMessageModel message)
|
||||
{
|
||||
// get mod IDs to notify
|
||||
HashSet<string> modIDs = new HashSet<string>(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
|
||||
if (message.FromPlayerID == Game1.player?.UniqueMultiplayerID)
|
||||
modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender
|
||||
if (this.EventManager.ModMessageReceived.HasListeners)
|
||||
{
|
||||
// get mod IDs to notify
|
||||
HashSet<string> modIDs = new(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.OrdinalIgnoreCase);
|
||||
if (message.FromPlayerID == Game1.player?.UniqueMultiplayerID)
|
||||
modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender
|
||||
|
||||
// raise events
|
||||
this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper), mod => modIDs.Contains(mod.Manifest.UniqueID));
|
||||
// raise events
|
||||
ModMessageReceivedEventArgs? args = null;
|
||||
this.EventManager.ModMessageReceived.Raise(
|
||||
invoke: (mod, invoke) =>
|
||||
{
|
||||
if (modIDs.Contains(mod.Manifest.UniqueID))
|
||||
{
|
||||
args ??= new(message, this.Toolkit.JsonHelper);
|
||||
invoke(args);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Constructor a content manager to read game content files.</summary>
|
||||
|
|
|
@ -353,7 +353,7 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
if (Game1.currentMinigame != null)
|
||||
{
|
||||
if (events.Rendering.HasListeners())
|
||||
if (events.Rendering.HasListeners)
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
|
||||
events.Rendering.RaiseEmpty();
|
||||
|
@ -372,7 +372,7 @@ namespace StardewModdingAPI.Framework
|
|||
Game1.PushUIMode();
|
||||
this.drawOverlays(Game1.spriteBatch);
|
||||
Game1.PopUIMode();
|
||||
if (events.Rendered.HasListeners())
|
||||
if (events.Rendered.HasListeners)
|
||||
{
|
||||
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null);
|
||||
events.Rendered.RaiseEmpty();
|
||||
|
|
|
@ -235,7 +235,8 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
|
||||
// raise event
|
||||
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer));
|
||||
if (this.EventManager.PeerContextReceived.HasListeners)
|
||||
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer));
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -259,7 +260,8 @@ namespace StardewModdingAPI.Framework
|
|||
resume();
|
||||
|
||||
// raise event
|
||||
this.EventManager.PeerConnected.Raise(new PeerConnectedEventArgs(this.Peers[message.FarmerID]));
|
||||
if (this.EventManager.PeerConnected.HasListeners)
|
||||
this.EventManager.PeerConnected.Raise(new PeerConnectedEventArgs(this.Peers[message.FarmerID]));
|
||||
break;
|
||||
|
||||
// handle mod message
|
||||
|
@ -370,7 +372,9 @@ namespace StardewModdingAPI.Framework
|
|||
{
|
||||
this.Monitor.Log($"Player quit: {playerID}");
|
||||
this.Peers.Remove(playerID);
|
||||
this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer));
|
||||
|
||||
if (this.EventManager.PeerDisconnected.HasListeners)
|
||||
this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -481,7 +485,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.HostPeer = peer;
|
||||
|
||||
// raise event
|
||||
if (raiseEvent)
|
||||
if (raiseEvent && this.EventManager.PeerContextReceived.HasListeners)
|
||||
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer));
|
||||
}
|
||||
|
||||
|
|
|
@ -1017,7 +1017,9 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
foreach (Farmer player in players)
|
||||
{
|
||||
this.Reflection.GetField<Dictionary<string, Dictionary<int, List<int>>>?>(typeof(FarmerRenderer), "_recolorOffsets").GetValue()?.Remove(player.getTexture());
|
||||
var recolorOffsets = this.Reflection.GetField<Dictionary<string, Dictionary<int, List<int>>>?>(typeof(FarmerRenderer), "_recolorOffsets").GetValue();
|
||||
recolorOffsets?.Clear();
|
||||
|
||||
player.FarmerRenderer.MarkSpriteDirty();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue