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

RPCS3 Pipelines #72

Merged
merged 21 commits into from
Oct 14, 2024
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
26 changes: 26 additions & 0 deletions Refresher.Core/Accessors/PatchAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Diagnostics.Contracts;
using System.Reflection;

namespace Refresher.Core.Accessors;

public abstract class PatchAccessor
Expand Down Expand Up @@ -38,4 +41,27 @@ public virtual void CopyFile(string inPath, string outPath)

inStream.CopyTo(outStream);
}

public static void Try(Action action)
{
try
{
action();
}
catch (TargetInvocationException targetInvocationException)
{
CatchAccessorException(targetInvocationException.InnerException!);
throw;
}
catch (Exception ex)
{
CatchAccessorException(ex);
throw;
}
}

private static void CatchAccessorException(Exception ex)
{
State.Logger.LogError(Accessor, ex.ToString());
}
}
5 changes: 4 additions & 1 deletion Refresher.Core/LogType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ public enum LogType : byte
PPU,
Verify,
CLI,
PatchAccessor,
Accessor,
PatchForm,
IntegratedPatchForm,
InfoRetrieval,
Crypto,
IDPS,
OSIntegration,
AutoDiscover,
PS3,
PSP,
Vita,
RPCS3,
Pipeline
}
2 changes: 1 addition & 1 deletion Refresher.Core/Patching/EbootPatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public partial class EbootPatcher : IPatcher
{
private readonly Lazy<List<PatchTargetInfo>> _targets;
private readonly Lazy<string?> _ppuHash = null;

Check warning on line 17 in Refresher.Core/Patching/EbootPatcher.cs

View workflow job for this annotation

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

Cannot convert null literal to non-nullable reference type.

Check warning on line 17 in Refresher.Core/Patching/EbootPatcher.cs

View workflow job for this annotation

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

Cannot convert null literal to non-nullable reference type.

Check warning on line 17 in Refresher.Core/Patching/EbootPatcher.cs

View workflow job for this annotation

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

Cannot convert null literal to non-nullable reference type.

Check warning on line 17 in Refresher.Core/Patching/EbootPatcher.cs

View workflow job for this annotation

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

Cannot convert null literal to non-nullable reference type.

Check warning on line 17 in Refresher.Core/Patching/EbootPatcher.cs

View workflow job for this annotation

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

Cannot convert null literal to non-nullable reference type.

Check warning on line 17 in Refresher.Core/Patching/EbootPatcher.cs

View workflow job for this annotation

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

Cannot convert null literal to non-nullable reference type.

public bool GenerateRpcs3Patch = false;
public string? Rpcs3PatchFolder = null;
Expand Down Expand Up @@ -436,7 +436,7 @@
PPU-{this._ppuHash}:
"Refresher Patch ({url})":
Games:
"{this.GameName}":
"{this.GameName.ReplaceLineEndings(string.Empty)}":
{this.TitleId}: [ {this.GameVersion} ]
Author: "Refresher (automated)"
Notes: "This patches the game to connect to {url}"
Expand Down
7 changes: 7 additions & 0 deletions Refresher.Core/Patching/EncryptionDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Refresher.Core.Patching;

public class EncryptionDetails
{
public string? LicenseDirectory { get; internal set; }
public string? DownloadedActDatPath { get; internal set; }
}
18 changes: 18 additions & 0 deletions Refresher.Core/Patching/GameInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Refresher.Core.Patching;

public class GameInformation
{
public string TitleId { get; set; } = null!;

public string? Name { get; set; }
public string? ContentId { get; set; }
public string? Version { get; set; }

public string? DownloadedEbootPath { get; set; }
public string? DecryptedEbootPath { get; internal set; }

public override string ToString()
{
return $"[{this.TitleId}] name: {this.Name}, contentId: {this.ContentId}, version: {this.Version}";
}
}
38 changes: 38 additions & 0 deletions Refresher.Core/Pipelines/CommonStepInputs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Refresher.Core.Pipelines;

internal static class CommonStepInputs
{
internal static readonly StepInput TitleId = new("title-id", "Game", StepInputType.Game)
{
Placeholder = "NPUA80662",
};

internal static readonly StepInput ServerUrl = new("url", "Server URL")
{
Placeholder = "https://lbp.littlebigrefresh.com",
};

internal static readonly StepInput RPCS3Folder = new("hdd0-path", "RPCS3 dev_hdd0 folder", StepInputType.Directory)
{
// provide an example to Windows users.
// don't bother with other platforms because they should be automatic
Placeholder = @$"C:\Users\{Environment.UserName}\RPCS3\dev_hdd0",
DetermineDefaultValue = DetermineDefaultRpcs3Path,
};

// TODO: Cache the last used location for easier entry
private static string? DetermineDefaultRpcs3Path()
{
// RPCS3 builds for Windows are portable, so we can't determine this automatically
if (OperatingSystem.IsWindows())
return null;

// ~/.config/rpcs3/dev_hdd0
string folder = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "rpcs3", "dev_hdd0");

if (Directory.Exists(folder))
return folder;

return null;
}
}
12 changes: 10 additions & 2 deletions Refresher.Core/Pipelines/Pipeline.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using System.Diagnostics;

