Esempio n. 1
0
 /*********
 ** Public methods
 *********/
 /// <summary>Construct an instance.</summary>
 /// <param name="displayName">The mod's display name.</param>
 /// <param name="directoryPath">The mod's full directory path.</param>
 /// <param name="manifest">The mod manifest.</param>
 /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
 public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility)
 {
     this.DisplayName   = displayName;
     this.DirectoryPath = directoryPath;
     this.Manifest      = manifest;
     this.Compatibility = compatibility;
 }
Esempio n. 2
0
 /// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
 /// <param name="mod">The mock mod metadata.</param>
 /// <param name="compatibility">The compatibility record to set.</param>
 private void SetupMetadataForValidation(Mock <IModMetadata> mod, ModCompatibility compatibility = null)
 {
     mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
     mod.Setup(p => p.Compatibility).Returns(() => null);
     mod.Setup(p => p.Manifest).Returns(this.GetManifest());
     mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath());
     mod.Setup(p => p.Compatibility).Returns(compatibility);
 }
Esempio n. 3
0
        private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
        {
            var spaceCore = this.Helper.ModRegistry.GetApi <ISpaceCoreApi>("spacechase0.SpaceCore");

            Type[] types =
            {
                typeof(BuildableGreenhouseBuilding),
                typeof(BuildableGreenhouseLocation)
            };

            foreach (Type type in types)
            {
                spaceCore.RegisterSerializerType(type);
            }

            ModCompatibility.applyGMCMCompatibility(sender, e);
        }
Esempio n. 4
0
        public override void OnApplicationStart() // Runs after Game Initialization.
        {
            if (Environment.CommandLine.Contains("--ff.debug") || MelonDebug.IsEnabled())
            {
                isDebug = true;
                MelonLogger.Msg("Debug mode is active");
            }

            melon               = MelonPreferences.CreateCategory(BuildInfo.Name, BuildInfo.Name);
            allowFrameLimit     = (MelonPreferences_Entry <bool>)melon.CreateEntry("allowFrameLimit", false, "Toggle Frame Focus");
            FrameLimit          = (MelonPreferences_Entry <int>)melon.CreateEntry("FrameLimit", 90, "Max Focused Frame Limit");
            FrameLimitUnfocused = (MelonPreferences_Entry <int>)melon.CreateEntry("FrameLimitUnfocused", 5, "Unfocused Frame Limit"); // suggested by ljoonal
            override_emmVRC     = (MelonPreferences_Entry <bool>)melon.CreateEntry("override_emmVRC", false, "Make FrameFocus ignore emmVRC integration (only works if emmVRC is detected)");

            MelonCoroutines.Start(ModCompatibility.RunCompatibilityCheck());
            MelonCoroutines.Start(StartLate.Init());
            MelonLogger.Msg("Initialized!");
        }
Esempio n. 5
0
        public HitokoriRuleset()
        {
            void RegisterMods(IEnumerable <Mod> mods)
            {
                foreach (var mod in mods)
                {
                    if (mod is MultiMod multi)
                    {
                        RegisterMods(multi.Mods);
                    }
                    else
                    {
                        ModCompatibility.RegisterMod(GetType(), mod.GetType());
                    }
                }
            }

            foreach (ModType type in Enum.GetValues(typeof(ModType)))
            {
                RegisterMods(GetModsFor(type));
            }
        }
Esempio n. 6
0
        public override void Entry(IModHelper helper)
        {
            this.Config         = helper.ReadConfig <ModConfig>();
            this.graphicsDevice = Game1.graphics.GraphicsDevice;

            ModPatch.Initialize(helper, this.Monitor);
            ModCompatibility.Initialize(helper, this.Monitor, this.ModManifest);

            helper.Events.Content.AssetRequested += this.OnAssetRequested;
            helper.Events.Player.Warped          += this.OnWarped;
            helper.Events.GameLoop.GameLaunched  += this.OnGameLaunched;
            helper.Events.GameLoop.DayStarted    += this.OnDayStarted;
            helper.Events.GameLoop.SaveLoaded    += this.OnSaveLoaded;
            helper.Events.Display.MenuChanged    += this.OnMenuChanged;

            var harmony = new Harmony(this.ModManifest.UniqueID);

            harmony.Patch(
                original: AccessTools.Method(typeof(GreenhouseBuilding), nameof(GreenhouseBuilding.drawInMenu)),
                prefix: new HarmonyMethod(typeof(ModPatch), nameof(ModPatch.drawInMenu_Prefix))
                );
        }
