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;
 }
Ejemplo n.º 2
0
        /// <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}'.");
            }
        }
Ejemplo n.º 3
0
        /// <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}'.");
            }
        }
Ejemplo n.º 4
0
        /// <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));
                }
            }
        }
Ejemplo n.º 5
0
 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;
 }
Ejemplo n.º 6
0
        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.");
        }
Ejemplo n.º 7
0
        /// <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);
        }
Ejemplo n.º 8
0
        /// <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);
        }
Ejemplo n.º 9
0
        /// <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);
        }
Ejemplo n.º 10
0
        /// <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);
        }
Ejemplo n.º 11
0
        /// <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));
                }
            }
        }
Ejemplo n.º 12
0
        /// <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));
            }
        }
Ejemplo n.º 13
0
        /// <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);
        }
Ejemplo n.º 14
0
 /*********
 ** 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;
 }
Ejemplo n.º 15
0
        /****
        ** 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);
        }
Ejemplo n.º 16
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;
            {
                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);
        }
Ejemplo n.º 17
0
 /// <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> >());
 }
Ejemplo n.º 18
0
 /****
 ** 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);
 }
Ejemplo n.º 19
0
 /*********
 ** 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;
 }
Ejemplo n.º 20
0
        /*********
        ** 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);
            }
        }
Ejemplo n.º 21
0
 /*********
 ** Public methods
 *********/
 /// <summary>Register a mod.</summary>
 /// <param name="metadata">The mod metadata.</param>
 public void Add(IModMetadata metadata)
 {
     this.Mods.Add(metadata);
 }
Ejemplo n.º 22
0
        /// <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();
        }
Ejemplo n.º 23
0
        /// <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();
        }
Ejemplo n.º 24
0
 /*********
 ** 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);
 }
Ejemplo n.º 25
0
 /// <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;
 }
Ejemplo n.º 26
0
 /// <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];
 }
Ejemplo n.º 27
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);
        }
Ejemplo n.º 28
0
 public IMod CreateOrGet(IModMetadata metadata)
 {
     return(metadata == null
         ? null
         : _mods.Where(m => m.Metadata == metadata).Select(m => m.Value).SingleOrDefault());
 }
Ejemplo n.º 29
0
 /*********
 ** 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)
 {
 }
Ejemplo n.º 30
0
        /// <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);
            }
        }
Ejemplo n.º 31
0
        /****
        ** 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);
        }