using Refresher.Core.Accessors;
using Refresher.Core.Patching;
using GlobalState = Refresher.Core.State;

namespace Refresher.Core.Pipelines;
Expand All @@ -10,9 +11,14 @@
public abstract string Id { get; }
public abstract string Name { get; }

public Dictionary<string, string> Inputs = [];
public readonly Dictionary<string, string> Inputs = [];
public FrozenSet<StepInput> RequiredInputs { get; private set; }

Check warning on line 15 in Refresher.Core/Pipelines/Pipeline.cs

View workflow job for this annotation

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

Non-nullable property 'RequiredInputs' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in Refresher.Core/Pipelines/Pipeline.cs

View workflow job for this annotation

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

Non-nullable property 'RequiredInputs' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in Refresher.Core/Pipelines/Pipeline.cs

View workflow job for this annotation

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

Non-nullable property 'RequiredInputs' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in Refresher.Core/Pipelines/Pipeline.cs

View workflow job for this annotation

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

Non-nullable property 'RequiredInputs' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in Refresher.Core/Pipelines/Pipeline.cs

View workflow job for this annotation

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

Non-nullable property 'RequiredInputs' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 15 in Refresher.Core/Pipelines/Pipeline.cs

View workflow job for this annotation

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

Non-nullable property 'RequiredInputs' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

public IPatcher? Patcher { get; internal set; }
public PatchAccessor? Accessor { get; internal set; }
public GameInformation? GameInformation { get; internal set; }
public EncryptionDetails? EncryptionDetails { get; internal set; }

public PipelineState State { get; private set; } = PipelineState.NotStarted;

public float Progress
Expand Down Expand Up @@ -67,6 +73,7 @@
throw new InvalidOperationException($"Input {input.Id} was not provided to the pipeline before execution.");
}

GlobalState.Logger.LogInfo(LogType.Pipeline, $"Pipeline {this.GetType().Name} started.");
this.State = PipelineState.Running;

byte i = 1;
Expand Down Expand Up @@ -95,6 +102,7 @@
i++;
}

GlobalState.Logger.LogInfo(LogType.Pipeline, $"Pipeline {this.GetType().Name} finished!");
this.State = PipelineState.Finished;
}
}
26 changes: 26 additions & 0 deletions Refresher.Core/Pipelines/RPCS3PatchPipeline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Refresher.Core.Pipelines.Steps;

namespace Refresher.Core.Pipelines;

public class RPCS3PatchPipeline : Pipeline
{
public override string Id => "rpcs3-patch";
public override string Name => "RPCS3 Patch";
protected override List<Type> StepTypes =>
[
// Info gathering stage
typeof(SetupEmulatorAccessorStep),
typeof(ValidateGameStep),
typeof(DownloadParamSfoStep),
typeof(DownloadGameEbootStep),
typeof(ReadEbootContentIdStep),
typeof(DownloadGameLicenseStep),

// Decryption and patch stage
typeof(PrepareSceToolStep),
typeof(DecryptGameEbootStep),
typeof(PrepareEbootPatchCreatorAndVerifyStep),
typeof(ApplyPatchToEbootStep),
// The patch creator will automatically write to the patch file. No upload steps are required.
];
}
5 changes: 5 additions & 0 deletions Refresher.Core/Pipelines/Step.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Refresher.Core.Patching;

namespace Refresher.Core.Pipelines;

public abstract class Step
Expand All @@ -7,6 +9,9 @@ public abstract class Step

public virtual List<StepInput> Inputs { get; } = [];

protected GameInformation Game => this.Pipeline.GameInformation!;
protected EncryptionDetails Encryption => this.Pipeline.EncryptionDetails!;

