Skip to content

Commit

Permalink
Merge pull request #72 from LittleBigRefresh/pipelines-rpcs3
Browse files Browse the repository at this point in the history
RPCS3 Pipelines
  • Loading branch information
jvyden authored Oct 14, 2024
2 parents d68452b + 8c092a9 commit c8d7368
Show file tree
Hide file tree
Showing 26 changed files with 664 additions and 14 deletions.
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 @@ -436,7 +436,7 @@ public void Patch(string url, bool patchDigest)
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 class Pipeline
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 (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 (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 (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 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = default)
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 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = default)
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 class StepInput
{
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 (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 (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 (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

0 comments on commit c8d7368

Please sign in to comment.