From 34a0d672038f9e7d694491e09cc3681230fa34dd Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 4 Jul 2024 15:48:56 -0400 Subject: [PATCH 1/5] Try to add unpatching --- .idea/.idea.OWML/.idea/.gitignore | 13 ++++ .idea/.idea.OWML/.idea/encodings.xml | 4 ++ .idea/.idea.OWML/.idea/indexLayout.xml | 8 +++ .idea/.idea.OWML/.idea/vcs.xml | 6 ++ schemas/manifest_schema.json | 4 ++ src/OWML.Common/Interfaces/IModManifest.cs | 3 + src/OWML.Common/Interfaces/IOwmlConfig.cs | 2 + src/OWML.Common/ModManifest.cs | 6 ++ src/OWML.Common/OwmlConfig.cs | 3 + src/OWML.Launcher/App.cs | 75 +++++++++++++++++++--- 10 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 .idea/.idea.OWML/.idea/.gitignore create mode 100644 .idea/.idea.OWML/.idea/encodings.xml create mode 100644 .idea/.idea.OWML/.idea/indexLayout.xml create mode 100644 .idea/.idea.OWML/.idea/vcs.xml diff --git a/.idea/.idea.OWML/.idea/.gitignore b/.idea/.idea.OWML/.idea/.gitignore new file mode 100644 index 00000000..7798d40d --- /dev/null +++ b/.idea/.idea.OWML/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.OWML.iml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.OWML/.idea/encodings.xml b/.idea/.idea.OWML/.idea/encodings.xml new file mode 100644 index 00000000..df87cf95 --- /dev/null +++ b/.idea/.idea.OWML/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.OWML/.idea/indexLayout.xml b/.idea/.idea.OWML/.idea/indexLayout.xml new file mode 100644 index 00000000..7b08163c --- /dev/null +++ b/.idea/.idea.OWML/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.OWML/.idea/vcs.xml b/.idea/.idea.OWML/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/.idea.OWML/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/schemas/manifest_schema.json b/schemas/manifest_schema.json index c6747018..0ed33f98 100644 --- a/schemas/manifest_schema.json +++ b/schemas/manifest_schema.json @@ -23,6 +23,10 @@ "type": "string", "description": "The path to the patcher to use" }, + "unpatcher": { + "type": "string", + "description": "The path to the unpatcher to use" + }, "author": { "type": "string", "description": "The author of the mod", diff --git a/src/OWML.Common/Interfaces/IModManifest.cs b/src/OWML.Common/Interfaces/IModManifest.cs index 8731ff46..97192823 100644 --- a/src/OWML.Common/Interfaces/IModManifest.cs +++ b/src/OWML.Common/Interfaces/IModManifest.cs @@ -7,6 +7,7 @@ public interface IModManifest string Filename { get; } string Patcher { get; } + string Unpatcher { get; } string Author { get; } @@ -20,6 +21,8 @@ public interface IModManifest string PatcherPath { get; } + string UnpatcherPath { get; } + string UniqueName { get; } string ModFolderPath { get; set; } diff --git a/src/OWML.Common/Interfaces/IOwmlConfig.cs b/src/OWML.Common/Interfaces/IOwmlConfig.cs index a1d996ba..56fd6b32 100644 --- a/src/OWML.Common/Interfaces/IOwmlConfig.cs +++ b/src/OWML.Common/Interfaces/IOwmlConfig.cs @@ -25,5 +25,7 @@ public interface IOwmlConfig bool IncrementalGC { get; set; } int SocketPort { get; set; } + + string[] PrepatchersExecuted { get; set; } } } diff --git a/src/OWML.Common/ModManifest.cs b/src/OWML.Common/ModManifest.cs index 7dca6b12..b54cfbd5 100644 --- a/src/OWML.Common/ModManifest.cs +++ b/src/OWML.Common/ModManifest.cs @@ -10,6 +10,9 @@ public class ModManifest : IModManifest [JsonProperty("patcher")] public string Patcher { get; private set; } + + [JsonProperty("unpatcher")] + public string Unpatcher { get; private set; } [JsonProperty("author")] public string Author { get; private set; } @@ -41,6 +44,9 @@ public class ModManifest : IModManifest [JsonIgnore] public string PatcherPath => ModFolderPath + Patcher; + [JsonIgnore] + public string UnpatcherPath => ModFolderPath + Unpatcher; + [JsonProperty("minGameVersion")] public string MinGameVersion { get; private set; } = ""; diff --git a/src/OWML.Common/OwmlConfig.cs b/src/OWML.Common/OwmlConfig.cs index 2b79d451..45de4a4b 100644 --- a/src/OWML.Common/OwmlConfig.cs +++ b/src/OWML.Common/OwmlConfig.cs @@ -17,6 +17,9 @@ public class OwmlConfig : IOwmlConfig [JsonProperty("incrementalGC")] public bool IncrementalGC { get; set; } + [JsonProperty("prepatchersExecuted")] + public string[] PrepatchersExecuted { get; set; } + [JsonIgnore] public bool IsSpaced => Directory.Exists(Path.Combine(GamePath, "Outer Wilds_Data")); diff --git a/src/OWML.Launcher/App.cs b/src/OWML.Launcher/App.cs index d51b8baa..35c89481 100644 --- a/src/OWML.Launcher/App.cs +++ b/src/OWML.Launcher/App.cs @@ -58,8 +58,18 @@ public void Run() _owPatcher.PatchGame(); - ExecutePatchers(mods); + var prepatchersExecuted = ExecutePatchers(mods); + if (_owmlConfig.PrepatchersExecuted.Length != 0) + { + var failedUnpatchers = ExecuteUnpatchers(mods, _owmlConfig.PrepatchersExecuted); + // If an unpatcher failed we want to try and execute it next time + prepatchersExecuted.AddRange(failedUnpatchers); + } + + _owmlConfig.PrepatchersExecuted = prepatchersExecuted.ToArray(); + JsonHelper.SaveJsonObject(Constants.OwmlConfigFileName, _owmlConfig); + var hasPortArgument = _argumentHelper.HasArgument(Constants.ConsolePortArgument); StartGame(); @@ -123,28 +133,72 @@ private void ShowModList(IList mods) } } - private void ExecutePatchers(IEnumerable mods) + private List ExecutePatchers(IEnumerable mods) { _writer.WriteLine("Executing patchers...", MessageType.Debug); - mods + return mods .Where(ShouldExecutePatcher) .ToList() - .ForEach(ExecutePatcher); + .Where(mod => !ExecutePatcher(mod)) + .Select(mod => mod.Manifest.UniqueName).ToList(); } private static bool ShouldExecutePatcher(IModData modData) => !string.IsNullOrEmpty(modData.Manifest.Patcher) && modData.Enabled; - private void ExecutePatcher(IModData modData) + private AppDomain CreateDomainForMod(IModData modData, string name) => + AppDomain.CreateDomain( + $"{modData.Manifest.UniqueName}.{name}", + AppDomain.CurrentDomain.Evidence, + new AppDomainSetup { ApplicationBase = _owmlConfig.GamePath }); + + private string[] ExecuteUnpatchers(IEnumerable mods, string[] needUnpatch) + { + _writer.WriteLine("Executing unpatchers...", MessageType.Debug); + return mods + .Where(modData => !string.IsNullOrEmpty(modData.Manifest.Unpatcher) && !modData.Enabled && + needUnpatch.Contains(modData.Manifest.UniqueName)) + .ToList() + .Where(UnpatchMod) + .Select(modData => modData.Manifest.UniqueName).ToArray(); + } + + private bool UnpatchMod(IModData modData) { _writer.WriteLine($"Executing patcher for {modData.Manifest.UniqueName} v{modData.Manifest.Version}", MessageType.Message); - var domain = AppDomain.CreateDomain( - $"{modData.Manifest.UniqueName}.Patcher", - AppDomain.CurrentDomain.Evidence, - new AppDomainSetup { ApplicationBase = _owmlConfig.GamePath }); + var domain = CreateDomainForMod(modData, "Unpatcher"); + + var failed = false; + + try + { + domain.ExecuteAssembly( + modData.Manifest.UnpatcherPath, + new[] { Path.GetDirectoryName(modData.Manifest.UnpatcherPath) }); + } + catch (Exception ex) + { + failed = true; + _writer.WriteLine($"Cannot run unpatcher for mod {modData.Manifest.UniqueName} v{modData.Manifest.Version}: {ex}", MessageType.Error); + } + finally + { + AppDomain.Unload(domain); + } + return failed; + } + + private bool ExecutePatcher(IModData modData) + { + _writer.WriteLine($"Executing patcher for {modData.Manifest.UniqueName} v{modData.Manifest.Version}", MessageType.Message); + + var domain = CreateDomainForMod(modData, "Patcher"); + + var failed = false; + try { domain.ExecuteAssembly( @@ -153,12 +207,15 @@ private void ExecutePatcher(IModData modData) } catch (Exception ex) { + failed = true; _writer.WriteLine($"Cannot run patcher for mod {modData.Manifest.UniqueName} v{modData.Manifest.Version}: {ex}", MessageType.Error); } finally { AppDomain.Unload(domain); } + + return failed; } private void StartGame() From 2513570d2e4f394e58e188f6c936059853b8b319 Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 4 Jul 2024 16:28:16 -0400 Subject: [PATCH 2/5] Add to docs, remove extra ToList --- docs/content/pages/guides/apis.md | 5 +- docs/content/pages/guides/prepatchers.md | 67 ++++++++++++++++++++++++ src/OWML.Launcher/App.cs | 1 - 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 docs/content/pages/guides/prepatchers.md diff --git a/docs/content/pages/guides/apis.md b/docs/content/pages/guides/apis.md index 643cf7fe..82d08d2b 100644 --- a/docs/content/pages/guides/apis.md +++ b/docs/content/pages/guides/apis.md @@ -5,11 +5,11 @@ Sort_Priority: 35 # Creating APIs -To allow for easy interoperability between mods, OWML provides an API system where mods can provide and consume APIs from eachother easily. +To allow for easy interoperability between mods, OWML provides an API system where mods can provide and consume APIs from each other easily. ## Creating an API -To create an API start by making an interface with all the methods your api will have. +To create an API start by making an interface with all the methods your api will have. ```csharp public interface IMyCoolApi { @@ -57,4 +57,3 @@ public class MyCoolMod : ModBehaviour { } } ``` - diff --git a/docs/content/pages/guides/prepatchers.md b/docs/content/pages/guides/prepatchers.md new file mode 100644 index 00000000..08f477ac --- /dev/null +++ b/docs/content/pages/guides/prepatchers.md @@ -0,0 +1,67 @@ +--- +Title: Prepatchers +Sort_Priority: 33 +--- + +# Prepatchers + +In certain contexts you may need to edit game files before the game starts. This is where +prepatchers come in. Prepatchers are run by OWML directly before a game starts, allowing you to modify game files. + +To create a prepatcher you'll need a separate project from your mod. This can be done by creating a new project in your solution with the executable type, it should automatically build to your mod folder. + +Now in your mod manifest you need to set the `patcher` field to the path of the executable relative to the root of the +mod folder. + +## Creating A Prepatcher + +A prepatcher is a simple console app that OWML executes, it's only passed the location of your mod folder. +However, it is possible to get the game's location by doing `AppDomain.CurrentDomain.BaseDirectory`: + +```csharp +public static void Main(string[] args) +{ + var modPath = args.Length > 0 ? args[0] : "."; + var gamePath = AppDomain.CurrentDomain.BaseDirectory; + + Console.WriteLine($"Mod Path: {modPath}"); + Console.WriteLine($"Game Path: {gamePath}"); + + // DoStuff(modPath, gamePath); +} +``` + +Keep in mind unlike in a ModBehaviour class the `ModHelper` is not available in a prepatcher. +You'll need to have the prepatcher include libraries like Newtonsoft.Json to read and write JSON files. + +### Logging + +Due to not having access to ModHelper, you'll need to use `Console.WriteLine` to log information. +This **will not output to the manager window** to test prepatchers, we recommend you launch `OWML.Launcher.exe` in a +terminal directly to properly see stdout. + +If a prepatcher errors it *should usually* be outputted to the manage window as OWML is setup to catch and +log any exceptions thrown by the prepatcher. + +### Warnings + +Due to the nature of prepatchers, the manager cannot undo changes made by them. This means if a prepatcher runs +and doesn't have an [unpatcher](#unpatching) to revert the changes, the game will continue to be modified even if the +mod is uninstalled or disabled. + +The manager will try it's best to warn the user of this. If your mod has prepatcher and +is disabled or uninstalled the manager will show a dialog explaining that +your mod has modified game files in an irreversible way and encourages them to validate the game files. + +If your mod uses an unpatcher the manager will forgo showing this dialog **on disable**, but will still show it on uninstall +as at that point your mod cannot have its unpatcher run. + +## Unpatching + +OWML can also run an "unpatcher", this is another executable that is run when OWML launches if you're mod has been disabled. +They work exactly like prepatchers and are passed the same thing. + +Setup for an unpatcher is exactly the same as a prepatcher, just set the `unpatcher` field in your manifest to the path of the executable. + +!!! alert-warning "Warning" + OWML tries its best to only run an unpatcher if your mod was previously enabled, however, it's possible for a unpatcher to run when the corresponding prepatcher didn't, so make sure your unpatcher can handle a state where the mod was never enabled i.e. checking a file exists before deleting it or checking if a file has been modified before reverting it. diff --git a/src/OWML.Launcher/App.cs b/src/OWML.Launcher/App.cs index 35c89481..d6092270 100644 --- a/src/OWML.Launcher/App.cs +++ b/src/OWML.Launcher/App.cs @@ -138,7 +138,6 @@ private List ExecutePatchers(IEnumerable mods) _writer.WriteLine("Executing patchers...", MessageType.Debug); return mods .Where(ShouldExecutePatcher) - .ToList() .Where(mod => !ExecutePatcher(mod)) .Select(mod => mod.Manifest.UniqueName).ToList(); } From b2f8f37bd99b3c94ccf5d41e2321f065bb6b09e9 Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Thu, 4 Jul 2024 13:37:10 -0700 Subject: [PATCH 3/5] rename method, clean up linq --- src/OWML.Launcher/App.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/OWML.Launcher/App.cs b/src/OWML.Launcher/App.cs index d6092270..f6898712 100644 --- a/src/OWML.Launcher/App.cs +++ b/src/OWML.Launcher/App.cs @@ -139,7 +139,8 @@ private List ExecutePatchers(IEnumerable mods) return mods .Where(ShouldExecutePatcher) .Where(mod => !ExecutePatcher(mod)) - .Select(mod => mod.Manifest.UniqueName).ToList(); + .Select(mod => mod.Manifest.UniqueName) + .ToList(); } private static bool ShouldExecutePatcher(IModData modData) => @@ -158,12 +159,12 @@ private string[] ExecuteUnpatchers(IEnumerable mods, string[] needUnpa return mods .Where(modData => !string.IsNullOrEmpty(modData.Manifest.Unpatcher) && !modData.Enabled && needUnpatch.Contains(modData.Manifest.UniqueName)) - .ToList() - .Where(UnpatchMod) - .Select(modData => modData.Manifest.UniqueName).ToArray(); + .Where(ExecuteUnpatcher) + .Select(modData => modData.Manifest.UniqueName) + .ToArray(); } - private bool UnpatchMod(IModData modData) + private bool ExecuteUnpatcher(IModData modData) { _writer.WriteLine($"Executing patcher for {modData.Manifest.UniqueName} v{modData.Manifest.Version}", MessageType.Message); From 28b65c5ff4206c70d5dbafb3da18842ffb7f1e1a Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 4 Jul 2024 16:40:45 -0400 Subject: [PATCH 4/5] Update prepatchers.md --- docs/content/pages/guides/prepatchers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/pages/guides/prepatchers.md b/docs/content/pages/guides/prepatchers.md index 08f477ac..736f0be2 100644 --- a/docs/content/pages/guides/prepatchers.md +++ b/docs/content/pages/guides/prepatchers.md @@ -37,7 +37,7 @@ You'll need to have the prepatcher include libraries like Newtonsoft.Json to rea ### Logging Due to not having access to ModHelper, you'll need to use `Console.WriteLine` to log information. -This **will not output to the manager window** to test prepatchers, we recommend you launch `OWML.Launcher.exe` in a +This **will not output to the manager window** to test prepatchers. We recommend you launch `OWML.Launcher.exe` in a terminal directly to properly see stdout. If a prepatcher errors it *should usually* be outputted to the manage window as OWML is setup to catch and From 1e679b0b9c0dabfcdd59a4825dfa203e21aebe0b Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 4 Jul 2024 16:44:44 -0400 Subject: [PATCH 5/5] Update prepatchers.md --- docs/content/pages/guides/prepatchers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/pages/guides/prepatchers.md b/docs/content/pages/guides/prepatchers.md index 736f0be2..8480584b 100644 --- a/docs/content/pages/guides/prepatchers.md +++ b/docs/content/pages/guides/prepatchers.md @@ -64,4 +64,4 @@ They work exactly like prepatchers and are passed the same thing. Setup for an unpatcher is exactly the same as a prepatcher, just set the `unpatcher` field in your manifest to the path of the executable. !!! alert-warning "Warning" - OWML tries its best to only run an unpatcher if your mod was previously enabled, however, it's possible for a unpatcher to run when the corresponding prepatcher didn't, so make sure your unpatcher can handle a state where the mod was never enabled i.e. checking a file exists before deleting it or checking if a file has been modified before reverting it. + OWML tries its best to only run an unpatcher if your mod was previously enabled. However, an unpatcher can run when the corresponding prepatcher doesn't. It's important to make sure your unpatcher can handle a state where the mod was never enabled. You can do this by checking if a file exists before deleting it, checking if a file has been modified before reverting it, etc.