protected Step(Pipeline pipeline)
{
this.Pipeline = pipeline;
Expand Down
8 changes: 7 additions & 1 deletion Refresher.Core/Pipelines/StepInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
{
public string Id { get; init; }
public string Name { get; init; }
public StepInputType Type { get; init; }

public StepInput(string id, string name)
public string Placeholder { get; init; }

public Func<string?>? DetermineDefaultValue { get; init; }

public StepInput(string id, string name, StepInputType type = StepInputType.Text)

Check warning on line 13 in Refresher.Core/Pipelines/StepInput.cs

View workflow job for this annotation

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

Non-nullable property 'Placeholder' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in Refresher.Core/Pipelines/StepInput.cs

View workflow job for this annotation

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

Non-nullable property 'Placeholder' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in Refresher.Core/Pipelines/StepInput.cs

View workflow job for this annotation

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

Non-nullable property 'Placeholder' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in Refresher.Core/Pipelines/StepInput.cs

View workflow job for this annotation

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

Non-nullable property 'Placeholder' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in Refresher.Core/Pipelines/StepInput.cs

View workflow job for this annotation

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

Non-nullable property 'Placeholder' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 13 in Refresher.Core/Pipelines/StepInput.cs

View workflow job for this annotation

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

Non-nullable property 'Placeholder' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
this.Id = id;
this.Name = name;
this.Type = type;
}

public string GetValueFromPipeline(Pipeline pipeline)
Expand Down
10 changes: 10 additions & 0 deletions Refresher.Core/Pipelines/StepInputType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Refresher.Core.Pipelines;

public enum StepInputType : byte
{
Text,
Directory,
OpenFile,
SaveFile,
Game,
}
20 changes: 20 additions & 0 deletions Refresher.Core/Pipelines/Steps/ApplyPatchToEbootStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Refresher.Core.Pipelines.Steps;

public class ApplyPatchToEbootStep : Step
{
public ApplyPatchToEbootStep(Pipeline pipeline) : base(pipeline)
{}

public override List<StepInput> Inputs =>
[
CommonStepInputs.ServerUrl,
];

public override float Progress { get; protected set; }
public override Task ExecuteAsync(CancellationToken cancellationToken = default)
{
string url = this.Pipeline.Inputs["url"];
this.Pipeline.Patcher!.Patch(url, true); // TODO: handle autodiscover in pipelines
return Task.CompletedTask;
}
}
24 changes: 24 additions & 0 deletions Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using SCEToolSharp;

namespace Refresher.Core.Pipelines.Steps;

public class DecryptGameEbootStep : Step
{
public DecryptGameEbootStep(Pipeline pipeline) : base(pipeline)
{}

public override float Progress { get; protected set; }
public override Task ExecuteAsync(CancellationToken cancellationToken = default)
{
string tempFile = this.Game.DecryptedEbootPath = Path.GetTempFileName();
LibSceToolSharp.Decrypt(this.Game.DownloadedEbootPath!, tempFile);

// HACK: scetool doesn't give us result codes, check if the file has been written to instead
if (new FileInfo(tempFile).Length == 0)
{
throw new Exception("Decryption failed.");
}

return Task.CompletedTask;
}
}
52 changes: 52 additions & 0 deletions Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Refresher.Core.Accessors;

namespace Refresher.Core.Pipelines.Steps;

public class DownloadGameEbootStep : Step
{
public DownloadGameEbootStep(Pipeline pipeline) : base(pipeline)
{}

public override float Progress { get; protected set; }
public override Task ExecuteAsync(CancellationToken cancellationToken = default)
{
string titleId = this.Game.TitleId;
string usrDir = $"game/{titleId}/USRDIR";

string ebootPath = Path.Combine(usrDir, "EBOOT.BIN.ORIG"); // Prefer original backup over active copy
PatchAccessor.Try(() =>
{
if (this.Pipeline.Accessor!.FileExists(ebootPath)) return;
// If the backup doesn't exist, use the EBOOT.BIN

State.Logger.LogInfo(Accessor, "Couldn't find an original backup of the EBOOT, using active copy. This is not an error.");
ebootPath = Path.Combine(usrDir, "EBOOT.BIN");

this.Progress = 0.25f;

if (this.Pipeline.Accessor.FileExists(ebootPath)) return;

// If we land here, then we have no valid patch target without any way to recover.
// This is very inconvenient for us and the user.
throw new FileNotFoundException("The EBOOT.BIN file does not exist, nor does the original backup exist." +
"This usually means you haven't installed any updates for your game.");
});

this.Progress = 0.5f;

string downloadedFile = null!;
PatchAccessor.Try(() =>
{
downloadedFile = this.Pipeline.Accessor!.DownloadFile(ebootPath);
this.Game.DownloadedEbootPath = downloadedFile;
});

State.Logger.LogDebug(Accessor, $"Downloaded EBOOT Path: {downloadedFile}");
if (!File.Exists(downloadedFile))
{
throw new FileNotFoundException("Could not find the EBOOT we downloaded. This is likely a bug. Patching cannot continue.");
}

return Task.CompletedTask;
}
}
Loading
Loading