diff options
Diffstat (limited to 'src/StardewModdingAPI')
29 files changed, 1185 insertions, 406 deletions
diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 3bd31c2d..7a790f1a 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -9,6 +9,7 @@ using StardewModdingAPI.AssemblyRewriters.Finders; using StardewModdingAPI.AssemblyRewriters.Rewriters; using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework; using StardewValley; namespace StardewModdingAPI @@ -35,7 +36,7 @@ namespace StardewModdingAPI /// <summary>SMAPI's current semantic version.</summary> public static ISemanticVersion ApiVersion { get; } = #if SMAPI_1_x - new SemanticVersion(1, 15, 1); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} + new SemanticVersion(1, 15, 2); #else new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); #endif @@ -86,7 +87,7 @@ namespace StardewModdingAPI internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); /// <summary>The game's current semantic version.</summary> - internal static ISemanticVersion GameVersion { get; } = Constants.GetGameVersion(); + internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// <summary>The target game platform.</summary> internal static Platform TargetPlatform { get; } = @@ -98,7 +99,7 @@ namespace StardewModdingAPI /********* - ** Protected methods + ** Internal methods *********/ /// <summary>Get metadata for mapping assemblies to the current platform.</summary> /// <param name="targetPlatform">The target game platform.</param> @@ -179,15 +180,6 @@ namespace StardewModdingAPI new TypeFinder("StardewModdingAPI.Command"), new TypeFinder("StardewModdingAPI.Config"), new TypeFinder("StardewModdingAPI.Log"), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), @@ -198,6 +190,15 @@ namespace StardewModdingAPI new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), #endif /**** @@ -219,19 +220,10 @@ namespace StardewModdingAPI }; } - /// <summary>Get game current version as it should be displayed to players.</summary> - /// <param name="version">The semantic game version.</param> - internal static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) - { - switch (version.ToString()) - { - case "1.1.1": - return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 - default: - return version; - } - } + /********* + ** Private methods + *********/ /// <summary>Get the name of a save directory for the current player.</summary> private static string GetSaveFolderName() { @@ -239,20 +231,14 @@ namespace StardewModdingAPI return $"{prefix}_{Game1.uniqueIDForThisGame}"; } - /// <summary>Get the game's current semantic version.</summary> - private static ISemanticVersion GetGameVersion() + /// <summary>Get the game's current version string.</summary> + private static string GetGameVersion() { - // get raw version // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); if (field == null) throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); - string version = (string)field.GetValue(null); - - // get semantic version - if (version == "1.11") - version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks - return new SemanticVersion(version); + return (string)field.GetValue(null); } } } diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 6c5ae40e..119e14c8 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -16,8 +16,11 @@ namespace StardewModdingAPI /// <summary>Whether the player has loaded a save and the world has finished initialising.</summary> public static bool IsWorldReady { get; internal set; } - /// <summary>Whether the player is free to move around (e.g. save is loaded, no menu is displayed, no cutscene is in progress, etc).</summary> - public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.player.CanMove && !Game1.dialogueUp && !Game1.eventUp; + /// <summary>Whether <see cref="IsWorldReady"/> is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc).</summary> + public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && !Game1.dialogueUp && !Game1.eventUp; + + /// <summary>Whether <see cref="IsPlayerFree"/> is true and the player is free to move (e.g. not using a tool).</summary> + public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; /// <summary>Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use <see cref="GraphicsEvents.OnPostRenderEvent"/> to draw to the screen.</summary> public static bool IsInDrawLoop { get; internal set; } diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/StardewModdingAPI/Events/GraphicsEvents.cs index 25b976f1..fff51bed 100644 --- a/src/StardewModdingAPI/Events/GraphicsEvents.cs +++ b/src/StardewModdingAPI/Events/GraphicsEvents.cs @@ -51,11 +51,9 @@ namespace StardewModdingAPI.Events ****/ /// <summary>Raise a <see cref="Resize"/> event.</summary> /// <param name="monitor">Encapsulates monitoring and logging.</param> - /// <param name="sender">The object which raised the event.</param> - /// <param name="e">The event arguments.</param> - internal static void InvokeResize(IMonitor monitor, object sender, EventArgs e) + internal static void InvokeResize(IMonitor monitor) { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList(), sender, e); + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); } /**** diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs new file mode 100644 index 00000000..ec9279f1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// <summary>An exception thrown when an assembly can't be loaded by SMAPI, with all the relevant details in the message.</summary> + internal class SAssemblyLoadFailedException : Exception + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="message">The error message.</param> + public SAssemblyLoadFailedException(string message) + : base(message) { } + } +} diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/StardewModdingAPI/Framework/GameVersion.cs new file mode 100644 index 00000000..48159f61 --- /dev/null +++ b/src/StardewModdingAPI/Framework/GameVersion.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework +{ + /// <summary>An implementation of <see cref="ISemanticVersion"/> that correctly handles the non-semantic versions used by older Stardew Valley releases.</summary> + internal class GameVersion : SemanticVersion + { + /********* + ** Private methods + *********/ + /// <summary>A mapping of game to semantic versions.</summary> + private static readonly IDictionary<string, string> VersionMap = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase) + { + ["1.01"] = "1.0.1", + ["1.02"] = "1.0.2", + ["1.03"] = "1.0.3", + ["1.04"] = "1.0.4", + ["1.05"] = "1.0.5", + ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. + ["1.051b"] = "1.0.6-prelease2", + ["1.06"] = "1.0.6", + ["1.07"] = "1.0.7", + ["1.07a"] = "1.0.8-prerelease1", + ["1.11"] = "1.1.1" + }; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="version">The game version string.</param> + public GameVersion(string version) + : base(GameVersion.GetSemanticVersionString(version)) { } + + /// <summary>Get a string representation of the version.</summary> + public override string ToString() + { + return GameVersion.GetGameVersionString(base.ToString()); + } + + + /********* + ** Private methods + *********/ + /// <summary>Convert a game version string to a semantic version string.</summary> + /// <param name="gameVersion">The game version string.</param> + private static string GetSemanticVersionString(string gameVersion) + { + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; + } + + /// <summary>Convert a game version string to a semantic version string.</summary> + /// <param name="gameVersion">The game version string.</param> + private static string GetGameVersionString(string gameVersion) + { + foreach (var mapping in GameVersion.VersionMap) + { + if (mapping.Value.Equals(gameVersion, StringComparison.InvariantCultureIgnoreCase)) + return mapping.Key; + } + return gameVersion; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 5f72176e..ffa78ff6 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -33,10 +33,19 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>The friendly mod name for use in errors.</summary> private readonly string ModName; + /// <summary>Encapsulates monitoring and logging for a given module.</summary> + private readonly IMonitor Monitor; + /********* ** Accessors *********/ + /// <summary>The game's current locale code (like <c>pt-BR</c>).</summary> + public string CurrentLocale => this.ContentManager.GetLocale(); + + /// <summary>The game's current locale as an enum value.</summary> + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + /// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary> internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>(); @@ -44,10 +53,10 @@ namespace StardewModdingAPI.Framework.ModHelpers internal ObservableCollection<IAssetLoader> ObservableAssetLoaders { get; } = new ObservableCollection<IAssetLoader>(); /// <summary>Interceptors which provide the initial versions of matching content assets.</summary> - internal IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders; + public IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders; /// <summary>Interceptors which edit matching content assets after they're loaded.</summary> - internal IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors; + public IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors; /********* @@ -58,13 +67,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="modFolderPath">The absolute path to the mod folder.</param> /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The friendly mod name for use in errors.</param> - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) + /// <param name="monitor">Encapsulates monitoring and logging.</param> + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.Monitor = monitor; } /// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> @@ -176,6 +187,25 @@ namespace StardewModdingAPI.Framework.ModHelpers } } + /// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary> + /// <param name="key">The asset key to invalidate in the content folder.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + /// <returns>Returns whether the given asset key was cached.</returns> + public bool InvalidateCache(string key) + { + this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); + return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); + } + + /// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary> + /// <typeparam name="T">The asset type to remove from the cache.</typeparam> + /// <returns>Returns whether any assets were invalidated.</returns> + public bool InvalidateCache<T>() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); + return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + } /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs index 9411a97a..14a339da 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -13,16 +13,21 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>The underlying reflection helper.</summary> private readonly Reflector Reflector; + /// <summary>The mod name for error messages.</summary> + private readonly string ModName; + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="modName">The mod name for error messages.</param> /// <param name="reflector">The underlying reflection helper.</param> - public ReflectionHelper(string modID, Reflector reflector) + public ReflectionHelper(string modID, string modName, Reflector reflector) : base(modID) { + this.ModName = modName; this.Reflector = reflector; } @@ -37,6 +42,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns> public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateField<TValue>(obj, name, required); } @@ -47,6 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateField<TValue>(type, name, required); } @@ -60,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private property is not found.</param> public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateProperty<TValue>(obj, name, required); } @@ -70,6 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private property is not found.</param> public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateProperty<TValue>(type, name, required); } @@ -89,6 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// </remarks> public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required); return field != null ? field.GetValue() @@ -107,6 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// </remarks> public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required); return field != null ? field.GetValue() @@ -122,6 +133,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, required); } @@ -131,6 +143,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, required); } @@ -144,6 +157,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); } @@ -154,7 +168,35 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="required">Whether to throw an exception if the private field is not found.</param> public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); } + + + /********* + ** Private methods + *********/ + /// <summary>Assert that mods can use the reflection helper to access the given type.</summary> + /// <param name="type">The type being accessed.</param> + private void AssertAccessAllowed(Type type) + { +#if !SMAPI_1_x + // validate type namespace + if (type.Namespace != null) + { + string rootSmapiNamespace = typeof(Program).Namespace; + if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) + throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); + } +#endif + } + + /// <summary>Assert that mods can use the reflection helper to access the given type.</summary> + /// <param name="obj">The object being accessed.</param> + private void AssertAccessAllowed(object obj) + { + if (obj != null) + this.AssertAccessAllowed(obj.GetType()); + } } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs new file mode 100644 index 00000000..11be19fc --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// <summary>Indicates the result of an assembly load.</summary> + internal enum AssemblyLoadStatus + { + /// <summary>The assembly was loaded successfully.</summary> + Okay = 1, + + /// <summary>The assembly could not be loaded.</summary> + Failed = 2, + + /// <summary>The assembly is already loaded.</summary> + AlreadyLoaded = 3 + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 406d49e1..b14ae56f 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,6 +6,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.Framework.Exceptions; namespace StardewModdingAPI.Framework.ModLoading { @@ -65,16 +66,27 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); HashSet<string> visitedAssemblyNames = new HashSet<string>(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); - if (!assemblies.Any()) - throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); - resolver.Add(assemblies.Select(p => p.Definition).ToArray()); } + // validate load + if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) + { + throw new SAssemblyLoadFailedException(!File.Exists(assemblyPath) + ? $"Could not load '{assemblyPath}' because it doesn't exist." + : $"Could not load '{assemblyPath}'." + ); + } + if (assemblies[0].Status == AssemblyLoadStatus.AlreadyLoaded) + throw new SAssemblyLoadFailedException($"Could not load '{assemblyPath}' because it was already loaded. Do you have two copies of this mod?"); + // rewrite & load assemblies in leaf-to-root order bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; foreach (AssemblyParseResult assembly in assemblies) { + if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) + continue; + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); if (changed) { @@ -143,7 +155,7 @@ namespace StardewModdingAPI.Framework.ModLoading // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) - yield break; + yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); visitedAssemblyNames.Add(assembly.Name.Name); // yield referenced assemblies @@ -155,7 +167,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // yield assembly - yield return new AssemblyParseResult(file, assembly); + yield return new AssemblyParseResult(file, assembly, AssemblyLoadStatus.Okay); } /**** diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs index 69c99afe..b56a776c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>The assembly definition.</summary> public readonly AssemblyDefinition Definition; + /// <summary>The result of the assembly load.</summary> + public AssemblyLoadStatus Status; + /********* |
