public ModAsset(string path, IModMetadata metadata, AssetWithImageCachedData cachedData, ModInformation information) : base(path, metadata, cachedData, AssetType.Mod) { if (cachedData == null) throw new ArgumentNullException(nameof(cachedData)); if (information == null) throw new ArgumentNullException(nameof(information)); _metadata = metadata; _cachedData = cachedData; Information = information; }
/// <summary>Process the result from an instruction handler.</summary> /// <param name="mod">The mod being analysed.</param> /// <param name="handler">The instruction handler.</param> /// <param name="result">The result returned by the handler.</param> /// <param name="loggedMessages">The messages already logged for the current mod.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet <string> loggedMessages, string logPrefix, bool assumeCompatible, string filename) { switch (result) { case InstructionHandleResult.Rewritten: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); break; case InstructionHandleResult.NotCompatible: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); if (!assumeCompatible) { throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); } this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found broken code ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); break; case InstructionHandleResult.DetectedSaveSerialiser: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses a specialised SMAPI event that may crash the game or corrupt your save file. If you encounter problems, try removing this mod first.", LogLevel.Warn); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", #if SMAPI_FOR_WINDOWS this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug #else LogLevel.Warn #endif ); break; case InstructionHandleResult.None: break; default: throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); } }
/// <summary>Process the result from an instruction handler.</summary> /// <param name="mod">The mod being analyzed.</param> /// <param name="handler">The instruction handler.</param> /// <param name="result">The result returned by the handler.</param> /// <param name="loggedMessages">The messages already logged for the current mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet <string> loggedMessages, string logPrefix, string filename) { switch (result) { case InstructionHandleResult.Rewritten: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); break; case InstructionHandleResult.NotCompatible: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerializer: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serializer change ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.ChangesSaveSerializer); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.DetectedFilesystemAccess: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.AccessesFilesystem); break; case InstructionHandleResult.DetectedShellAccess: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected shell or process access ({handler.NounPhrase}) in assembly {filename}."); mod.SetWarning(ModWarning.AccessesShell); break; case InstructionHandleResult.None: break; default: throw new NotSupportedException($"Unrecognized instruction handler result '{result}'."); } }
/// <summary>Get all registered interceptors from a list.</summary> private IEnumerable <KeyValuePair <IModMetadata, T> > GetInterceptors <T>(IDictionary <IModMetadata, IList <T> > entries) { foreach (var entry in entries) { IModMetadata mod = entry.Key; IList <T> interceptors = entry.Value; // registered editors foreach (T interceptor in interceptors) { yield return(new KeyValuePair <IModMetadata, T>(mod, interceptor)); } } }
public ModAsset(string path, IModMetadata metadata, AssetWithImageCachedData cachedData, ModInformation information) : base(path, metadata, cachedData, AssetType.Mod) { if (cachedData == null) { throw new ArgumentNullException(nameof(cachedData)); } if (information == null) { throw new ArgumentNullException(nameof(information)); } _metadata = metadata; _cachedData = cachedData; Information = information; }
public void ReadBasicManifest_EmptyModFolder_ReturnsFailedManifest() { // arrange string rootFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); string modFolder = Path.Combine(rootFolder, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(modFolder); // act IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed."); Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); }
/// <summary>Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible.</summary> /// <typeparam name="T">The asset type.</typeparam> /// <param name="info">The basic asset metadata.</param> /// <param name="data">The loaded asset data.</param> /// <param name="mod">The mod which loaded the asset.</param> /// <returns>Returns whether the asset passed validation checks (after any fixes were applied).</returns> private bool TryFixAndValidateLoadedAsset <T>(IAssetInfo info, T data, IModMetadata mod) { // can't load a null asset if (data == null) { mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': mod incorrectly set asset to a null value.", LogLevel.Error); return(false); } // when replacing a map, the vanilla tilesheets must have the same order and IDs if (data is Map loadedMap) { TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName); foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs) { // add missing tilesheet if (loadedMap.GetTileSheet(vanillaSheet.Id) == null) { mod.Monitor.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn); this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.AssetName}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource})."); loadedMap.AddTileSheet(new TileSheet(vanillaSheet.Id, loadedMap, vanillaSheet.ImageSource, vanillaSheet.SheetSize, vanillaSheet.TileSize)); } // handle mismatch if (loadedMap.TileSheets.Count <= vanillaSheet.Index || loadedMap.TileSheets[vanillaSheet.Index].Id != vanillaSheet.Id) { // only show warning if not farm map // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); string reason = $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); if (isFarmMap) { mod.LogAsMod($"SMAPI blocked '{info.AssetName}' map load: {reason}", LogLevel.Error); return(false); } mod.LogAsMod($"SMAPI found an issue with '{info.AssetName}' map load: {reason}", LogLevel.Warn); } } } return(true); }
/// <summary>Validate that an asset loaded by a mod is valid and won't cause issues.</summary> /// <typeparam name="T">The asset type.</typeparam> /// <param name="info">The basic asset metadata.</param> /// <param name="data">The loaded asset data.</param> /// <param name="mod">The mod which loaded the asset.</param> private bool TryValidateLoadedAsset <T>(IAssetInfo info, T data, IModMetadata mod) { // can't load a null asset if (data == null) { mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': mod incorrectly set asset to a null value.", LogLevel.Error); return(false); } // when replacing a map, the vanilla tilesheets must have the same order and IDs if (data is Map loadedMap && this.Coordinator.TryLoadVanillaAsset(info.AssetName, out Map vanillaMap)) { for (int i = 0; i < vanillaMap.TileSheets.Count; i++) { // check for match TileSheet vanillaSheet = vanillaMap.TileSheets[i]; bool found = this.TryFindTilesheet(loadedMap, vanillaSheet.Id, out int loadedIndex, out TileSheet loadedSheet); if (found && loadedIndex == i) { continue; } // handle mismatch { // only show warning if not farm map // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); string reason = found ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaMap.TileSheets.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {i}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); if (isFarmMap) { mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': {reason}", LogLevel.Error); return(false); } mod.LogAsMod($"SMAPI detected a potential issue with asset replacement for '{info.AssetName}' map: {reason}", LogLevel.Warn); } } } return(true); }
/// <summary>Validate that an asset loaded by a mod is valid and won't cause issues.</summary> /// <typeparam name="T">The asset type.</typeparam> /// <param name="info">The basic asset metadata.</param> /// <param name="data">The loaded asset data.</param> /// <param name="mod">The mod which loaded the asset.</param> private bool TryValidateLoadedAsset <T>(IAssetInfo info, T data, IModMetadata mod) { // can't load a null asset if (data == null) { mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': mod incorrectly set asset to a null value.", LogLevel.Error); return(false); } // when replacing a map, the vanilla tilesheets must have the same order and IDs if (data is Map loadedMap) { TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName); foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs) { // skip if match if (loadedMap.TileSheets.Count > vanillaSheet.Index && loadedMap.TileSheets[vanillaSheet.Index].Id == vanillaSheet.Id) { continue; } // handle mismatch { // only show warning if not farm map // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id); string reason = loadedIndex != -1 ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help." : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); if (isFarmMap) { mod.LogAsMod($"SMAPI blocked '{info.AssetName}' map load: {reason}", LogLevel.Error); return(false); } mod.LogAsMod($"SMAPI found an issue with '{info.AssetName}' map load: {reason}", LogLevel.Warn); } } } return(true); }
/// <inheritdoc /> public object GetApi(string uniqueID) { // validate ready if (!this.Registry.AreAllModsInitialized) { this.Monitor.Log("Tried to access a mod-provided API before all mods were initialized.", LogLevel.Error); return(null); } // get raw API IModMetadata mod = this.Registry.Get(uniqueID); if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID)) { this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace); } return(mod?.Api); }
/// <summary>Get all registered interceptors from a list.</summary> private IEnumerable <KeyValuePair <IModMetadata, T> > GetInterceptors <T>(IDictionary <IModMetadata, IList <T> > entries) { foreach (var entry in entries) { IModMetadata metadata = entry.Key; IList <T> interceptors = entry.Value; // special case if mod is an interceptor if (metadata.Mod is T modAsInterceptor) { yield return(new KeyValuePair <IModMetadata, T>(metadata, modAsInterceptor)); } // registered editors foreach (T interceptor in interceptors) { yield return(new KeyValuePair <IModMetadata, T>(metadata, interceptor)); } } }
/// <summary>Process the result from an instruction handler.</summary> /// <param name="mod">The mod being analyzed.</param> /// <param name="handler">The instruction handler.</param> /// <param name="result">The result returned by the handler.</param> /// <param name="loggedMessages">The messages already logged for the current mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet <string> loggedMessages, string logPrefix, string filename) { // get message template // ($phrase is replaced with the noun phrase or messages) string template = null; switch (result) { case InstructionHandleResult.Rewritten: template = $"{logPrefix}Rewrote {filename} to fix $phrase..."; break; case InstructionHandleResult.NotCompatible: template = $"{logPrefix}Broken code in {filename}: $phrase."; mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: template = $"{logPrefix}Detected game patcher ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerializer: template = $"{logPrefix}Detected possible save serializer change ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.ChangesSaveSerializer); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: template = $"{logPrefix}Detected reference to $phrase in assembly {filename}."; mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: template = $"{logPrefix}Detected 'dynamic' keyword ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.DetectedConsoleAccess: template = $"{logPrefix}Detected direct console access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesConsole); break; case InstructionHandleResult.DetectedFilesystemAccess: template = $"{logPrefix}Detected filesystem access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesFilesystem); break; case InstructionHandleResult.DetectedShellAccess: template = $"{logPrefix}Detected shell or process access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesShell); break; case InstructionHandleResult.None: break; default: throw new NotSupportedException($"Unrecognized instruction handler result '{result}'."); } if (template == null) { return; } // format messages if (handler.Phrases.Any()) { foreach (string message in handler.Phrases) { this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", message)); } } else { this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); } }
/// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary> /// <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) { IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); // special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead. { Type actualType = asset.Data.GetType(); Type actualOpenType = actualType.IsGenericType ? actualType.GetGenericTypeDefinition() : null; if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary <,>) || actualOpenType == typeof(List <>) || actualType == typeof(Texture2D) || actualType == typeof(Map))) { return((IAssetData)this.GetType() .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance) .MakeGenericMethod(actualType) .Invoke(this, new object[] { info, asset })); } } // edit asset foreach (var entry in this.Editors) { // check for match IModMetadata mod = entry.Mod; IAssetEditor editor = entry.Data; try { if (!editor.CanEdit <T>(info)) { continue; } } catch (Exception ex) { mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } // try edit object prevAsset = asset.Data; try { editor.Edit <T>(asset); this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}.", LogLevel.Trace); } catch (Exception ex) { mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit if (asset.Data == null) { mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } else if (!(asset.Data is T)) { mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); asset = GetNewData(prevAsset); } } // return result return(asset); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="mod">The mod using this instance.</param> /// <param name="commandManager">Manages console commands.</param> public CommandHelper(IModMetadata mod, CommandManager commandManager) : base(mod?.Manifest?.UniqueID ?? "SMAPI") { this.Mod = mod; this.CommandManager = commandManager; }
/**** ** Assembly rewriting ****/ /// <summary>Rewrite the types referenced by an assembly.</summary> /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assembly">The assembly to rewrite.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <param name="loggedMessages">The messages that have already been logged for this mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <returns>Returns whether the assembly was modified.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet <string> loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; for (int i = 0; i < module.AssemblyReferences.Count; i++) { // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; } } if (platformChanged) { // add target assembly references foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) { module.AssemblyReferences.Add(target); } // rewrite type scopes to use target assemblies IEnumerable <TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); foreach (TypeReference type in typeReferences) { this.ChangeTypeScope(type); } } // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { // check method definition foreach (IInstructionHandler handler in handlers) { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) { anyRewritten = true; } } // check CIL instructions ILProcessor cil = method.Body.GetILProcessor(); var instructions = cil.Body.Instructions; // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers for (int offset = 0; offset < instructions.Count; offset++) { foreach (IInstructionHandler handler in handlers) { Instruction instruction = instructions[offset]; InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) { anyRewritten = true; } } } } return(platformChanged || anyRewritten); }
/// <summary>Preprocess and load an assembly.</summary> /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assemblyPath">The assembly file path.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; { 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, this.AssemblyDefinitionResolver).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.Last().Status == AssemblyLoadStatus.AlreadyLoaded) // mod assembly is last in dependency order { 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; HashSet <string> loggedMessages = new HashSet <string>(); foreach (AssemblyParseResult assembly in assemblies) { if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) { continue; } // rewrite assembly bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) { if (!reference.Name.StartsWith("System.") && !this.IsAssemblyLoaded(reference)) { this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'."); if (!assumeCompatible) { throw new IncompatibleInstructionException($"assembly reference to {reference.FullName}", $"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}."); } mod.SetWarning(ModWarning.BrokenCodeLoaded); break; } } // load assembly if (changed) { if (!oneAssembly) { this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); } using (MemoryStream outStream = new MemoryStream()) { assembly.Definition.Write(outStream); byte[] bytes = outStream.ToArray(); lastAssembly = Assembly.Load(bytes); } } else { if (!oneAssembly) { this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); } lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } // track loaded assembly for definition resolution this.AssemblyDefinitionResolver.Add(assembly.Definition); } // last assembly loaded is the root return(lastAssembly); }
/// <summary>Add an event handler.</summary> /// <param name="handler">The event handler.</param> /// <param name="mod">The mod which added the event handler.</param> public void Add(EventHandler <TEventArgs> handler, IModMetadata mod) { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast <EventHandler <TEventArgs> >()); }
/**** ** IModMetadata ****/ /// <summary>Log a message using the mod's monitor.</summary> /// <param name="metadata">The mod whose monitor to use.</param> /// <param name="message">The message to log.</param> /// <param name="level">The log severity level.</param> public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) { metadata.Monitor.Log(message, level); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="mod">The mod which uses this instance.</param> /// <param name="eventManager">The underlying event manager.</param> internal ModEventsBase(IModMetadata mod, EventManager eventManager) { this.Mod = mod; this.EventManager = eventManager; }
/********* ** Private methods *********/ /// <summary>Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies.</summary> /// <param name="mods">The full list of mods being validated.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <param name="mod">The mod whose dependencies to process.</param> /// <param name="states">The dependency state for each mod.</param> /// <param name="sortedMods">The list in which to save mods sorted by dependency order.</param> /// <param name="currentChain">The current change of mod dependencies.</param> /// <returns>Returns the mod dependency status.</returns> private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary <IModMetadata, ModDependencyStatus> states, Stack <IModMetadata> sortedMods, ICollection <IModMetadata> currentChain) { // check if already visited switch (states[mod]) { // already sorted or failed case ModDependencyStatus.Sorted: case ModDependencyStatus.Failed: return(states[mod]); // dependency loop case ModDependencyStatus.Checking: // This should never happen. The higher-level mod checks if the dependency is // already being checked, so it can fail without visiting a mod twice. If this // case is hit, that logic didn't catch the dependency loop for some reason. throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName}))."); // not visited yet, start processing case ModDependencyStatus.Queued: break; // sanity check default: throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } // collect dependencies ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); // mark sorted if no dependencies if (!dependencies.Any()) { sortedMods.Push(mod); return(states[mod] = ModDependencyStatus.Sorted); } // mark failed if missing dependencies { string[] failedModNames = ( from entry in dependencies where entry.IsRequired && entry.Mod == null let displayName = modDatabase.Get(entry.ID)?.DisplayName ?? entry.ID let modUrl = modDatabase.GetModPageUrlFor(entry.ID) orderby displayName select modUrl != null ? $"{displayName}: {modUrl}" : displayName ).ToArray(); if (failedModNames.Any()) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return(states[mod] = ModDependencyStatus.Failed); } } // dependency min version not met, mark failed { string[] failedLabels = ( from entry in dependencies where entry.Mod != null && entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" ) .ToArray(); if (failedLabels.Any()) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return(states[mod] = ModDependencyStatus.Failed); } } // process dependencies { states[mod] = ModDependencyStatus.Checking; // recursively sort dependencies foreach (var dependency in dependencies) { IModMetadata requiredMod = dependency.Mod; var subchain = new List <IModMetadata>(currentChain) { mod }; // ignore missing optional dependency if (!dependency.IsRequired && requiredMod == null) { continue; } // detect dependency loop if (states[requiredMod] == ModDependencyStatus.Checking) { sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); return(states[mod] = ModDependencyStatus.Failed); } // recursively process each dependency var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); switch (substatus) { // sorted successfully case ModDependencyStatus.Sorted: case ModDependencyStatus.Failed when !dependency.IsRequired: // ignore failed optional dependency break; // failed, which means this mod can't be loaded either case ModDependencyStatus.Failed: sortedMods.Push(mod); mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); return(states[mod] = ModDependencyStatus.Failed); // unexpected status case ModDependencyStatus.Queued: case ModDependencyStatus.Checking: throw new InvalidModStateException($"Something went wrong sorting dependencies: mod '{requiredMod.DisplayName}' unexpectedly stayed in the '{substatus}' status."); // sanity check default: throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); } } // all requirements sorted successfully sortedMods.Push(mod); return(states[mod] = ModDependencyStatus.Sorted); } }
/********* ** Public methods *********/ /// <summary>Register a mod.</summary> /// <param name="metadata">The mod metadata.</param> public void Add(IModMetadata metadata) { this.Mods.Add(metadata); }
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> /// <param name="mods">The mods to include in the update check (if eligible).</param> private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) { return; } new Thread(() => { // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); // check SMAPI version try { this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; if (response.Error != null) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log($"Error: {response.Error}"); } else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) { this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); } else { this.VerboseLog(" OK."); } } catch (Exception ex) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log($"Error: {ex.GetLogSummary()}"); } // check mod versions try { // log issues if (this.Settings.VerboseLogging) { this.VerboseLog("Validating mod update keys..."); foreach (IModMetadata mod in mods) { if (mod.Manifest == null) { this.VerboseLog($" {mod.DisplayName}: no manifest."); } else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) { this.VerboseLog($" {mod.DisplayName}: no update keys."); } } } // prepare update keys Dictionary <string, IModMetadata[]> modsByKey = ( from mod in mods where mod.Manifest?.UpdateKeys != null from key in mod.Manifest.UpdateKeys select new { key, mod } ) .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) .ToDictionary( group => group.Key, group => group.Select(p => p.mod).ToArray(), StringComparer.InvariantCultureIgnoreCase ); // fetch results this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); var results = ( from entry in client.GetModInfo(modsByKey.Keys.ToArray()) from mod in modsByKey[entry.Key] orderby mod.DisplayName select new { entry.Key, Mod = mod, Info = entry.Value } ) .ToArray(); // extract latest versions IDictionary <IModMetadata, ModInfoModel> updatesByMod = new Dictionary <IModMetadata, ModInfoModel>(); foreach (var result in results) { IModMetadata mod = result.Mod; ModInfoModel info = result.Info; // handle error if (info.Error != null) { this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); continue; } // track update ISemanticVersion localVersion = mod.DataRecord != null ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) : mod.Manifest.Version; ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) : info.Version ); bool isUpdate = latestVersion.IsNewerThan(localVersion); this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); if (isUpdate) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) { updatesByMod[mod] = info; } } } // output if (updatesByMod.Any()) { this.Monitor.Newline(); this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) { this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); } } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } }).Start(); }
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> /// <param name="mods">The mods to include in the update check (if eligible).</param> private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) { return; } new Thread(() => { // update info List <string> updates = new List <string>(); bool smapiUpdate = false; int modUpdates = 0; // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); // fetch SMAPI version try { ModInfoModel response = client.GetModInfoAsync($"GitHub:{this.Settings.GitHubProjectName}").Result.Single().Value; if (response.Error != null) { this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{response.Error}", LogLevel.Warn); } else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) { smapiUpdate = true; updates.Add($"SMAPI {response.Version}: {response.Url}"); } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); } // fetch mod versions try { // prepare update-check data IDictionary <string, IModMetadata> modsByKey = new Dictionary <string, IModMetadata>(StringComparer.InvariantCultureIgnoreCase); foreach (IModMetadata mod in mods) { if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) { modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod; } if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) { modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; } if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) { modsByKey[$"GitHub:{mod.Manifest.GitHubProject}"] = mod; } } // fetch results IDictionary <string, ModInfoModel> response = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result; IDictionary <IModMetadata, ModInfoModel> updatesByMod = new Dictionary <IModMetadata, ModInfoModel>(); foreach (var entry in response) { // handle error if (entry.Value.Error != null) { this.Monitor.Log($"Couldn't fetch version of {modsByKey[entry.Key].DisplayName} with key {entry.Key}:\n{entry.Value.Error}", LogLevel.Trace); continue; } // collect latest mod version IModMetadata mod = modsByKey[entry.Key]; ISemanticVersion version = new SemanticVersion(entry.Value.Version); if (version.IsNewerThan(mod.Manifest.Version)) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) { updatesByMod[mod] = entry.Value; modUpdates++; } } } // add to output queue if (updatesByMod.Any()) { foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) { updates.Add($"{entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}"); } } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } // output if (updates.Any()) { this.Monitor.Newline(); // print intro string intro = ""; if (smapiUpdate) { intro = "You can update SMAPI"; } if (modUpdates > 0) { intro += $"{(smapiUpdate ? " and" : "You can update")} {modUpdates} mod{(modUpdates != 1 ? "s" : "")}"; } intro += ":"; this.Monitor.Log(intro, LogLevel.Alert); // print update list foreach (string line in updates) { this.Monitor.Log($" {line}", LogLevel.Alert); } } }).Start(); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="mod">The mod which uses this instance.</param> /// <param name="eventManager">The underlying event manager.</param> public ModEvents(IModMetadata mod, EventManager eventManager) { this.GameLoop = new ModGameLoopEvents(mod, eventManager); this.Input = new ModInputEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); }
/// <summary>Track a mod's assembly for use via <see cref="GetFrom"/>.</summary> /// <param name="metadata">The mod metadata.</param> /// <param name="modAssembly">The mod assembly.</param> public void TrackAssemblies(IModMetadata metadata, Assembly modAssembly) { this.ModNamesByAssembly[modAssembly.FullName] = metadata; }
/// <summary>Track an event handler.</summary> /// <param name="mod">The mod which added the handler.</param> /// <param name="handler">The event handler.</param> /// <param name="invocationList">The updated event invocation list.</param> protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable <TEventHandler> invocationList) { this.SourceMods[handler] = mod; this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; }
/// <summary>Preprocess and load an assembly.</summary> /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assemblyPath">The assembly file path.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; { 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(); } // 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.Last().Status == AssemblyLoadStatus.AlreadyLoaded) // mod assembly is last in dependency order { 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; HashSet <string> loggedMessages = new HashSet <string>(); foreach (AssemblyParseResult assembly in assemblies) { if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) { continue; } bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); if (changed) { if (!oneAssembly) { this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); } using (MemoryStream outStream = new MemoryStream()) { assembly.Definition.Write(outStream); byte[] bytes = outStream.ToArray(); lastAssembly = Assembly.Load(bytes); } } else { if (!oneAssembly) { this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); } lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } } // last assembly loaded is the root return(lastAssembly); }
public IMod CreateOrGet(IModMetadata metadata) { return(metadata == null ? null : _mods.Where(m => m.Metadata == metadata).Select(m => m.Value).SingleOrDefault()); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="mod">The mod which uses this instance.</param> /// <param name="eventManager">The underlying event manager.</param> internal ModWorldEvents(IModMetadata mod, EventManager eventManager) : base(mod, eventManager) { }
/// <summary>Load and hook up the given mods.</summary> /// <param name="mods">The mods to load.</param> /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param> /// <param name="contentManager">The content manager to use for mod content.</param> private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) { this.Monitor.Log("Loading mods...", LogLevel.Trace); // load mod assemblies IDictionary <IModMetadata, string> skippedMods = new Dictionary <IModMetadata, string>(); { void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); foreach (IModMetadata metadata in mods) { // get basic info IManifest manifest = metadata.Manifest; string assemblyPath = metadata.Manifest?.EntryDll != null ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll) : null; this.Monitor.Log(assemblyPath != null ? $"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}..." : $"Loading {metadata.DisplayName}...", LogLevel.Trace); // validate status if (metadata.Status == ModMetadataStatus.Failed) { this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace); TrackSkip(metadata, metadata.Error); continue; } // preprocess & load mod assembly Assembly modAssembly; try { modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); 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()}"); continue; } // validate assembly try { int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); if (modEntries == 0) { TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass."); continue; } if (modEntries > 1) { TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses."); continue; } } catch (Exception ex) { TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}"); continue; } // initialise mod try { // get implementation TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { TrackSkip(metadata, "its entry class couldn't be instantiated."); continue; } // inject data { IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); mod.Monitor = monitor; } // track mod metadata.SetMod(mod); this.ModRegistry.Add(metadata); } catch (Exception ex) { TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}"); } } } IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods this.Monitor.Newline(); if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) { IModMetadata mod = pair.Key; string reason = pair.Value; if (mod.Manifest?.Version != null) { this.Monitor.Log($" {mod.DisplayName} {mod.Manifest.Version} because {reason}", LogLevel.Error); } else { this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); } } this.Monitor.Newline(); } // log loaded mods this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) { IManifest manifest = metadata.Manifest; this.Monitor.Log( $" {metadata.DisplayName} {manifest.Version}" + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), LogLevel.Info ); } this.Monitor.Newline(); // initialise translations this.ReloadTranslations(); // initialise loaded mods foreach (IModMetadata metadata in loadedMods) { // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) { this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; } // call entry method try { IMod mod = metadata.Mod; mod.Entry(mod.Helper); } catch (Exception ex) { this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); } } // invalidate cache entries when needed // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) { helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(e.NewItems.Cast <IAssetEditor>().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast <IAssetLoader>().ToArray()); } }; } } // reset cache now if any editors or loaders were added during entry IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); if (editors.Any() || loaders.Any()) { this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); this.ContentManager.InvalidateCacheFor(editors, loaders); } }
/**** ** Assembly rewriting ****/ /// <summary>Rewrite the types referenced by an assembly.</summary> /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assembly">The assembly to rewrite.</param> /// <param name="loggedMessages">The messages that have already been logged for this mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> /// <returns>Returns whether the assembly was modified.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet <string> loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; for (int i = 0; i < module.AssemblyReferences.Count; i++) { // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; } } if (platformChanged) { // add target assembly references foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) { module.AssemblyReferences.Add(target); } // rewrite type scopes to use target assemblies IEnumerable <TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); foreach (TypeReference type in typeReferences) { this.ChangeTypeScope(type); } // rewrite types using custom attributes foreach (TypeDefinition type in module.GetTypes()) { foreach (var attr in type.CustomAttributes) { foreach (var conField in attr.ConstructorArguments) { if (conField.Value is TypeReference typeRef) { this.ChangeTypeScope(typeRef); } } } } } // find or rewrite code IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged, this.RewriteMods).ToArray(); RecursiveRewriter rewriter = new RecursiveRewriter( module: module, rewriteType: (type, replaceWith) => { bool rewritten = false; foreach (IInstructionHandler handler in handlers) { rewritten |= handler.Handle(module, type, replaceWith); } return(rewritten); }, rewriteInstruction: (ref Instruction instruction, ILProcessor cil) => { bool rewritten = false; foreach (IInstructionHandler handler in handlers) { rewritten |= handler.Handle(module, cil, instruction); } return(rewritten); } ); bool anyRewritten = rewriter.RewriteModule(); // handle rewrite flags foreach (IInstructionHandler handler in handlers) { foreach (var flag in handler.Flags) { this.ProcessInstructionHandleResult(mod, handler, flag, loggedMessages, logPrefix, filename); } } return(platformChanged || anyRewritten); }