diff --git a/OWML.sln b/OWML.sln index 5aac5bd78..0c32b00ad 100644 --- a/OWML.sln +++ b/OWML.sln @@ -208,7 +208,7 @@ Global {739D16FB-7848-4047-A173-500CE7C40399} = {C447A599-2700-44E1-BBFA-52880B7BFFBA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35;packages\Unity.2.1.505.0\lib\NET35 SolutionGuid = {0E767163-75F9-420A-80EB-320429543CAD} + EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35;packages\Unity.2.1.505.0\lib\NET35 EndGlobalSection EndGlobal diff --git a/schemas/manifest_schema.json b/schemas/manifest_schema.json index 88806fc26..1818364a3 100644 --- a/schemas/manifest_schema.json +++ b/schemas/manifest_schema.json @@ -91,7 +91,7 @@ "minGameVersion": { "type": "string", "description": "The minimum version of the game that this mod is compatible with", - "pattern": "^\\d+\\.\\d+\\.\\d+$", + "pattern": "^\\d+\\.\\d+\\.\\d+\\.\\d+$", "examples": [ "1.0.0", "2.0.1", @@ -101,13 +101,26 @@ "maxGameVersion": { "type": "string", "description": "The maximum version of the game that this mod is compatible with", - "pattern": "^\\d+\\.\\d+\\.\\d+$", + "pattern": "^\\d+\\.\\d+\\.\\d+\\.\\d+$", "examples": [ "1.0.0", "2.0.1", "0.1.4" ] }, + "requireLatestVersion": { + "type": "boolean", + "description": "Whether this mod needs the very latest game version", + "default": false + }, + "incompatibleVendors": { + "type": "array", + "description": "The vendors this mod does not work on", + "items": { + "type": "string", + "enum": ["Steam", "Epic", "Gamepass"] + } + }, "pathsToPreserve": { "type": "array", "description": "The paths to preserve when updating the mod", diff --git a/src/OWML.Common/Constants.cs b/src/OWML.Common/Constants.cs index 662fa4e8c..2958e4edb 100644 --- a/src/OWML.Common/Constants.cs +++ b/src/OWML.Common/Constants.cs @@ -19,5 +19,7 @@ public class Constants public const string ModDefaultConfigFileName = "default-config.json"; public const string ModManifestFileName = "manifest.json"; + + public const string GameVersionsFileName = "game-versions.json"; } } diff --git a/src/OWML.Common/Enums/GameVendor.cs b/src/OWML.Common/Enums/GameVendor.cs new file mode 100644 index 000000000..ad6dd63cc --- /dev/null +++ b/src/OWML.Common/Enums/GameVendor.cs @@ -0,0 +1,10 @@ +namespace OWML.Common.Enums +{ + public enum GameVendor + { + None, + Steam, + Epic, + Gamepass + } +} diff --git a/src/OWML.Common/GameVersions.cs b/src/OWML.Common/GameVersions.cs new file mode 100644 index 000000000..055fb1021 --- /dev/null +++ b/src/OWML.Common/GameVersions.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using OWML.Common.Interfaces; + +namespace OWML.Common +{ + public class GameVersions : IGameVersions + { + [JsonProperty("steam")] + public string Steam { get; private set; } + + [JsonProperty("epic")] + public string Epic { get; private set; } + + [JsonProperty("gamepass")] + public string Gamepass { get; private set; } + } +} diff --git a/src/OWML.Common/Interfaces/IGameVendorGetter.cs b/src/OWML.Common/Interfaces/IGameVendorGetter.cs new file mode 100644 index 000000000..e851e6d3a --- /dev/null +++ b/src/OWML.Common/Interfaces/IGameVendorGetter.cs @@ -0,0 +1,9 @@ +using OWML.Common.Enums; + +namespace OWML.Common.Interfaces +{ + public interface IGameVendorGetter + { + GameVendor GetGameVendor(); + } +} diff --git a/src/OWML.Common/Interfaces/IGameVersions.cs b/src/OWML.Common/Interfaces/IGameVersions.cs new file mode 100644 index 000000000..843035c20 --- /dev/null +++ b/src/OWML.Common/Interfaces/IGameVersions.cs @@ -0,0 +1,11 @@ +namespace OWML.Common.Interfaces +{ + public interface IGameVersions + { + string Steam { get; } + + string Epic { get; } + + string Gamepass { get; } + } +} diff --git a/src/OWML.Common/Interfaces/IModManifest.cs b/src/OWML.Common/Interfaces/IModManifest.cs index 555e2097c..8731ff463 100644 --- a/src/OWML.Common/Interfaces/IModManifest.cs +++ b/src/OWML.Common/Interfaces/IModManifest.cs @@ -1,4 +1,6 @@ -namespace OWML.Common +using OWML.Common.Enums; + +namespace OWML.Common { public interface IModManifest { @@ -29,5 +31,9 @@ public interface IModManifest string MinGameVersion { get; } string MaxGameVersion { get; } + + bool RequireLatestVersion { get; } + + GameVendor[] IncompatibleVendors { get; } } } diff --git a/src/OWML.Common/Interfaces/IModVersionChecker.cs b/src/OWML.Common/Interfaces/IModVersionChecker.cs index f03882d07..d8a32d8b8 100644 --- a/src/OWML.Common/Interfaces/IModVersionChecker.cs +++ b/src/OWML.Common/Interfaces/IModVersionChecker.cs @@ -1,7 +1,11 @@ -namespace OWML.Common +using System; + +namespace OWML.Common { public interface IModVersionChecker { bool CheckModVersion(IModData data); + + bool CheckModGameVersion(IModData data, Version latestGameVersion); } } \ No newline at end of file diff --git a/src/OWML.Common/ModManifest.cs b/src/OWML.Common/ModManifest.cs index 1ed72b953..7dca6b12d 100644 --- a/src/OWML.Common/ModManifest.cs +++ b/src/OWML.Common/ModManifest.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using OWML.Common.Enums; namespace OWML.Common { @@ -45,5 +46,11 @@ public class ModManifest : IModManifest [JsonProperty("maxGameVersion")] public string MaxGameVersion { get; private set; } = ""; + + [JsonProperty("requireLatestVersion")] + public bool RequireLatestVersion { get; private set; } + + [JsonProperty("incompatibleVendors")] + public GameVendor[] IncompatibleVendors { get; private set; } = { }; } } diff --git a/src/OWML.Launcher/App.cs b/src/OWML.Launcher/App.cs index eb1b207df..ed317d661 100644 --- a/src/OWML.Launcher/App.cs +++ b/src/OWML.Launcher/App.cs @@ -100,7 +100,7 @@ private void CopyGameFiles() } catch (Exception ex) { - _writer.WriteLine($"Error while copying game file {fileName}: {ex.Message}"); + _writer.WriteLine($"Error while copying game file {fileName}: {ex}"); } } _writer.WriteLine("Game files copied."); @@ -153,7 +153,7 @@ private void ExecutePatcher(IModData modData) } catch (Exception ex) { - _writer.WriteLine($"Cannot run patcher for mod {modData.Manifest.UniqueName} v{modData.Manifest.Version}: {ex.Message}", MessageType.Error); + _writer.WriteLine($"Cannot run patcher for mod {modData.Manifest.UniqueName} v{modData.Manifest.Version}: {ex}", MessageType.Error); } finally { @@ -172,6 +172,7 @@ void StartGameViaExe() if (_owmlConfig.ForceExe) { + _writer.WriteLine($"Launching game..."); StartGameViaExe(); return; } @@ -210,9 +211,15 @@ void StartGameViaExe() StartGameViaExe(); } } + catch (ReflectionTypeLoadException ex) + { + _writer.WriteLine($"ReflectionTypeLoadException while starting game: {ex}\n" + + "Top 5 LoaderExceptions:\n" + + $"* {string.Join("\n* ", ex.LoaderExceptions.Take(5).ToList().Select(e => e.ToString()).ToArray())}", MessageType.Error); + } catch (Exception ex) { - _writer.WriteLine($"Error while starting game: {ex.Message}", MessageType.Error); + _writer.WriteLine($"Error while starting game: {ex}", MessageType.Error); } } diff --git a/src/OWML.Launcher/OWML.Launcher.csproj b/src/OWML.Launcher/OWML.Launcher.csproj index 9797565f0..1c99a9c41 100644 --- a/src/OWML.Launcher/OWML.Launcher.csproj +++ b/src/OWML.Launcher/OWML.Launcher.csproj @@ -12,6 +12,9 @@ + + Always + Always diff --git a/src/OWML.Launcher/OWML.Manifest.json b/src/OWML.Launcher/OWML.Manifest.json index d8e36c20a..9ff0ae167 100644 --- a/src/OWML.Launcher/OWML.Manifest.json +++ b/src/OWML.Launcher/OWML.Manifest.json @@ -1,8 +1,9 @@ { + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/dev/schemas/manifest_schema.json", "author": "Alek", "name": "OWML", "uniqueName": "Alek.OWML", - "version": "2.6.0", + "version": "2.7.0", "minGameVersion": "1.1.10.47", "maxGameVersion": "1.1.12.201" } diff --git a/src/OWML.Launcher/SocketListener.cs b/src/OWML.Launcher/SocketListener.cs index f6868c34c..b1ced61b3 100644 --- a/src/OWML.Launcher/SocketListener.cs +++ b/src/OWML.Launcher/SocketListener.cs @@ -103,7 +103,7 @@ private void ProcessMessage(byte[] bytes, int count) catch (Exception ex) { ConsoleUtils.WriteByType(MessageType.Warning, $"Failed to process following message:{Separator}\n{json}{Separator}"); - ConsoleUtils.WriteByType(MessageType.Warning, $"Reason: {ex.Message}"); + ConsoleUtils.WriteByType(MessageType.Warning, $"Reason: {ex}"); continue; } diff --git a/src/OWML.Launcher/game-versions.json b/src/OWML.Launcher/game-versions.json new file mode 100644 index 000000000..594f45847 --- /dev/null +++ b/src/OWML.Launcher/game-versions.json @@ -0,0 +1,5 @@ +{ + "steam": "1.1.12.201", + "epic": "1.1.12.168", + "gamepass": "1.1.12.181" +} \ No newline at end of file diff --git a/src/OWML.Logging/ModSocketOutput.cs b/src/OWML.Logging/ModSocketOutput.cs index d18f87a07..5ee7368e3 100644 --- a/src/OWML.Logging/ModSocketOutput.cs +++ b/src/OWML.Logging/ModSocketOutput.cs @@ -72,7 +72,7 @@ private string GetCallingType(StackTrace frame) SenderName = Constants.OwmlTitle, SenderType = nameof(ModSocketOutput), Type = MessageType.Error, - Message = $"Error while getting calling type : {ex.Message}" + Message = $"Error while getting calling type : {ex}" }; _socket.WriteToSocket(message); return string.Empty; diff --git a/src/OWML.ModHelper.Menus/ModConfigMenu.cs b/src/OWML.ModHelper.Menus/ModConfigMenu.cs index e925f9f79..8b556ce45 100644 --- a/src/OWML.ModHelper.Menus/ModConfigMenu.cs +++ b/src/OWML.ModHelper.Menus/ModConfigMenu.cs @@ -7,8 +7,6 @@ namespace OWML.ModHelper.Menus { public class ModConfigMenu : ModConfigMenuBase, IModConfigMenu { - private const string EnabledTitle = "Enabled"; - public IModData ModData { get; } public IModBehaviour Mod { get; } @@ -22,8 +20,7 @@ public ModConfigMenu(IModData modData, IModBehaviour mod, IModStorage storage, I protected override void AddInputs() { - var index = 2; - AddConfigInput(EnabledTitle, ModData.Config.Enabled, index++); + var index = 3; foreach (var setting in ModData.Config.Settings) { AddConfigInput(setting.Key, setting.Value, index++); @@ -34,7 +31,6 @@ protected override void AddInputs() public override void UpdateUIValues() { - GetToggleInput(EnabledTitle).Value = ModData.Config.Enabled; foreach (var setting in ModData.Config.Settings) { SetInputValue(setting.Key, setting.Value); @@ -43,7 +39,6 @@ public override void UpdateUIValues() protected override void OnSave() { - ModData.Config.Enabled = GetInputValue(EnabledTitle); var keys = ModData.Config.Settings.Select(x => x.Key).ToList(); foreach (var key in keys) { @@ -62,7 +57,7 @@ protected override void OnSave() } catch (Exception e) { - Console.WriteLine($"Exception thrown when changing settings for {Mod?.ModHelper?.Manifest?.UniqueName} : {e.Message}, {e.StackTrace}", MessageType.Error); + Console.WriteLine($"Exception thrown when changing settings for {Mod?.ModHelper?.Manifest?.UniqueName} : {e}", MessageType.Error); } Close(); diff --git a/src/OWML.ModHelper.Menus/ModMenuWithSelectables.cs b/src/OWML.ModHelper.Menus/ModMenuWithSelectables.cs index 88466ae55..739c10a9b 100644 --- a/src/OWML.ModHelper.Menus/ModMenuWithSelectables.cs +++ b/src/OWML.ModHelper.Menus/ModMenuWithSelectables.cs @@ -137,6 +137,7 @@ protected virtual void OnActivateMenu() protected virtual void OnDeactivateMenu() { CommandListener.OnNewlyPressed -= OnButton; + OnSave(); } protected virtual void OnButton(IInputCommands command) diff --git a/src/OWML.ModHelper.Menus/ModsMenu.cs b/src/OWML.ModHelper.Menus/ModsMenu.cs index d317c549c..af748886d 100644 --- a/src/OWML.ModHelper.Menus/ModsMenu.cs +++ b/src/OWML.ModHelper.Menus/ModsMenu.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using OWML.Common; using OWML.Common.Menus; @@ -16,6 +17,7 @@ public class ModsMenu : ModPopupMenu, IModsMenu private readonly IModStorage _storage; private readonly List _modConfigMenus = new(); + private readonly List _noConfigMods = new(); private readonly List _menuOptions = new(); private IModMenus _menus; @@ -31,7 +33,14 @@ public ModsMenu( public void AddMod(IModData modData, IModBehaviour mod) { - _modConfigMenus.Add(new ModConfigMenu(modData, mod, _storage, Console)); + if (modData.Config.Settings.Count != 0) + { + _modConfigMenus.Add(new ModConfigMenu(modData, mod, _storage, Console)); + } + else + { + _noConfigMods.Add(modData); + } } public IModConfigMenu GetModMenu(IModBehaviour modBehaviour) @@ -64,6 +73,17 @@ public void Initialize(IModMenus menus, IModOWMenu owMenu) OwmlMenu.OnClosed += modsMenu.Open; } + private int CreateSeparator(IModTabbedMenu options, IModMenu menu, int index, string text) + { + var separator = new ModSeparator(menu) + { + Title = text + }; + menu.AddSeparator(separator, index++); + separator.Element.transform.localScale = options.GameplayTab.Buttons.First().Button.transform.localScale; + return index; + } + private IModPopupMenu CreateModsMenu(IModTabbedMenu options) { _menuOptions.Clear(); @@ -76,9 +96,20 @@ private IModPopupMenu CreateModsMenu(IModTabbedMenu options) owmlButton.OnClick += () => owmlTab.Open(); var enabledMods = _modConfigMenus.Where(modConfigMenu => modConfigMenu.ModData.Config.Enabled).ToList(); - var index = CreateBlockOfButtons(options, modsTab, enabledMods, 1, "ENABLED MODS"); + var index = CreateBlockOfButtons(options, modsTab, enabledMods, 1, "-- ENABLED MODS --"); + + foreach (var mod in _noConfigMods.Where(modData => modData.Config.Enabled)) + { + index = CreateSeparator(options, modsTab, index, mod.Manifest.Name); + } + var disabledMods = _modConfigMenus.Except(enabledMods).ToList(); - CreateBlockOfButtons(options, modsTab, disabledMods, index, "DISABLED MODS"); + index = CreateBlockOfButtons(options, modsTab, disabledMods, index, "-- DISABLED MODS --"); + + foreach (var mod in _noConfigMods.Where(modData => !modData.Config.Enabled)) + { + index = CreateSeparator(options, modsTab, index, mod.Manifest.Name); + } modsTab.Menu.SetValue("_menuOptions", _menuOptions.ToArray()); return modsTab; @@ -87,16 +118,13 @@ private IModPopupMenu CreateModsMenu(IModTabbedMenu options) private int CreateBlockOfButtons(IModTabbedMenu options, IModTabMenu menu, List configMenus, int index, string title) { + index = CreateSeparator(options, menu, index, $"{Environment.NewLine}{title}{Environment.NewLine}"); + if (configMenus.Count <= 0) { return index; } - var separator = new ModSeparator(menu) - { - Title = title - }; - menu.AddSeparator(separator, index++); - separator.Element.transform.localScale = options.GameplayTab.Buttons.First().Button.transform.localScale; + foreach (var modConfigMenu in configMenus) { var modButton = CreateButton(options, modConfigMenu.Manifest.Name); diff --git a/src/OWML.ModHelper/ModConfig.cs b/src/OWML.ModHelper/ModConfig.cs index 8986758cc..a5224997f 100644 --- a/src/OWML.ModHelper/ModConfig.cs +++ b/src/OWML.ModHelper/ModConfig.cs @@ -36,7 +36,7 @@ private T GetSettingsValue(string key, object setting) } catch (Exception ex) { - Debug.LogError($"Error when getting setting {key}: " + ex.Message); + Debug.LogError($"Error when getting setting {key}: {ex}"); return default; } } @@ -62,7 +62,7 @@ private T ConvertToEnum(object value) } catch (ArgumentException ex) { - Debug.LogError($"Can't convert {valueString} to enum {typeof(T)}: {ex.Message}"); + Debug.LogError($"Can't convert {valueString} to enum {typeof(T)}: {ex}"); return default; } } diff --git a/src/OWML.ModHelper/OWML.ModHelper.nuspec b/src/OWML.ModHelper/OWML.ModHelper.nuspec index 299fe2abc..23a429a50 100644 --- a/src/OWML.ModHelper/OWML.ModHelper.nuspec +++ b/src/OWML.ModHelper/OWML.ModHelper.nuspec @@ -15,7 +15,7 @@ Outer Wilds - + @@ -29,6 +29,7 @@ + diff --git a/src/OWML.ModLoader/GameVendorGetter.cs b/src/OWML.ModLoader/GameVendorGetter.cs new file mode 100644 index 000000000..84f0e85f9 --- /dev/null +++ b/src/OWML.ModLoader/GameVendorGetter.cs @@ -0,0 +1,55 @@ +using OWML.Common; +using OWML.Common.Enums; +using OWML.Common.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace OWML.ModLoader +{ + public class GameVendorGetter : IGameVendorGetter + { + private readonly IModConsole _console; + private readonly IOwmlConfig _owmlConfig; + + private GameVendor _gameVendor = GameVendor.None; + + public GameVendorGetter(IModConsole console, IOwmlConfig owmlConfig) + { + _console = console; + _owmlConfig = owmlConfig; + } + + public GameVendor GetGameVendor() + { + if (_gameVendor != GameVendor.None) + { + return _gameVendor; + } + + var gameDll = $"{_owmlConfig.ManagedPath}/Assembly-CSharp.dll"; + var assembly = Assembly.LoadFrom(gameDll); + var types = assembly.GetTypes(); + + if (types.Any(x => x.Name == "EpicEntitlementRetriever")) + { + _gameVendor = GameVendor.Epic; + } + else if (types.Any(x => x.Name == "SteamEntitlementRetriever")) + { + _gameVendor = GameVendor.Steam; + } + else + { + _gameVendor = GameVendor.Gamepass; + } + + _console.WriteLine($"Detected vendor as {_gameVendor}.", MessageType.Debug); + + return _gameVendor; + } + } +} diff --git a/src/OWML.ModLoader/ModLoader.cs b/src/OWML.ModLoader/ModLoader.cs index c50d0f721..2093b272d 100644 --- a/src/OWML.ModLoader/ModLoader.cs +++ b/src/OWML.ModLoader/ModLoader.cs @@ -8,6 +8,7 @@ using OWML.ModHelper.Menus; using OWML.Utils; using UnityEngine; +using OWML.Common.Interfaces; namespace OWML.ModLoader { @@ -70,6 +71,7 @@ public static Container CreateContainer(IApplicationHelper appHelper, IGameObjec .Add() .Add() .Add() + .Add() .Add(); } } diff --git a/src/OWML.ModLoader/ModVersionChecker.cs b/src/OWML.ModLoader/ModVersionChecker.cs index a7afb451d..e0aff0d8d 100644 --- a/src/OWML.ModLoader/ModVersionChecker.cs +++ b/src/OWML.ModLoader/ModVersionChecker.cs @@ -1,4 +1,5 @@ using OWML.Common; +using System; namespace OWML.ModLoader { @@ -6,11 +7,13 @@ class ModVersionChecker : IModVersionChecker { private readonly IModConsole _console; private readonly IModManifest _owmlManifest; + private readonly IApplicationHelper _appHelper; - public ModVersionChecker(IModConsole console, IModManifest owmlManifest) + public ModVersionChecker(IModConsole console, IModManifest owmlManifest, IApplicationHelper appHelper) { _console = console; _owmlManifest = owmlManifest; + _appHelper = appHelper; } public bool CheckModVersion(IModData data) @@ -92,5 +95,43 @@ public bool CheckModVersion(IModData data) return (major, minor, patch); } + + public bool CheckModGameVersion(IModData data, Version latestGameVersion) + { + var currentGameVersion = new Version(_appHelper.Version); + + var manifest = data.Manifest; + + _console.WriteLine($"Current game version is {currentGameVersion}, latest game version is {latestGameVersion}", MessageType.Debug); + + var isValidMinVersion = Version.TryParse(manifest.MinGameVersion, out var minVersion); + var isValidMaxVersion = Version.TryParse(manifest.MaxGameVersion, out var maxVersion); + + if (!isValidMinVersion && !isValidMaxVersion && !manifest.RequireLatestVersion) + { + _console.WriteLine($"No min/max versions given for {manifest.UniqueName}, and it doesn't require latest version.", MessageType.Debug); + return true; + } + + if (isValidMinVersion && currentGameVersion < minVersion) + { + _console.WriteLine($"Not loading {data.Manifest.UniqueName}, as the current game version {currentGameVersion} is lower than the set minimum {minVersion}.", MessageType.Error); + return false; + } + + if (isValidMaxVersion && maxVersion < currentGameVersion) + { + _console.WriteLine($"Not loading {data.Manifest.UniqueName}, as the current game version {currentGameVersion} is higher than the set maximum {maxVersion}.", MessageType.Error); + return false; + } + + if (manifest.RequireLatestVersion && currentGameVersion.ToString() != latestGameVersion.ToString()) + { + _console.WriteLine($"Not loading {data.Manifest.UniqueName}, as the current game version {currentGameVersion} is different to the latest version {latestGameVersion}", MessageType.Error); + return false; + } + + return true; + } } } diff --git a/src/OWML.ModLoader/Owo.cs b/src/OWML.ModLoader/Owo.cs index 1ace6dccc..fb1f67cfa 100644 --- a/src/OWML.ModLoader/Owo.cs +++ b/src/OWML.ModLoader/Owo.cs @@ -2,8 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Reflection; +using Newtonsoft.Json; using OWML.Common; +using OWML.Common.Enums; +using OWML.Common.Interfaces; using OWML.Common.Menus; using OWML.Logging; using OWML.ModHelper; @@ -31,6 +35,8 @@ public class Owo private readonly IModManifest _owmlManifest; private readonly IModVersionChecker _modVersionChecker; private readonly IHarmonyHelper _harmonyHelper; + private readonly IGameVendorGetter _vendorChecker; + private readonly IGameVersions _gameVersions; private readonly IList _modList = new List(); public Owo( @@ -47,7 +53,8 @@ public Owo( IProcessHelper processHelper, IModUnityEvents unityEvents, IModVersionChecker modVersionChecker, - IHarmonyHelper harmonyHelper) + IHarmonyHelper harmonyHelper, + IGameVendorGetter vendorChecker) { _modFinder = modFinder; _console = console; @@ -63,7 +70,9 @@ public Owo( _unityEvents = unityEvents; _modVersionChecker = modVersionChecker; _harmonyHelper = harmonyHelper; + _vendorChecker = vendorChecker; _owmlManifest = JsonHelper.LoadJsonObject($"{_owmlConfig.ManagedPath}/{Constants.OwmlManifestFileName}"); + _gameVersions = JsonHelper.LoadJsonObject($"{_owmlConfig.ManagedPath}/{Constants.GameVersionsFileName}"); } public void LoadMods() @@ -91,6 +100,23 @@ public void LoadMods() var modNames = mods.Where(mod => mod.Config.Enabled) .Select(mod => mod.Manifest.UniqueName).ToList(); + _console.WriteLine($"Getting game vendor...", MessageType.Debug); + var gameVendor = _vendorChecker.GetGameVendor(); + + var latestGameVersion = new Version(); + switch (gameVendor) + { + case GameVendor.Steam: + latestGameVersion = new Version(_gameVersions.Steam); + break; + case GameVendor.Epic: + latestGameVersion = new Version(_gameVersions.Epic); + break; + case GameVendor.Gamepass: + latestGameVersion = new Version(_gameVersions.Gamepass); + break; + } + foreach (var modData in sortedMods) { var missingDependencies = modData.Config.Enabled @@ -99,12 +125,18 @@ public void LoadMods() missingDependencies.ForEach(dependency => _console.WriteLine($"Error! {modData.Manifest.UniqueName} needs {dependency}, but it's disabled/missing!", MessageType.Error)); - var shouldLoad = _modVersionChecker.CheckModVersion(modData); + var shouldLoad = _modVersionChecker.CheckModVersion(modData) && _modVersionChecker.CheckModGameVersion(modData, latestGameVersion); if (!shouldLoad) { continue; } + if (modData.Manifest.IncompatibleVendors.Contains(gameVendor)) + { + _console.WriteLine($"Mod {modData.Manifest.UniqueName} is not compatible with the {gameVendor} vendor.", MessageType.Error); + continue; + } + var modType = LoadMod(modData); if (modType == null || missingDependencies.Any()) { @@ -114,7 +146,9 @@ public void LoadMods() var helper = CreateModHelper(modData); var initMod = InitializeMod(modType, helper); + _menus.ModsMenu?.AddMod(modData, initMod); + _modList.Add(initMod); } } @@ -160,7 +194,7 @@ private Type LoadMod(IModData modData) } catch (Exception ex) { - _console.WriteLine($"Exception while registering enum holders of mod {modData.Manifest.UniqueName}: {ex.Message}", MessageType.Error); + _console.WriteLine($"Exception while registering enum holders of mod {modData.Manifest.UniqueName}: {ex}", MessageType.Error); } try @@ -169,14 +203,14 @@ private Type LoadMod(IModData modData) } catch (ReflectionTypeLoadException ex) { - _console.WriteLine($"ReflectionTypeLoadException while trying to load {nameof(ModBehaviour)} of mod {modData.Manifest.UniqueName}: {ex.Message}\n" + + _console.WriteLine($"ReflectionTypeLoadException while trying to load {nameof(ModBehaviour)} of mod {modData.Manifest.UniqueName}: {ex}\n" + "Top 5 LoaderExceptions:\n" + - $"* {string.Join("\n* ", ex.LoaderExceptions.Take(5).ToList().Select(e => e.Message).ToArray())}", MessageType.Error); + $"* {string.Join("\n* ", ex.LoaderExceptions.Take(5).ToList().Select(e => e.ToString()).ToArray())}", MessageType.Error); return null; } catch (Exception ex) { - _console.WriteLine($"Exception while trying to get {nameof(ModBehaviour)} of mod {modData.Manifest.UniqueName}: {ex.Message}", MessageType.Error); + _console.WriteLine($"Exception while trying to get {nameof(ModBehaviour)} of mod {modData.Manifest.UniqueName}: {ex}", MessageType.Error); return null; } } diff --git a/src/OWML.Patcher/OWPatcher.cs b/src/OWML.Patcher/OWPatcher.cs index 076f2ff53..a7e2adb2f 100644 --- a/src/OWML.Patcher/OWPatcher.cs +++ b/src/OWML.Patcher/OWPatcher.cs @@ -57,7 +57,8 @@ private void CopyOWMLFiles() "System.ValueTuple.dll", Constants.OwmlManifestFileName, Constants.OwmlConfigFileName, - Constants.OwmlDefaultConfigFileName + Constants.OwmlDefaultConfigFileName, + Constants.GameVersionsFileName }; CopyFiles(filesToCopy, "", $"{_owmlConfig.ManagedPath}"); diff --git a/src/OWML.Utils/EnumCreator.cs b/src/OWML.Utils/EnumCreator.cs index 42dc6b5cb..4ee2c3d9d 100644 --- a/src/OWML.Utils/EnumCreator.cs +++ b/src/OWML.Utils/EnumCreator.cs @@ -472,8 +472,6 @@ public static bool IsPowerOfTwoEnum(Type enumType) /// /// Type of the enum /// The first undefined enum value - /// is - /// is not an enum /// No unused values in the enum public static T GetFirstFreeValue() where T : Enum => (T)GetFirstFreeValue(typeof(T)); diff --git a/src/OWML.Utils/EnumUtils.cs b/src/OWML.Utils/EnumUtils.cs index 251cc2f79..bc456d148 100644 --- a/src/OWML.Utils/EnumUtils.cs +++ b/src/OWML.Utils/EnumUtils.cs @@ -9,6 +9,8 @@ namespace OWML.Utils /// public static partial class EnumUtils { + private static readonly Random Rng = new Random(); + /// /// Parses an enum in a easier way /// @@ -54,6 +56,57 @@ public static object Parse(Type enumType, string value, bool ignoreCase) } } + /// + /// Parses an enum in a easier way + /// + /// Type of the enum + /// Value to parse + /// The parsed enum if successful. + /// on success, on failure. + /// is + /// is not an enum + public static bool TryParse(Type enumType, string value, out object result) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + try + { + result = Enum.Parse(enumType, value); + return true; + } + catch + { + result = null; + return false; + } + } + + /// + /// Parses an enum in a easier way + /// + /// Type of the enum + /// Value to parse + /// true to ignore case; false to regard case. + /// The parsed enum if successful. + /// on success, on failure. + /// is + /// is not an enum + public static bool TryParse(Type enumType, string value, bool ignoreCase, out object result) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + try + { + result = Enum.Parse(enumType, value, ignoreCase); + return true; + } + catch + { + result = null; + return false; + } + } + /// /// Converts a number to an enum. /// @@ -189,6 +242,35 @@ public static object FromObject(Type enumType, ulong value) return System.Enum.ToObject(enumType, value); } + /// + /// Gets the underlying type of an enum type. + /// + /// Type of the enum + /// The underlying type of + /// is + /// is not an enum + public static Type GetUnderlyingType(Type enumType) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + return System.Enum.GetUnderlyingType(enumType); + } + + /// + /// Get the name of an enum value + /// + /// Type of the enum + /// Value to get the name of + /// The name of the enum value + /// is + /// is not an enum + public static string GetName(Type enumType, object value) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + return System.Enum.GetName(enumType, value); + } + /// /// Gets all names in an enum /// @@ -217,6 +299,20 @@ public static object[] GetValues(Type enumType) return System.Enum.GetValues(enumType).Cast().ToArray(); } + /// + /// Counts the number of enums values contained in a given enum type. + /// + /// Type of the enum + /// The number of enum values. + /// is + /// is not an enum + public static int Count(Type enumType) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + return Enum.GetValues(enumType).Length; + } + /// /// Checks if an enum is defined. /// @@ -357,6 +453,36 @@ public static object GetMaxValue(Type enumType) return GetValues(enumType).Max(); } + /// + /// Gets a random value from an enum + /// + /// Type of the enum + /// A randomly selected enum value from the given enum type + public static object GetRandom(Type enumType) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + var values = Enum.GetValues(enumType); + var item = Rng.Next(0, values.Length); + return values.GetValue(item); + } + + /// + /// Gets a random value from an enum with exclusions + /// + /// Type of the enum + /// Enums to exclude from the randomization + /// A randomly selected enum value from the given enum type + public static object GetRandom(Type enumType, params object[] excluded) + { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new NotAnEnumException(enumType); + if (excluded == null) throw new ArgumentNullException("excluded"); + var values = Enum.GetValues(enumType).Cast().Where(v => !excluded.Contains(v)).ToArray(); + var item = Rng.Next(0, values.Length); + return values.GetValue(item); + } + /// /// Parses an enum in a easier way /// @@ -396,97 +522,135 @@ public static T Parse(string value, bool ignoreCase, T errorReturn = default) } } + /// + /// Parses an enum in a easier way + /// + /// Type of the enum + /// Value to parse + /// The parsed enum if successful. + /// on success, on failure. + public static bool TryParse(string value, out T result) where T : Enum + { + try + { + result = (T)Enum.Parse(typeof(T), value); + return true; + } + catch + { + result = default; + return false; + } + } + + /// + /// Parses an enum in a easier way + /// + /// Type of the enum + /// Value to parse + /// true to ignore case; false to regard case. + /// The parsed enum if successful. + /// on success, on failure. + public static bool TryParse(string value, bool ignoreCase, out T result) where T : Enum + { + try + { + result = (T)Enum.Parse(typeof(T), value, ignoreCase); + return true; + } + catch + { + result = default; + return false; + } + } /// /// Converts a number to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(object value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts a to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(sbyte value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts a to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(byte value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts a to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(short value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts an to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(ushort value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts an to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(int value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts an to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(uint value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts a to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(long value) where T : Enum => (T)Enum.ToObject(typeof(T), value); /// /// Converts an to an enum. /// - /// Type of the enum + /// Type of the enum /// Value to convert /// The number as an enum - /// is - /// is not an enum public static T FromObject(ulong value) where T : Enum => (T)Enum.ToObject(typeof(T), value); + /// + /// Gets the underlying type of an enum type. + /// + /// Type of the enum + /// The underlying type of + public static Type GetUnderlyingType() where T : Enum => Enum.GetUnderlyingType(typeof(T)); + + /// + /// Get the name of an enum value + /// + /// Type of the enum + /// Value to get the name of + /// The name of the enum value + public static string GetName(this T value) where T : Enum => Enum.GetName(typeof(T), value); /// /// Gets all names in an enum @@ -499,10 +663,16 @@ public static T Parse(string value, bool ignoreCase, T errorReturn = default) /// Gets all enum values in an enum /// /// Type of the enum - /// What to return if the parse fails. /// The list of all values in the enum public static T[] GetValues() where T : Enum => Enum.GetValues(typeof(T)).Cast().ToArray(); + /// + /// Counts the number of enums values contained in a given enum type. + /// + /// Type of the enum + /// The number of enum values. + public static int Count() where T : Enum => Enum.GetValues(typeof(T)).Length; + /// /// Checks if an enum is defined. /// @@ -593,6 +763,14 @@ public static bool IsDefined(object value) where T : Enum /// if defined, if not. public static bool IsDefined(string value) where T : Enum => IsDefined((object)value); + /// + /// Checks if an enum is defined. + /// + /// Type of the enum + /// Value to check + /// if defined, if not. + public static bool IsDefined(this T value) where T : Enum => IsDefined((object)value); + /// /// Gets the minimum value in the enum /// @@ -606,18 +784,69 @@ public static bool IsDefined(object value) where T : Enum /// Type of the enum /// the maximum value in the enum public static T GetMaxValue() where T : Enum => GetValues().Max(); + + /// + /// Converts the enum to its string representation according to the given format. + /// + /// Type of the enum + /// The enum value to convert. + /// The output format to use. + /// The underlying type of + public static string Format(this T value, string format) where T : Enum => Enum.Format(typeof(T), value, format); + + /// + /// Gets a random value from an enum + /// + /// Type of the enum + /// A randomly selected enum value from the given enum type + public static T GetRandom() where T : Enum + { + var values = EnumUtils.GetValues(); + var item = Rng.Next(0, values.Length); + return (T)values.GetValue(item); + } + + /// + /// Gets a random value from an enum with exclusions + /// + /// Type of the enum + /// Enums to exclude from the randomization + /// A randomly selected enum value from the given enum type + public static T GetRandom(params T[] excluded) where T : Enum + { + var values = Enum.GetValues(typeof(T)).Cast().Where(v => !excluded.Contains(v)).ToArray(); + var item = Rng.Next(0, values.Length); + return (T)values.GetValue(item); + } } } +/// +/// The exception that is thrown when an enum type is needed but the given type is not an enum. +/// public class NotAnEnumException : Exception { private Type _type; + + /// + /// The type that is not an enum + /// public Type Type => _type; + /// + /// Initializes a new instance of the NotAnEnumException class with a type that is not an enum. + /// + /// The type that is not an enum public NotAnEnumException(Type type) : base($"The given type isn't an enum ({type.FullName} isn't an Enum)") { _type = type; } + + /// + /// Initializes a new instance of the NotAnEnumException class with a type that is not an enum and a reference to the inner exception that is the cause of this exception. + /// + /// The type that is not an enum + /// The exception caused the current exception public NotAnEnumException(Type type, Exception innerException) : base($"The given type isn't an enum ({type.FullName} isn't an Enum)", innerException) { _type = type; diff --git a/src/SampleMods/OWML.EnableDebugMode/manifest.json b/src/SampleMods/OWML.EnableDebugMode/manifest.json index 9cca6e1d9..006f3bdf3 100644 --- a/src/SampleMods/OWML.EnableDebugMode/manifest.json +++ b/src/SampleMods/OWML.EnableDebugMode/manifest.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/dev/schemas/manifest_schema.json", "filename": "OWML.EnableDebugMode.dll", "author": "Alek", "name": "EnableDebugMode", diff --git a/src/SampleMods/OWML.EnumExample/manifest.json b/src/SampleMods/OWML.EnumExample/manifest.json index 27792b2d0..a7887d8f6 100644 --- a/src/SampleMods/OWML.EnumExample/manifest.json +++ b/src/SampleMods/OWML.EnumExample/manifest.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/dev/schemas/manifest_schema.json", "filename": "OWML.EnumExample.dll", "author": "MegaPiggy", "name": "Enum Example", diff --git a/src/SampleMods/OWML.ExampleAPI/manifest.json b/src/SampleMods/OWML.ExampleAPI/manifest.json index 670428a75..5a4e253a2 100644 --- a/src/SampleMods/OWML.ExampleAPI/manifest.json +++ b/src/SampleMods/OWML.ExampleAPI/manifest.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/dev/schemas/manifest_schema.json", "filename": "OWML.ExampleAPI.dll", "author": "_nebula", "name": "ExampleAPI", diff --git a/src/SampleMods/OWML.LoadCustomAssets/manifest.json b/src/SampleMods/OWML.LoadCustomAssets/manifest.json index a706a47b4..3771652c1 100644 --- a/src/SampleMods/OWML.LoadCustomAssets/manifest.json +++ b/src/SampleMods/OWML.LoadCustomAssets/manifest.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/ow-mods/owml/dev/schemas/manifest_schema.json", "filename": "OWML.LoadCustomAssets.dll", "author": "Alek", "name": "LoadCustomAssets",