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

Functional RPCS3 patch file creation #40

Merged
merged 2 commits into from
Nov 11, 2023
Merged
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
76 changes: 72 additions & 4 deletions Refresher/Patching/EbootPatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Text;
using System.Text.RegularExpressions;
using ELFSharp.ELF;
using ELFSharp.ELF.Sections;
using Refresher.Verification;

namespace Refresher.Patching;
Expand All @@ -12,6 +11,12 @@ public partial class EbootPatcher : IPatcher
{
private readonly Lazy<List<PatchTargetInfo>> _targets;

public bool GenerateRpcs3Patch = false;
public string? Rpcs3PatchFolder = null;
public string? PpuHash = null;
public string? GameVersion = null;
public string? GameName;

public EbootPatcher(Stream stream)
{
if (!stream.CanRead || !stream.CanSeek || !stream.CanWrite)
Expand Down Expand Up @@ -172,7 +177,6 @@ private static void FindLbpkDomains(BinaryReader reader, List<long> foundLbpkPos

private static void FilterValidUrls(BinaryReader reader, List<long> foundPossibleUrlPositions, List<PatchTargetInfo> foundItems)
{

foreach (long foundPosition in foundPossibleUrlPositions)
{
bool tooLong = false;
Expand Down Expand Up @@ -330,9 +334,73 @@ public List<Message> Verify(string url, bool patchDigest)

public void Patch(string url, bool patchDigest)
{
using BinaryWriter writer = new(this.Stream);
if (this.GenerateRpcs3Patch)
{
Debug.Assert(this.Rpcs3PatchFolder != null);
Debug.Assert(this.PpuHash != null);
Debug.Assert(this.GameName != null);
Debug.Assert(this.GameVersion != null);

string patchesFile = Path.Combine(this.Rpcs3PatchFolder, "imported_patch.yml");

if (!File.Exists(patchesFile))
//Write the header to the patches file
File.WriteAllText(patchesFile, "Version: 1.2\n\n");

string template = $"""
PPU-{this.PpuHash}:
"Refresher Patch ({url})":
Games:
"{this.GameName}":
NPUA80662: [ {this.GameVersion} ]
Author: "Refresher (automated)"
Notes: "This patches the game to connect to {url}"
Patch Version:
Patch:
""";
string strPatchTemplate = """
- [ utf8, 0x{0}, "{1}" ]
""";

StringBuilder finalPatch = new();
finalPatch.AppendLine("");
finalPatch.AppendLine(template);

foreach (PatchTargetInfo patchTargetInfo in this._targets.Value)
{
//The offset that an ELF is loaded into memory
const long elfLoadOffset = 0x10000;
//Offset the file offset by the offset the ELF is loaded into memory,
//this is due to RPCS3 patches working in memory space rather than file space
long fileOffset = patchTargetInfo.Offset + elfLoadOffset;

switch (patchTargetInfo.Type)
{
case PatchTargetType.Url:
finalPatch.AppendLine(string.Format(strPatchTemplate, fileOffset.ToString("x8"), $"{url}\\0"));
break;
case PatchTargetType.Host:
finalPatch.AppendLine(string.Format(strPatchTemplate, fileOffset.ToString("x8"), $"{new Uri(url).Host}\\0"));
break;
case PatchTargetType.Digest:
//If we shouldn't patch digests, skip writing this
if (!patchDigest) break;

finalPatch.AppendLine(string.Format(strPatchTemplate, fileOffset.ToString("x8"), "CustomServerDigest"));
break;
default:
throw new ArgumentOutOfRangeException();
}
}

File.AppendAllText(patchesFile, finalPatch.ToString());
}
else
{
using BinaryWriter writer = new(this.Stream);

PatchFromInfoList(writer, this._targets.Value, url, patchDigest);
PatchFromInfoList(writer, this._targets.Value, url, patchDigest);
}
}

private static void PatchFromInfoList(BinaryWriter writer, List<PatchTargetInfo> targets, string url, bool patchDigest)
Expand Down
4 changes: 2 additions & 2 deletions Refresher/UI/ConsolePatchForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ private void DisposePatchAccessor()
disposable.Dispose();
}

protected override TableRow AddRemoteField()
protected override IEnumerable<TableRow> AddFields()
{
return AddField("PS3's IP", out this._remoteAddress, new Button(this.PathChanged) { Text = "Connect" });
return new[] { AddField("PS3's IP", out this._remoteAddress, new Button(this.PathChanged) { Text = "Connect" }) };
}

public override void Guide(object? sender, EventArgs e)
Expand Down
62 changes: 62 additions & 0 deletions Refresher/UI/EmulatorFilePatchForm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Diagnostics;
using Eto;
using Eto.Forms;
using Refresher.Accessors;
using Refresher.UI.Items;

namespace Refresher.UI;

public class EmulatorFilePatchForm : IntegratedPatchForm
{
private FilePicker _folderField = null!;

public EmulatorFilePatchForm() : base("RPCS3 Patch")
{
this._folderField.FileAction = FileAction.SelectFolder;
this._folderField.FilePathChanged += this.PathChanged;

// RPCS3 builds for Windows are portable
// TODO: Cache the last used location for easier entry
if (!OperatingSystem.IsWindows())
{
// ~/.config/rpcs3/dev_hdd0
string folder = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "rpcs3", "dev_hdd0");
if (Directory.Exists(folder))
{
this._folderField.FilePath = folder;
this.PathChanged(this, EventArgs.Empty);
this.LogMessage("RPCS3's path has been detected automatically! You do not need to change the path.");
}
}
}

protected override void BeforePatch(object? sender, EventArgs e)
{
if (this.Patcher != null)
{
this.Patcher.GenerateRpcs3Patch = false;
}
}

public override void Guide(object? sender, EventArgs e)
{
this.OpenUrl("https://docs.littlebigrefresh.com/patching/rpcs3");
}

protected override void PathChanged(object? sender, EventArgs ev)
{
this.Accessor = new EmulatorPatchAccessor(this._folderField.FilePath);
base.PathChanged(sender, ev);
}

protected override IEnumerable<TableRow> AddFields()
{
return new[]
{
AddField("RPCS3 dev_hdd0 folder", out this._folderField),
};
}

protected override bool NeedsResign => false;
protected override bool ShouldReplaceExecutable => false;
}
40 changes: 36 additions & 4 deletions Refresher/UI/EmulatorPatchForm.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
using System.Diagnostics;
using Eto;
using Eto.Forms;
using Refresher.Accessors;
using Refresher.UI.Items;

namespace Refresher.UI;

public class EmulatorPatchForm : IntegratedPatchForm
{
private FilePicker _folderField = null!;

private TextBox _ppuHash = null!;
private TextBox _gameVersion = null!;
private Button _filePatchButton = null!;

public EmulatorPatchForm() : base("RPCS3 Patch")
{
this._folderField.FileAction = FileAction.SelectFolder;
this._folderField.FilePathChanged += this.PathChanged;

this._filePatchButton.Text = "Go to RPCS3 file patching menu (Advanced)";
this._filePatchButton.Click += (_, _) => this.ShowChild<EmulatorFilePatchForm>();

// RPCS3 builds for Windows are portable
// TODO: Cache the last used location for easier entry
if (!OperatingSystem.IsWindows())
Expand All @@ -28,6 +36,23 @@ public EmulatorPatchForm() : base("RPCS3 Patch")
}
}

protected override void BeforePatch(object? sender, EventArgs e)
{
if (this.Patcher != null)
{
this.Patcher.GenerateRpcs3Patch = true;
this.Patcher.PpuHash = this._ppuHash.Text;
this.Patcher.GameVersion = this._gameVersion.Text;
this.Patcher.Rpcs3PatchFolder = Path.Combine(this._folderField.FilePath, "../patches");
this.Patcher.GameName = ((GameItem)this.GameDropdown.SelectedValue).Text;
}
}

public override void CompletePatch(object? sender, EventArgs e)
{
MessageBox.Show(this, "Successfully saved the patch to RPCS3! Open the patch manager and search for the game you patched.", "Success!");
}

public override void Guide(object? sender, EventArgs e)
{
this.OpenUrl("https://docs.littlebigrefresh.com/patching/rpcs3");
Expand All @@ -39,11 +64,18 @@ protected override void PathChanged(object? sender, EventArgs ev)
base.PathChanged(sender, ev);
}

protected override TableRow AddRemoteField()
protected override IEnumerable<TableRow> AddFields()
{
return AddField("RPCS3 dev_hdd0 folder", out this._folderField);
return new[]
{
AddField("Game PPU hash", out this._ppuHash),
AddField("Game Version", out this._gameVersion),
AddField("RPCS3 dev_hdd0 folder", out this._folderField),
AddField("", out this._filePatchButton),
};
}

protected override bool NeedsResign => false;
protected override bool ShouldReplaceExecutable => false;
protected override bool ShouldReplaceExecutable => true;
protected override bool ShowRevertEbootButton => false;
}
35 changes: 17 additions & 18 deletions Refresher/UI/IntegratedPatchForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

public abstract class IntegratedPatchForm : PatchForm<EbootPatcher>
{
private readonly DropDown _gameDropdown;
private readonly TextBox? _outputField;
protected readonly DropDown GameDropdown;
protected readonly TextBox? OutputField;

private string _tempFile;
private string _usrDir;
Expand All @@ -23,21 +23,19 @@
protected override TableLayout FormPanel { get; }

[SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")]
protected IntegratedPatchForm(string subtitle) : base(subtitle)

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (ubuntu-latest)

Non-nullable field '_tempFile' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (ubuntu-latest)

Non-nullable field '_usrDir' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (windows-latest)

Non-nullable field '_tempFile' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (windows-latest)

Non-nullable field '_usrDir' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (ubuntu-latest)

Non-nullable field '_tempFile' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (ubuntu-latest)

Non-nullable field '_usrDir' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (windows-latest)

Non-nullable field '_tempFile' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

Check warning on line 26 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (windows-latest)

Non-nullable field '_usrDir' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.
{
List<TableRow> rows = new()
{
this.AddRemoteField(),
AddField("Game to patch", out this._gameDropdown, forceHeight: 56),
AddField("Server URL", out this.UrlField),
};

this._gameDropdown.SelectedValueChanged += this.GameChanged;
List<TableRow> rows = new();
rows.AddRange(this.AddFields());
rows.Add(AddField("Game to patch", out this.GameDropdown, forceHeight: 56));
rows.Add(AddField("Server URL", out this.UrlField));

this.GameDropdown.SelectedValueChanged += this.GameChanged;

if (!this.ShouldReplaceExecutable)
{
rows.Add(AddField("Identifier (EBOOT.<value>.elf)", out this._outputField));
this._outputField!.PlaceholderText = "refresh";
rows.Add(AddField("Identifier (EBOOT.<value>.elf)", out this.OutputField));

Check warning on line 37 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (ubuntu-latest)

The type 'Eto.Forms.TextBox?' cannot be used as type parameter 'TControl' in the generic type or method 'PatchForm<EbootPatcher>.AddField<TControl>(string, out TControl, Button?, int)'. Nullability of type argument 'Eto.Forms.TextBox?' doesn't match constraint type 'Eto.Forms.Control'.

Check warning on line 37 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (windows-latest)

The type 'Eto.Forms.TextBox?' cannot be used as type parameter 'TControl' in the generic type or method 'PatchForm<EbootPatcher>.AddField<TControl>(string, out TControl, Button?, int)'. Nullability of type argument 'Eto.Forms.TextBox?' doesn't match constraint type 'Eto.Forms.Control'.

Check warning on line 37 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (ubuntu-latest)

The type 'Eto.Forms.TextBox?' cannot be used as type parameter 'TControl' in the generic type or method 'PatchForm<EbootPatcher>.AddField<TControl>(string, out TControl, Button?, int)'. Nullability of type argument 'Eto.Forms.TextBox?' doesn't match constraint type 'Eto.Forms.Control'.

Check warning on line 37 in Refresher/UI/IntegratedPatchForm.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds (windows-latest)

The type 'Eto.Forms.TextBox?' cannot be used as type parameter 'TControl' in the generic type or method 'PatchForm<EbootPatcher>.AddField<TControl>(string, out TControl, Button?, int)'. Nullability of type argument 'Eto.Forms.TextBox?' doesn't match constraint type 'Eto.Forms.Control'.
this.OutputField!.PlaceholderText = "refresh";
}

this.FormPanel = new TableLayout(rows);
Expand All @@ -48,7 +46,7 @@
protected virtual void PathChanged(object? sender, EventArgs ev)
{
Debug.Assert(this.Accessor != null);
this._gameDropdown.Items.Clear();
this.GameDropdown.Items.Clear();

if (!this.Accessor.DirectoryExists("game")) return;

Expand Down Expand Up @@ -92,15 +90,15 @@

item.TitleId = game;

this._gameDropdown.Items.Add(item);
this.GameDropdown.Items.Add(item);
}
}

protected virtual void GameChanged(object? sender, EventArgs ev)
{
LibSceToolSharp.Init();

GameItem? game = this._gameDropdown.SelectedValue as GameItem;
GameItem? game = this.GameDropdown.SelectedValue as GameItem;
Debug.Assert(game != null);
Debug.Assert(this.Accessor != null);

Expand Down Expand Up @@ -156,7 +154,7 @@
public override void CompletePatch(object? sender, EventArgs e) {
Debug.Assert(this.Accessor != null);

string? identifier = string.IsNullOrWhiteSpace(this._outputField?.Text) ? this._outputField?.PlaceholderText : this._outputField?.Text;
string? identifier = string.IsNullOrWhiteSpace(this.OutputField?.Text) ? this.OutputField?.PlaceholderText : this.OutputField?.Text;
identifier ??= "";

string fileToUpload;
Expand Down Expand Up @@ -200,7 +198,7 @@

public override IEnumerable<Button> AddExtraButtons()
{
if (this.ShouldReplaceExecutable)
if (this.ShouldReplaceExecutable && this.ShowRevertEbootButton)
{
yield return new Button(this.RevertToOriginalExecutable) { Text = "Revert EBOOT" };
}
Expand All @@ -227,7 +225,7 @@
this.GameChanged(this, EventArgs.Empty);
}

protected abstract TableRow AddRemoteField();
protected abstract IEnumerable<TableRow> AddFields();
/// <summary>
/// Whether the target platform requires the executable to be resigned or not
/// </summary>
Expand All @@ -236,4 +234,5 @@
/// Whether the target platform requires the executable to be named <c>EBOOT.BIN</c>
/// </summary>
protected abstract bool ShouldReplaceExecutable { get; }
protected virtual bool ShowRevertEbootButton => true;
}
4 changes: 4 additions & 0 deletions Refresher/UI/PatchForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ private void Patch(object? sender, EventArgs e)
if (!this._patchButton.Enabled) return; // shouldn't happen ever but just in-case
if (this.Patcher == null) return;

this.BeforePatch(sender, e);

if (!this._usedAutoDiscover)
{
DialogResult result = MessageBox.Show("You didn't use AutoDiscover. Would you like to try to run it now?", MessageBoxButtons.YesNoCancel, MessageBoxType.Question);
Expand All @@ -219,6 +221,8 @@ private void Patch(object? sender, EventArgs e)

this.CompletePatch(sender, e);
}

protected virtual void BeforePatch(object? sender, EventArgs e) {}

protected void FailVerify(string reason, Exception? e = null, bool clear = true)
{
Expand Down
Loading