show friendlier error when players have two copies of a mod

This commit is contained in:
Jesse Plamondon-Willard 2017-08-21 14:22:19 -04:00
parent 723ddc255e
commit 80fe706f19
7 changed files with 65 additions and 8 deletions

View File

@ -6,6 +6,7 @@ For players:
* The SMAPI console is now much simpler and easier to read.
* The SMAPI console now adjusts its colors when you have a light terminal background.
* Updated compatibility list.
* Improved errors when a mod DLL can't be loaded.
For mod developers:
* Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time.

View File

@ -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) { }
}
}

View File

@ -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
}
}

View File

@ -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);
}
/****

View File

@ -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;
/*********
** Public methods
@ -22,10 +25,12 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Construct an instance.</summary>
/// <param name="file">The original assembly file.</param>
/// <param name="assembly">The assembly definition.</param>
public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly)
/// <param name="status">The result of the assembly load.</param>
public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status)
{
this.File = file;
this.Definition = assembly;
this.Status = status;
}
}
}
}

View File

@ -15,6 +15,7 @@ using Newtonsoft.Json;
using StardewModdingAPI.AssemblyRewriters;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Logging;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModHelpers;
@ -655,6 +656,11 @@ namespace StardewModdingAPI
#endif
continue;
}
catch (SAssemblyLoadFailedException ex)
{
TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}");
continue;
}
catch (Exception ex)
{
TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}");

View File

@ -91,6 +91,8 @@
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Command.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
<Compile Include="Framework\Utilities\ContextHash.cs" />
<Compile Include="Metadata\CoreAssets.cs" />
<Compile Include="ContentSource.cs" />