Esempio n. 7
0
        /// <summary>Validate manifest metadata.</summary>
        /// <param name="mods">The mod manifests to validate.</param>
        /// <param name="apiVersion">The current SMAPI version.</param>
        /// <param name="vendorModUrls">Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID).</param>
        public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion, IDictionary <string, string> vendorModUrls)
        {
            mods = mods.ToArray();

            // validate each manifest
            foreach (IModMetadata mod in mods)
            {
                // skip if already failed
                if (mod.Status == ModMetadataStatus.Failed)
                {
                    continue;
                }

                // validate compatibility
                ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version);
                switch (compatibility?.Status)
                {
                case ModStatus.Obsolete:
                    mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}");
                    continue;

                case ModStatus.AssumeBroken:
                {
                    // get reason
                    string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible";

                    // get update URLs
                    List <string> updateUrls = new List <string>();
                    foreach (string key in mod.Manifest.UpdateKeys ?? new string[0])
                    {
                        string[] parts = key.Split(new[] { ':' }, 2);
                        if (parts.Length != 2)
                        {
                            continue;
                        }

                        string vendorKey = parts[0].Trim();
                        string modID     = parts[1].Trim();

                        if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate))
                        {
                            updateUrls.Add(string.Format(urlTemplate, modID));
                        }
                    }
                    if (mod.DataRecord.AlternativeUrl != null)
                    {
                        updateUrls.Add(mod.DataRecord.AlternativeUrl);
                    }

                    // build error
                    string error = $"{reasonPhrase}. Please check for a ";
                    if (mod.Manifest.Version.Equals(compatibility.UpperVersion))
                    {
                        error += "newer version";
                    }
                    else
                    {
                        error += $"version newer than {compatibility.UpperVersion}";
                    }
                    error += " at " + string.Join(" or ", updateUrls);

                    mod.SetStatus(ModMetadataStatus.Failed, error);
                }
                    continue;
                }

                // validate SMAPI version
                if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true)
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod.");
                    continue;
                }

                // validate DLL value
                if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll))
                {
                    mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field.");
                    continue;
                }
                if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any())
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field.");
                    continue;
                }

                // validate DLL path
                string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
                if (!File.Exists(assemblyPath))
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
                    continue;
                }

                // validate required fields
                {
                    List <string> missingFields = new List <string>(3);

                    if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
                    {
                        missingFields.Add(nameof(IManifest.Name));
                    }
                    if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0")
                    {
                        missingFields.Add(nameof(IManifest.Version));
                    }
                    if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
                    {
                        missingFields.Add(nameof(IManifest.UniqueID));
                    }

                    if (missingFields.Any())
                    {
                        mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
                    }
                }
            }

            // validate IDs are unique
            {
                var duplicatesByID = mods
                                     .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase)
                                     .Where(p => p.Count() > 1);
                foreach (var group in duplicatesByID)
                {
                    foreach (IModMetadata mod in group)
                    {
                        if (mod.Status == ModMetadataStatus.Failed)
                        {
                            continue; // don't replace metadata error
                        }
                        mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))}).");
                    }
                }
            }
        }
