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/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..8480584b
--- /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, 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.
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..f6898712 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))
+ .Where(ExecuteUnpatcher)
+ .Select(modData => modData.Manifest.UniqueName)
+ .ToArray();
+ }
+
+ private bool ExecuteUnpatcher(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()