Skip to content

Commit

Permalink
Merge branch 'dev' into raicuparta/vr-flag
Browse files Browse the repository at this point in the history
  • Loading branch information
Raicuparta committed Jun 27, 2020
2 parents 3f2079f + cea5b69 commit 0e82fb3
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 104 deletions.
10 changes: 10 additions & 0 deletions OWML.Launcher/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ private void PatchGame(bool enableVR)
{
_owPatcher.PatchGame();
_vrPatcher.PatchVR(enableVR);
var enableVR = vrMod != null;
_writer.WriteLine(enableVR ? $"{vrMod.Manifest.UniqueName} requires VR." : "No mods require VR.");
try
{
_vrPatcher.PatchVR(enableVR);
}
catch (Exception ex)
{
_writer.WriteLine($"Error while applying VR patch: {ex}");
}
}

private void StartGame(string[] args, bool enableVR)
Expand Down
179 changes: 179 additions & 0 deletions OWML.Patcher/BinaryPatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using OWML.Common;
using System;
using System.IO;
using System.Linq;
using System.Text;

namespace OWML.Patcher
{
public class BinaryPatcher
{
private readonly IOwmlConfig _owmlConfig;
private readonly IModConsole _writer;

// Indexes of addresses that need to be shifted due to added bytes.
private static readonly int[] _addressIndexes = { 0x2d0, 0x2e0, 0x2f4, 0x308, 0x31c, 0x330, 0x344, 0x358, 0x36c, 0x380 };
private readonly string _filePath;

private const string EnabledVRDevice = "OpenVR";
private const int RemovedBytes = 2;
// String that comes right before the bytes we want to patch.
private const string PatchZoneText = "Assets/Scenes/PostCreditScene.unity";
private const int PatchStartZoneOffset = 6;
private const int FileSizeStartIndex = 4;
private const int FileSizeEndIndex = FileSizeStartIndex + 4;
private const string FileName = "globalgamemanagers";
private const string BackupSuffix = ".bak";
private const int BuildSettingsStartAddressIndex = 0x2CC;
private const int BuildSettingsSizeIndex = 0x2D0;
private const int BlockAddressOffset = 0x1000;

public BinaryPatcher(IOwmlConfig owmlConfig, IModConsole writer)
{
_owmlConfig = owmlConfig;
_writer = writer;
_filePath = $"{_owmlConfig.DataPath}/{FileName}";
}

public void Patch()
{
if (!File.Exists(_filePath))
{
throw new FileNotFoundException(_filePath);
}

var fileBytes = File.ReadAllBytes(_filePath);

var buildSettingsStartIndex = BitConverter.ToInt32(fileBytes, BuildSettingsStartAddressIndex) + BlockAddressOffset;
var buildSettingsSize = BitConverter.ToInt32(fileBytes, BuildSettingsSizeIndex);
var buildSettingsEndIndex = buildSettingsStartIndex + buildSettingsSize;

var patchStartIndex = FindPatchStartIndex(fileBytes, buildSettingsStartIndex, buildSettingsEndIndex);
var isAlreadyPatched = FindExistingPatch(fileBytes, patchStartIndex, buildSettingsEndIndex);

if (isAlreadyPatched)
{
_writer.WriteLine("globalgamemanagers already patched.");
return;
}

BackupFile(_filePath);
var patchedBytes = CreatePatchedFileBytes(fileBytes, patchStartIndex);
File.WriteAllBytes(_filePath, patchedBytes);
_writer.WriteLine("Successfully patched globalgamemanagers.");
}

private int FindPatchStartIndex(byte[] fileBytes, int startIndex, int endIndex)
{
byte[] patchZoneBytes = Encoding.ASCII.GetBytes(PatchZoneText);
var patchZoneMatch = 0;
for (var i = startIndex; i < endIndex; i++)
{
var fileByte = fileBytes[i];
var patchZoneByte = patchZoneBytes[patchZoneMatch];
if (fileByte == patchZoneByte)
{
patchZoneMatch++;
}
else
{
patchZoneMatch = 0;
}
if (patchZoneMatch == patchZoneBytes.Length)
{
return i + PatchStartZoneOffset;
}
}
throw new Exception("Could not find patch zone in globalgamemanagers. This probably means the VR patch needs to be updated.");
}

private bool FindExistingPatch(byte[] fileBytes, int startIndex, int endIndex)
{
byte[] existingPatchBytes = Encoding.ASCII.GetBytes(EnabledVRDevice);
var existingPatchMatch = 0;

for (var i = startIndex; i < endIndex; i++)
{
var fileByte = fileBytes[i];
var existingPatchByte = existingPatchBytes[existingPatchMatch];
if (fileByte == existingPatchByte)
{
existingPatchMatch++;
}
else
{
existingPatchMatch = 0;
}
if (existingPatchMatch == existingPatchBytes.Length)
{
return true;
}
}
return false;
}

private byte[] CreatePatchedFileBytes(byte[] fileBytes, int patchStartIndex)
{
// First byte is the number of elements in the array.
var vrDevicesDeclarationBytes = new byte[] { 1, 0, 0, 0, (byte)EnabledVRDevice.Length, 0, 0, 0 };

// Bytes that need to be inserted into the file.
var patchBytes = vrDevicesDeclarationBytes.Concat(Encoding.ASCII.GetBytes(EnabledVRDevice));

PatchFileSize(fileBytes, patchBytes.Count());

// Split the file in two parts. The patch bytes will be inserted between these parts.
var originalFirstPart = fileBytes.Take(patchStartIndex);
var originalSecondPart = fileBytes.Skip(patchStartIndex + RemovedBytes);

return originalFirstPart
.Concat(patchBytes)
.Concat(originalSecondPart)
.ToArray();
}

private void PatchFileSize(byte[] fileBytes, int patchSize)
{
// Read file size from original file. Reversed due to big endianness.
var originalFileSizeBytes = fileBytes.Take(FileSizeEndIndex).Skip(FileSizeStartIndex).Reverse().ToArray();
var originalFileSize = BitConverter.ToInt32(originalFileSizeBytes, 0);

// Generate bytes for new patched file.
var fileSizeChange = patchSize - RemovedBytes;
var patchedFileSize = originalFileSize + fileSizeChange;
var patchedFileSizeBytes = BitConverter.GetBytes(patchedFileSize).Reverse().ToArray();

// Overwrite original file size bytes with patched size.
for (int i = 0; i < patchedFileSizeBytes.Length; i++)
{
fileBytes[FileSizeStartIndex + i] = patchedFileSizeBytes[i];
}

// Shift addresses where necessary.
foreach (var startIndex in _addressIndexes)
{
var address = BitConverter.ToInt32(fileBytes, startIndex);
var patchedAddressBytes = BitConverter.GetBytes(address + fileSizeChange);
for (int i = 0; i < patchedAddressBytes.Length; i++)
{
fileBytes[startIndex + i] = patchedAddressBytes[i];
}
}
}

private void BackupFile(string path)
{
File.Copy(path, path + BackupSuffix, true);
}

public void RestoreFromBackup()
{
var backupPath = _filePath + BackupSuffix;
if (File.Exists(backupPath))
{
File.Copy(backupPath, _filePath, true);
File.Delete(backupPath);
}
}
}
}
5 changes: 1 addition & 4 deletions OWML.Patcher/OWML.Patcher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="BsPatch, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>VR\BsPatch.dll</HintPath>
</Reference>
<Reference Include="dnlib, Version=1.6.0.0, Culture=neutral, PublicKeyToken=50e96378b6e77999, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>dnpatch\dnlib.dll</HintPath>
Expand All @@ -57,6 +53,7 @@
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="BinaryPatcher.cs" />
<Compile Include="OWPatcher.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="VRPatcher.cs" />
Expand Down
Binary file not shown.
Binary file removed OWML.Patcher/VR/BsPatch.dll
Binary file not shown.
Binary file not shown.
Binary file not shown.
102 changes: 5 additions & 97 deletions OWML.Patcher/VrPatcher.cs
Original file line number Diff line number Diff line change
@@ -1,129 +1,37 @@
using System;
using OWML.Common;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using BsDiff;
using OWML.Common;

