Skip to content

Commit

Permalink
Binary patcher generalization (#287)
Browse files Browse the repository at this point in the history
* Generalized BinaryPatcher

* moved VR stuff to separate class
  • Loading branch information
TAImatem authored Jul 30, 2020
1 parent d6796a8 commit b7f83cc
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 101 deletions.
190 changes: 93 additions & 97 deletions OWML.Patcher/BinaryPatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System;
using System.IO;
using System.Linq;
using System.Text;

namespace OWML.Patcher
{
Expand All @@ -11,152 +10,149 @@ public class BinaryPatcher
private readonly IOwmlConfig _owmlConfig;
private readonly IModConsole _writer;

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

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;
private const int FirstAddressIndex = 0x204;
private const int AddressStructureSize = 0x14;
private const int SizeOffset = 0x4;
private const int SectorCount = 20;

public BinaryPatcher(IOwmlConfig owmlConfig, IModConsole writer)
{
_owmlConfig = owmlConfig;
_writer = writer;
}

public void Patch()
public (int sectorStart, int sectorSize) GetSectorInfo(byte[] fileBytes, int sector)
{
if (sector < 0 || sector >= SectorCount)
{
return (-1, -1);
}
var sectorIndex = FirstAddressIndex + sector * AddressStructureSize;
var sectorStart = BitConverter.ToInt32(fileBytes, sectorIndex) + BlockAddressOffset;
var sectorSize = BitConverter.ToInt32(fileBytes, sectorIndex + SizeOffset);
return (sectorStart, sectorSize);
}

public byte[] ReadFileBytes()
{
var filePath = $"{_owmlConfig.DataPath}/{FileName}";
if (!File.Exists(filePath))
{
throw new FileNotFoundException(filePath);
}

var fileBytes = File.ReadAllBytes(filePath);
return File.ReadAllBytes(filePath);
}

var buildSettingsStartIndex = BitConverter.ToInt32(fileBytes, BuildSettingsStartAddressIndex) + BlockAddressOffset;
var buildSettingsSize = BitConverter.ToInt32(fileBytes, BuildSettingsSizeIndex);
var buildSettingsEndIndex = buildSettingsStartIndex + buildSettingsSize;
public void WriteFileBytes(byte[] fileBytes)
{
var filePath = $"{_owmlConfig.DataPath}/{FileName}";
if (!File.Exists(filePath))
{
throw new FileNotFoundException(filePath);
}

var patchStartIndex = FindPatchStartIndex(fileBytes, buildSettingsStartIndex, buildSettingsEndIndex);
var isAlreadyPatched = FindExistingPatch(fileBytes, patchStartIndex, buildSettingsEndIndex);
BackupFile(filePath);
File.WriteAllBytes(filePath, fileBytes);
}

if (isAlreadyPatched)
public byte[] GetSectorBytes(byte[] fileBytes, int sector)
{
if (sector < 0 || sector >= SectorCount)
{
_writer.WriteLine("globalgamemanagers already patched.");
return;
return new byte[0];
}

BackupFile(filePath);
var patchedBytes = CreatePatchedFileBytes(fileBytes, patchStartIndex);
File.WriteAllBytes(filePath, patchedBytes);
_writer.WriteLine("Successfully patched globalgamemanagers.", MessageType.Success);
var (sectorStart, sectorSize) = GetSectorInfo(fileBytes, sector);
var sectorEnd = sectorStart + sectorSize;
return fileBytes.Take(sectorEnd).Skip(sectorStart).ToArray();
}

private int FindPatchStartIndex(byte[] fileBytes, int startIndex, int endIndex)
public byte[] PatchSectionBytes(byte[] fileBytes, byte[] newBytes, int sectionStart, int sectionOriginalSize, int sector = -1)
{
var patchZoneBytes = Encoding.ASCII.GetBytes(PatchZoneText);
var patchZoneMatch = 0;
for (var i = startIndex; i < endIndex; i++)
if (sector == -1)
{
var fileByte = fileBytes[i];
var patchZoneByte = patchZoneBytes[patchZoneMatch];
if (fileByte == patchZoneByte)
{
patchZoneMatch++;
}
else
{
patchZoneMatch = 0;
}
if (patchZoneMatch == patchZoneBytes.Length)
sector = GetSectorIndex(fileBytes, sectionStart);
}
if (sector < 0 || sector >= SectorCount)
{
if (sector == -1)
{
return i + PatchStartZoneOffset;
_writer.WriteLine("Error - Patching header is not supported", MessageType.Error);
}
return fileBytes;
}
throw new Exception("Could not find patch zone in globalgamemanagers. This probably means the VR patch needs to be updated.");

var sectionEnd = sectionStart + sectionOriginalSize;

var sizeDifference = newBytes.Length - sectionOriginalSize;

var newFileBytes = fileBytes.Take(sectionStart).Concat(newBytes).Concat(fileBytes.Skip(sectionEnd)).ToArray();

PatchFileSize(newFileBytes, sizeDifference, sector);

return newFileBytes;
}

private bool FindExistingPatch(byte[] fileBytes, int startIndex, int endIndex)
public byte[] PatchSectorBytes(byte[] fileBytes, byte[] newBytes, int sector)
{
var existingPatchBytes = Encoding.ASCII.GetBytes(EnabledVRDevice);
var existingPatchMatch = 0;
if (sector < 0 || sector >= SectorCount)
{
return fileBytes;
}

var (sectorStart, sectorSize) = GetSectorInfo(fileBytes, sector);
return PatchSectionBytes(fileBytes, newBytes, sectorStart, sectorSize, sector);
}

for (var i = startIndex; i < endIndex; i++)
private int GetSectorIndex(byte[] fileBytes, int address)
{
var sector = -1;
for (var sectorIndex = 0; sectorIndex < SectorCount; sectorIndex++)
{
var fileByte = fileBytes[i];
var existingPatchByte = existingPatchBytes[existingPatchMatch];
if (fileByte == existingPatchByte)
{
existingPatchMatch++;
}
else
var sectorStart = GetSectorInfo(fileBytes, sectorIndex).sectorStart;
if (sectorStart > address)
{
existingPatchMatch = 0;
}
if (existingPatchMatch == existingPatchBytes.Length)
{
return true;
return sector;
}
sector = sectorIndex;
}
return false;
return sector;
}

private byte[] CreatePatchedFileBytes(byte[] fileBytes, int patchStartIndex)
private void ShiftInt(byte[] fileBytes, int location, int difference, bool isBigEndian = false)
{
// 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)).ToList();
var value = isBigEndian ?
BitConverter.ToInt32(fileBytes.Take(location + 4).Reverse().ToArray(), 0) :
BitConverter.ToInt32(fileBytes, location);

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);
var increasedValueBytes = BitConverter.GetBytes(value + difference);
if (isBigEndian)
{
increasedValueBytes = increasedValueBytes.Reverse().ToArray();
}

return originalFirstPart
.Concat(patchBytes)
.Concat(originalSecondPart)
.ToArray();
increasedValueBytes.CopyTo(fileBytes, location);
}

private void PatchFileSize(byte[] fileBytes, int patchSize)
private void PatchFileSize(byte[] fileBytes, int patchSize, int patchedSector)
{
// 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);
// Shift file size.
ShiftInt(fileBytes, FileSizeStartIndex, patchSize, true);

// Generate bytes for new patched file.
var fileSizeChange = patchSize - RemovedBytes;
var patchedFileSize = originalFileSize + fileSizeChange;
var patchedFileSizeBytes = BitConverter.GetBytes(patchedFileSize).Reverse().ToArray();
// Shift sector size.
var sizeIndex = FirstAddressIndex + patchedSector * AddressStructureSize + SizeOffset;
ShiftInt(fileBytes, sizeIndex, patchSize);

// Overwrite original file size bytes with patched size.
for (var i = 0; i < patchedFileSizeBytes.Length; i++)
// Shift conscutive sector addresses.
for (var sector = patchedSector + 1; sector < SectorCount; sector++)
{
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 (var i = 0; i < patchedAddressBytes.Length; i++)
{
fileBytes[startIndex + i] = patchedAddressBytes[i];
}
var startIndex = FirstAddressIndex + sector * AddressStructureSize;
ShiftInt(fileBytes, startIndex, patchSize);
}
}

Expand Down
1 change: 1 addition & 0 deletions OWML.Patcher/OWML.Patcher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="VRFilePatcher.cs" />
<Compile Include="BinaryPatcher.cs" />
<Compile Include="OWPatcher.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand Down
106 changes: 106 additions & 0 deletions OWML.Patcher/VRFilePatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using OWML.Common;
using System;
using System.Linq;
using System.Text;

namespace OWML.Patcher
{
public class VRFilePatcher
{
private readonly IModConsole _writer;
private readonly BinaryPatcher _binaryPatcher;

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 BuildSettingsSector = 10;//count from zero

public VRFilePatcher(IModConsole writer, BinaryPatcher binaryPatcher)
{
_writer = writer;
_binaryPatcher = binaryPatcher;
}

public void Patch()
{
var fileBytes = _binaryPatcher.ReadFileBytes();
var buildSettingsStartIndex = _binaryPatcher.GetSectorInfo(fileBytes, BuildSettingsSector).sectorStart;

var buildSettingsBytes = _binaryPatcher.GetSectorBytes(fileBytes, BuildSettingsSector);
var patchStartOffset = FindPatchStartOffset(buildSettingsBytes);
var isAlreadyPatched = FindExistingPatch(buildSettingsBytes, patchStartOffset);

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

var patchedBytes = CreatePatchFileBytes(fileBytes, buildSettingsStartIndex + patchStartOffset);
_binaryPatcher.WriteFileBytes(patchedBytes);
_writer.WriteLine("Successfully patched globalgamemanagers.", MessageType.Success);
}

private int FindPatchStartOffset(byte[] sectorBytes)
{
var patchZoneBytes = Encoding.ASCII.GetBytes(PatchZoneText);
var patchZoneMatch = 0;
for (var i = 0; i < sectorBytes.Length; i++)
{
var fileByte = sectorBytes[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[] sectorBytes, int startIndex)
{
var existingPatchBytes = Encoding.ASCII.GetBytes(EnabledVRDevice);
var existingPatchMatch = 0;

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

private byte[] CreatePatchFileBytes(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)).ToArray();

return _binaryPatcher.PatchSectionBytes(fileBytes, patchBytes, patchStartIndex, RemovedBytes, BuildSettingsSector);
}
}
}
8 changes: 4 additions & 4 deletions OWML.Patcher/VrPatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ namespace OWML.Patcher
public class VRPatcher
{
private readonly IOwmlConfig _owmlConfig;
private readonly IModConsole _writer;
private readonly BinaryPatcher _binaryPatcher;
private readonly VRFilePatcher _vrPatcher;

private static readonly string[] PluginFilenames = { "openvr_api.dll", "OVRPlugin.dll" };

public VRPatcher(IOwmlConfig owmlConfig, IModConsole writer)
{
_owmlConfig = owmlConfig;
_writer = writer;
_binaryPatcher = new BinaryPatcher(_owmlConfig, _writer);
_binaryPatcher = new BinaryPatcher(_owmlConfig, writer);
_vrPatcher = new VRFilePatcher(writer, _binaryPatcher);
}

public void PatchVR(bool enableVR)
{
if (enableVR)
{
_binaryPatcher.Patch();
_vrPatcher.Patch();
AddPluginFiles();
}
else
Expand Down

0 comments on commit b7f83cc

Please sign in to comment.