Esempio n. 8
0
        /// <summary>Validate manifest metadata.</summary>
        /// <param name="mods">The mod manifests to validate.</param>
        /// <param name="apiVersion">The current SMAPI version.</param>
        public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion)
        {
            mods = mods.ToArray();

            // validate each manifest
            foreach (IModMetadata mod in mods)
            {
                // skip if already failed
                if (mod.Status == ModMetadataStatus.Failed)
                {
                    continue;
                }

                // validate compatibility
                {
                    ModCompatibility compatibility = mod.Compatibility;
                    if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
                    {
#if SMAPI_1_x
                        bool hasOfficialUrl   = mod.Compatibility.UpdateUrls.Length > 0;
                        bool hasUnofficialUrl = mod.Compatibility.UpdateUrls.Length > 1;

                        string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI";
                        string error        = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:";
                        if (hasOfficialUrl)
                        {
                            error += !hasUnofficialUrl ? $" {compatibility.UpdateUrls[0]}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrls[0]}";
                        }
                        if (hasUnofficialUrl)
                        {
                            error += $"{Environment.NewLine}- unofficial update: {compatibility.UpdateUrls[1]}";
                        }
#else
                        string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible";
                        string error        = $"{reasonPhrase}. Please check for a ";
                        if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null)
                        {
                            error += "newer version";
                        }
                        else
                        {
                            error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}";
                        }
                        error += " at " + string.Join(" or ", compatibility.UpdateUrls);
#endif

                        mod.SetStatus(ModMetadataStatus.Failed, error);
                        continue;
                    }
                }

                // validate SMAPI version
                if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true)
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod.");
                    continue;
                }

                // validate DLL path
                string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
                if (!File.Exists(assemblyPath))
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
                    continue;
                }

                // validate required fields
#if !SMAPI_1_x
                {
                    List <string> missingFields = new List <string>(3);

                    if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
                    {
                        missingFields.Add(nameof(IManifest.Name));
                    }
                    if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0")
                    {
                        missingFields.Add(nameof(IManifest.Version));
                    }
                    if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
                    {
                        missingFields.Add(nameof(IManifest.UniqueID));
                    }

                    if (missingFields.Any())
                    {
                        mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
                    }
                }
#endif
            }

            // validate IDs are unique
#if !SMAPI_1_x
            {
                var duplicatesByID = mods
                                     .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase)
                                     .Where(p => p.Count() > 1);
                foreach (var group in duplicatesByID)
                {
                    foreach (IModMetadata mod in group)
                    {
                        if (mod.Status == ModMetadataStatus.Failed)
                        {
                            continue; // don't replace metadata error
                        }
                        mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))}).");
                    }
                }
            }
#endif
        }
Esempio n. 9
0
        /*********
        ** Public methods
        *********/
        /// <summary>Get manifest metadata for each folder in the given root path.</summary>
        /// <param name="rootPath">The root path to search for mods.</param>
        /// <param name="jsonHelper">The JSON helper with which to read manifests.</param>
        /// <param name="compatibilityRecords">Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
        /// <param name="disabledMods">Metadata about mods that SMAPI should consider obsolete and not load.</param>
        /// <returns>Returns the manifests by relative folder.</returns>
        public IEnumerable <IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable <ModCompatibility> compatibilityRecords, IEnumerable <DisabledMod> disabledMods)
        {
            compatibilityRecords = compatibilityRecords.ToArray();
            disabledMods         = disabledMods.ToArray();

            foreach (DirectoryInfo modDir in this.GetModFolders(rootPath))
            {
                // read file
                Manifest manifest = null;
                string   path     = Path.Combine(modDir.FullName, "manifest.json");
                string   error    = null;
                try
                {
                    // read manifest
                    manifest = jsonHelper.ReadJsonFile <Manifest>(path);

                    // validate
                    if (manifest == null)
                    {
                        error = File.Exists(path)
                            ? "its manifest is invalid."
                            : "it doesn't have a manifest.";
                    }
                    else if (string.IsNullOrWhiteSpace(manifest.EntryDll))
                    {
                        error = "its manifest doesn't set an entry DLL.";
                    }
                }
                catch (SParseException ex)
                {
                    error = $"parsing its manifest failed: {ex.Message}";
                }
                catch (Exception ex)
                {
                    error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
                }

                // validate metadata
                ModCompatibility compatibility = null;
                if (manifest != null)
                {
                    // get unique key for lookups
                    string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll;

                    // check if mod should be disabled
                    DisabledMod disabledMod = disabledMods.FirstOrDefault(mod => mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase));
                    if (disabledMod != null)
                    {
                        error = $"it's obsolete: {disabledMod.ReasonPhrase}";
                    }

                    // get compatibility record
                    compatibility = (
                        from mod in compatibilityRecords
                        where
                        mod.ID.Any(p => p.Matches(key, manifest)) &&
                        (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) &&
                        !manifest.Version.IsNewerThan(mod.UpperVersion)
                        select mod
                        ).FirstOrDefault();
                }

                // build metadata
                string displayName = !string.IsNullOrWhiteSpace(manifest?.Name)
                    ? manifest.Name
                    : modDir.FullName.Replace(rootPath, "").Trim('/', '\\');
                ModMetadataStatus status = error == null
                    ? ModMetadataStatus.Found
                    : ModMetadataStatus.Failed;

                yield return(new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error));
            }
        }