namespace OWML.Patcher
{
public class VRPatcher
{
private readonly IOwmlConfig _owmlConfig;
private readonly IModConsole _writer;
private readonly SHA256 _sha;
private readonly BinaryPatcher _binaryPatcher;

private static readonly string[] PluginFilenames = { "openvr_api.dll", "OVRPlugin.dll" };
private static readonly string[] PatchChecksums =
{
"cacc71fcb141d936f1b59e57bf10dc52e8edb3481988379f7d95ecb65c4d3c90",
"d3979abb3b0d2468c3e03e2ee862d5297f5885bd9fc8f3b16cb16805e010d097",
"7ed2c835ec6653009d29b6b7fa9dc36cd754f64b2f359f7ca635ec6cd4ad8c32"
};

public VRPatcher(IOwmlConfig owmlConfig, IModConsole writer)
{
_owmlConfig = owmlConfig;
_writer = writer;
_sha = SHA256.Create();
_binaryPatcher = new BinaryPatcher(_owmlConfig, _writer);
}

public void PatchVR(bool enableVR)
{
PatchGlobalManager(enableVR);
if (enableVR)
{
_binaryPatcher.Patch();
AddPluginFiles();
}
else
{
_binaryPatcher.RestoreFromBackup();
RemovePluginFiles();
}
}

private void PatchGlobalManager(bool enableVR)
{
var currentPath = _owmlConfig.DataPath + "/globalgamemanagers";
if (!File.Exists(currentPath))
{
_writer.WriteLine("Error: can't find " + currentPath);
return;
}

var currentChecksum = CalculateChecksum(currentPath);
_writer.WriteLine("Current checksum: " + currentChecksum);

var backupPath = $"{_owmlConfig.DataPath}/globalgamemanagers.{currentChecksum}.bak";
if (!File.Exists(backupPath))
{
_writer.WriteLine("Taking backup of globalgamemanagers.");
File.Copy(currentPath, backupPath, true);
}

var vrPath = $"{_owmlConfig.DataPath}/globalgamemanagers.{currentChecksum}.vr";
if (enableVR && !File.Exists(vrPath))
{
_writer.WriteLine("Patching globalgamemanagers for VR...");
if (PatchChecksums.Contains(currentChecksum))
{
var patchPath = $"{_owmlConfig.OWMLPath}VR/{currentChecksum}";
ApplyPatch(currentPath, vrPath, patchPath);
}
else
{
var patchedChecksum = PatchChecksums.FirstOrDefault(checksum =>
CalculateChecksum($"{_owmlConfig.DataPath}/globalgamemanagers.{checksum}.vr") == currentChecksum);
if (!string.IsNullOrEmpty(patchedChecksum))
{
_writer.WriteLine("Already patched! Original checksum: " + patchedChecksum);
vrPath = $"{_owmlConfig.DataPath}/globalgamemanagers.{patchedChecksum}.vr";
}
else
{
_writer.WriteLine($"Error: invalid checksum: {currentChecksum}. " +
"VR patch for this version of Outer Wilds is not yet supported by OWML.");
return;
}
}
}

var copyFrom = enableVR ? vrPath : backupPath;
File.Copy(copyFrom, currentPath, true);
}

private string CalculateChecksum(string filePath)
{
if (!File.Exists(filePath))
{
return null;
}
var bytes = File.ReadAllBytes(filePath);
var hash = _sha.ComputeHash(bytes);
var sb = new StringBuilder();
foreach (var b in hash)
{
sb.Append(b.ToString("x2").ToLower());
}
return sb.ToString();
}

private void ApplyPatch(string oldFile, string newFile, string patchFile)
{
try
{
using (var input = new FileStream(oldFile, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var output = new FileStream(newFile, FileMode.Create))
{
BinaryPatchUtility.Apply(input, () => new FileStream(patchFile, FileMode.Open, FileAccess.Read, FileShare.Read), output);
}
}
catch (Exception ex)
{
_writer.WriteLine("Error while patching VR: " + ex);
}
}

private void AddPluginFiles()
{
foreach (var filename in PluginFilenames)
Expand Down
5 changes: 2 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ More info about config can be found [here](https://github.com/amazingalek/owml/w

## Compatibility

* Tested with all versions of Outer Wilds up to and including v1.0.6.
* Tested with all versions of Outer Wilds up to and including v1.0.7.
* Currently Windows only.

## Feedback
Expand All @@ -120,7 +120,7 @@ Special thanks to:
* [Outer Wilds](http://www.outerwilds.com)
* [Outer Wilds on Discord](https://discord.gg/csKYR3w)
* [Outer Wilds on Reddit](https://www.reddit.com/r/outerwilds)
* Inspired by [SMAPI](https://smapi.io)
* Inspired by (and some code from) [SMAPI](https://smapi.io)
* Texture_Turtle for graphics on [Nexus page](https://www.nexusmods.com/outerwilds/mods/1)

Dependencies:
Expand All @@ -130,5 +130,4 @@ Dependencies:
* [Json.Net.Unity3D](https://github.com/SaladLab/Json.Net.Unity3D)
* [ObjImporter](https://wiki.unity3d.com/index.php?title=ObjImporter)
* [NAudio-Unity](https://github.com/WulfMarius/NAudio-Unity)
* [BsDiff](https://github.com/LogosBible/bsdiff.net)
* [Gameloop.Vdf](https://github.com/shravan2x/Gameloop.Vdf)

0 comments on commit 0e82fb3

Please sign in to comment.