Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Unpatching #588

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .idea/.idea.OWML/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.OWML/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.OWML/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.OWML/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions docs/content/pages/guides/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -57,4 +57,3 @@ public class MyCoolMod : ModBehaviour {
}
}
```

67 changes: 67 additions & 0 deletions docs/content/pages/guides/prepatchers.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions schemas/manifest_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/OWML.Common/Interfaces/IModManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface IModManifest
string Filename { get; }

string Patcher { get; }
string Unpatcher { get; }

string Author { get; }

Expand All @@ -20,6 +21,8 @@ public interface IModManifest

string PatcherPath { get; }

string UnpatcherPath { get; }

string UniqueName { get; }

string ModFolderPath { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions src/OWML.Common/Interfaces/IOwmlConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ public interface IOwmlConfig
bool IncrementalGC { get; set; }

int SocketPort { get; set; }

string[] PrepatchersExecuted { get; set; }
}
}
6 changes: 6 additions & 0 deletions src/OWML.Common/ModManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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; } = "";

Expand Down
3 changes: 3 additions & 0 deletions src/OWML.Common/OwmlConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));

Expand Down
77 changes: 67 additions & 10 deletions src/OWML.Launcher/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -123,28 +133,72 @@ private void ShowModList(IList<IModData> mods)
}
}

private void ExecutePatchers(IEnumerable<IModData> mods)
private List<string> ExecutePatchers(IEnumerable<IModData> 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<IModData> 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(
Expand All @@ -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()
Expand Down
Loading