Esempio n. 10
0
 public override void VRChat_OnUiManagerInit()
 {
     MelonCoroutines.Start(ModCompatibility.RunCompatibilityCheck());
     MelonCoroutines.Start(StartLate.Init());
 }
Esempio n. 11
0
 private void OnDayStarted(object sender, DayStartedEventArgs e)
 {
     ModCompatibility.applyGreenhouseUpgradesCompatibility();
 }
Esempio n. 12
0
        /// <summary>Validate manifest metadata.</summary>
        /// <param name="mods">The mod manifests to validate.</param>
        /// <param name="apiVersion">The current SMAPI version.</param>
        public void ValidateManifests(IEnumerable <IModMetadata> mods, ISemanticVersion apiVersion)
        {
            mods = mods.ToArray();

            // validate each manifest
            foreach (IModMetadata mod in mods)
            {
                // skip if already failed
                if (mod.Status == ModMetadataStatus.Failed)
                {
                    continue;
                }

                // validate compatibility
                ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version);
                switch (compatibility?.Status)
                {
                case ModStatus.Obsolete:
                    mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}");
                    continue;

                case ModStatus.AssumeBroken:
                {
                    string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible";
                    string error        = $"{reasonPhrase}. Please check for a ";
                    if (mod.Manifest.Version.Equals(compatibility.UpperVersion))
                    {
                        error += "newer version";
                    }
                    else
                    {
                        error += $"version newer than {compatibility.UpperVersion}";
                    }
                    error += " at " + string.Join(" or ", mod.DataRecord.UpdateUrls);

                    mod.SetStatus(ModMetadataStatus.Failed, error);
                }
                    continue;
                }

                // validate SMAPI version
                if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true)
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod.");
                    continue;
                }

                // validate DLL path
                string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
                if (!File.Exists(assemblyPath))
                {
                    mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
                    continue;
                }

                // validate required fields
                {
                    List <string> missingFields = new List <string>(3);

                    if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
                    {
                        missingFields.Add(nameof(IManifest.Name));
                    }
                    if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0")
                    {
                        missingFields.Add(nameof(IManifest.Version));
                    }
                    if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
                    {
                        missingFields.Add(nameof(IManifest.UniqueID));
                    }

                    if (missingFields.Any())
                    {
                        mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
                    }
                }
            }

            // validate IDs are unique
            {
                var duplicatesByID = mods
                                     .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase)
                                     .Where(p => p.Count() > 1);
                foreach (var group in duplicatesByID)
                {
                    foreach (IModMetadata mod in group)
                    {
                        if (mod.Status == ModMetadataStatus.Failed)
                        {
                            continue; // don't replace metadata error
                        }
                        mod.SetStatus(ModMetadataStatus.Failed, $"its unique ID '{mod.Manifest.UniqueID}' is used by multiple mods ({string.Join(", ", group.Select(p => p.DisplayName))}).");
                    }
                }
            }
        }
Esempio n. 13
0
        /// <summary>Load and hook up all mods in the mod directory.</summary>
        private void LoadMods()
        {
            this.Monitor.Log("Loading mods...");

            // get JSON helper
            JsonHelper jsonHelper = new JsonHelper();

            // get assembly loader
            AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor);

            AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);

            // load mod assemblies
            int           modsLoaded          = 0;
            List <Action> deprecationWarnings = new List <Action>(); // queue up deprecation warnings to show after mod list

            foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath))
            {
                // passthrough empty directories
                DirectoryInfo directory = new DirectoryInfo(directoryPath);
                while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
                {
                    directory = directory.GetDirectories().First();
                }

                // check for cancellation
                if (this.CancellationTokenSource.IsCancellationRequested)
                {
                    this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
                    return;
                }

                // get manifest path
                string manifestPath = Path.Combine(directory.FullName, "manifest.json");
                if (!File.Exists(manifestPath))
                {
                    this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn);
                    continue;
                }
                string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}";

                // read manifest
                Manifest manifest;
                try
                {
                    // read manifest text
                    string json = File.ReadAllText(manifestPath);
                    if (string.IsNullOrEmpty(json))
                    {
                        this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error);
                        continue;
                    }

                    // deserialise manifest
                    manifest = jsonHelper.ReadJsonFile <Manifest>(Path.Combine(directory.FullName, "manifest.json"));
                    if (manifest == null)
                    {
                        this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error);
                        continue;
                    }
                    if (string.IsNullOrEmpty(manifest.EntryDll))
                    {
                        this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error);
                        continue;
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
                    continue;
                }
                if (!string.IsNullOrWhiteSpace(manifest.Name))
                {
                    skippedPrefix = $"Skipped {manifest.Name}";
                }

                // validate compatibility
                ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
                if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
                {
                    bool hasOfficialUrl   = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
                    bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);

                    string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
                    string warning      = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
                    if (hasOfficialUrl)
                    {
                        warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
                    }
                    if (hasUnofficialUrl)
                    {
                        warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
                    }

                    this.Monitor.Log(warning, LogLevel.Error);
                    continue;
                }

                // validate SMAPI version
                if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
                {
                    try
                    {
                        ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
                        if (minVersion.IsNewerThan(Constants.ApiVersion))
                        {
                            this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
                            continue;
                        }
                    }
                    catch (FormatException ex) when(ex.Message.Contains("not a valid semantic version"))
                    {
                        this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error);
                        continue;
                    }
                }

                // create per-save directory
                if (manifest.PerSaveConfigs)
                {
                    deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
                    try
                    {
                        string psDir = Path.Combine(directory.FullName, "psconfigs");
                        Directory.CreateDirectory(psDir);
                        if (!Directory.Exists(psDir))
                        {
                            this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error);
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error);
                        continue;
                    }
                }

                // validate mod path to simplify errors
                string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
                if (!File.Exists(assemblyPath))
                {
                    this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error);
                    continue;
                }

                // preprocess & load mod assembly
                Assembly modAssembly;
                try
                {
                    modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible);
                }
                catch (IncompatibleInstructionException ex)
                {
                    this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error);
                    continue;
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
                    continue;
                }

                // validate assembly
                try
                {
                    int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
                    if (modEntries == 0)
                    {
                        this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error);
                        continue;
                    }
                    if (modEntries > 1)
                    {
                        this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error);
                        continue;
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
                    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)
                    {
                        this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated.");
                        continue;
                    }

                    // inject data
                    // get helper
                    mod.ModManifest = manifest;
                    mod.Helper      = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager);
                    mod.Monitor     = this.GetSecondaryMonitor(manifest.Name);
                    mod.PathOnDisk  = directory.FullName;

                    // track mod
                    this.ModRegistry.Add(mod);
                    modsLoaded += 1;
                    this.Monitor.Log($"Loaded {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info);
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error);
                }
            }

            // initialise mods
            foreach (Mod mod in this.ModRegistry.GetMods())
            {
                try
                {
                    // call entry methods
                    mod.Entry(); // deprecated since 1.0
                    mod.Entry(mod.Helper);

                    // raise deprecation warning for old Entry() methods
                    if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) }))
                    {
                        deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info));
                    }
                }
                catch (Exception ex)
                {
                    this.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn);
                }
            }

            // print result
            this.Monitor.Log($"Loaded {modsLoaded} mods.");
            foreach (Action warning in deprecationWarnings)
            {
                warning();
            }
            Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods";
        }
Esempio n. 14
0
        /// <summary>Find all mods in the given folder.</summary>
        /// <param name="rootPath">The root mod path to search.</param>
        /// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param>
        /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
        private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList <Action> deprecationWarnings)
        {
            this.Monitor.Log("Finding mods...");
            void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level);

            // load mod metadata
            List <ModMetadata> mods = new List <ModMetadata>();

            foreach (string modRootPath in Directory.GetDirectories(rootPath))
            {
                if (this.Monitor.IsExiting)
                {
                    return(new ModMetadata[0]); // exit in progress
                }
                // init metadata
                string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\');

                // passthrough empty directories
                DirectoryInfo directory = new DirectoryInfo(modRootPath);
                while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
                {
                    directory = directory.GetDirectories().First();
                }

                // get manifest path
                string manifestPath = Path.Combine(directory.FullName, "manifest.json");
                if (!File.Exists(manifestPath))
                {
                    LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn);
                    continue;
                }

                // read manifest
                Manifest manifest;
                try
                {
                    // read manifest file
                    string json = File.ReadAllText(manifestPath);
                    if (string.IsNullOrEmpty(json))
                    {
                        LogSkip(displayName, "its manifest is empty.");
                        continue;
                    }

                    // parse manifest
                    manifest = jsonHelper.ReadJsonFile <Manifest>(Path.Combine(directory.FullName, "manifest.json"));
                    if (manifest == null)
                    {
                        LogSkip(displayName, "its manifest is invalid.");
                        continue;
                    }

                    // validate manifest
                    if (string.IsNullOrWhiteSpace(manifest.EntryDll))
                    {
                        LogSkip(displayName, "its manifest doesn't set an entry DLL.");
                        continue;
                    }
                    if (string.IsNullOrWhiteSpace(manifest.UniqueID))
                    {
                        deprecationWarnings.Add(() => this.Monitor.Log($"{manifest.Name} doesn't have a {nameof(IManifest.UniqueID)} in its manifest. This will be required in an upcoming SMAPI release.", LogLevel.Warn));
                    }
                }
                catch (Exception ex)
                {
                    LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}");
                    continue;
                }
                if (!string.IsNullOrWhiteSpace(manifest.Name))
                {
                    displayName = manifest.Name;
                }

                // validate compatibility
                ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
                if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken)
                {
                    bool hasOfficialUrl   = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl);
                    bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);

                    string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
                    string error        = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
                    if (hasOfficialUrl)
                    {
                        error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
                    }
                    if (hasUnofficialUrl)
                    {
                        error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
                    }

                    LogSkip(displayName, error);
                }

                // validate SMAPI version
                if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
                {
                    try
                    {
                        ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
                        if (minVersion.IsNewerThan(Constants.ApiVersion))
                        {
                            LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
                            continue;
                        }
                    }
                    catch (FormatException ex) when(ex.Message.Contains("not a valid semantic version"))
                    {
                        LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
                        continue;
                    }
                }

                // create per-save directory
                if (manifest.PerSaveConfigs)
                {
                    deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info));
                    try
                    {
                        string psDir = Path.Combine(directory.FullName, "psconfigs");
                        Directory.CreateDirectory(psDir);
                        if (!Directory.Exists(psDir))
                        {
                            LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
                        continue;
                    }
                }

                // validate DLL path
                string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
                if (!File.Exists(assemblyPath))
                {
                    LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist.");
                    continue;
                }

                // add mod metadata
                mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility));
            }

            return(mods.ToArray());
        }