From 3b765b0258b383a03ee0fe78915b7b1cd5a5d3ce Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 03:24:40 -0400 Subject: [PATCH 01/20] RPCS3 patch pipeline progress --- Refresher.Core/Accessors/PatchAccessor.cs | 27 +++++ .../Extensions/ControlExtensions.cs | 16 +++ Refresher.Core/LogType.cs | 5 +- Refresher.Core/Patching/GameInformation.cs | 15 +++ Refresher.Core/Pipelines/CommonStepInputs.cs | 9 ++ Refresher.Core/Pipelines/Pipeline.cs | 10 +- .../Pipelines/RPCS3PatchPipeline.cs | 14 +++ Refresher.Core/Pipelines/StepInput.cs | 8 +- Refresher.Core/Pipelines/StepInputType.cs | 10 ++ .../Pipelines/Steps/DownloadParamSfoStep.cs | 98 +++++++++++++++++++ .../Steps/SetupEmulatorAccessorStep.cs | 46 +++++++++ Refresher.Core/Refresher.Core.csproj | 6 ++ Refresher/UI/ConsolePatchForm.cs | 4 +- Refresher/UI/MainForm.cs | 3 +- Refresher/UI/PipelineForm.cs | 43 +++++++- 15 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 Refresher.Core/Extensions/ControlExtensions.cs create mode 100644 Refresher.Core/Patching/GameInformation.cs create mode 100644 Refresher.Core/Pipelines/CommonStepInputs.cs create mode 100644 Refresher.Core/Pipelines/RPCS3PatchPipeline.cs create mode 100644 Refresher.Core/Pipelines/StepInputType.cs create mode 100644 Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs create mode 100644 Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs diff --git a/Refresher.Core/Accessors/PatchAccessor.cs b/Refresher.Core/Accessors/PatchAccessor.cs index 905d4c4..3fe478a 100644 --- a/Refresher.Core/Accessors/PatchAccessor.cs +++ b/Refresher.Core/Accessors/PatchAccessor.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.Contracts; +using System.Reflection; + namespace Refresher.Core.Accessors; public abstract class PatchAccessor @@ -38,4 +41,28 @@ public virtual void CopyFile(string inPath, string outPath) inStream.CopyTo(outStream); } + + [Pure] + 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) + { + + } } \ No newline at end of file diff --git a/Refresher.Core/Extensions/ControlExtensions.cs b/Refresher.Core/Extensions/ControlExtensions.cs new file mode 100644 index 0000000..8beff6c --- /dev/null +++ b/Refresher.Core/Extensions/ControlExtensions.cs @@ -0,0 +1,16 @@ +using Eto.Forms; + +namespace Refresher.Core.Extensions; + +public static class ControlExtensions +{ + public static string GetUserInput(this Control control) + { + return control switch + { + TextControl textControl => textControl.Text, + FilePicker filePicker => filePicker.FilePath, + _ => throw new ArgumentOutOfRangeException(control.GetType().Name), + }; + } +} \ No newline at end of file diff --git a/Refresher.Core/LogType.cs b/Refresher.Core/LogType.cs index ab73a9f..af0bfec 100644 --- a/Refresher.Core/LogType.cs +++ b/Refresher.Core/LogType.cs @@ -6,7 +6,7 @@ public enum LogType : byte PPU, Verify, CLI, - PatchAccessor, + Accessor, PatchForm, IntegratedPatchForm, InfoRetrieval, @@ -14,6 +14,9 @@ public enum LogType : byte IDPS, OSIntegration, AutoDiscover, + PS3, PSP, + Vita, + RPCS3, Pipeline } \ No newline at end of file diff --git a/Refresher.Core/Patching/GameInformation.cs b/Refresher.Core/Patching/GameInformation.cs new file mode 100644 index 0000000..b8dc04e --- /dev/null +++ b/Refresher.Core/Patching/GameInformation.cs @@ -0,0 +1,15 @@ +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 override string ToString() + { + return $"[{this.TitleId}] name: {this.Name}, contentId: {this.ContentId}, version: {this.Version}"; + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/CommonStepInputs.cs b/Refresher.Core/Pipelines/CommonStepInputs.cs new file mode 100644 index 0000000..b60536d --- /dev/null +++ b/Refresher.Core/Pipelines/CommonStepInputs.cs @@ -0,0 +1,9 @@ +namespace Refresher.Core.Pipelines; + +internal class CommonStepInputs +{ + internal static readonly StepInput TitleId = new("title-id", "Game to patch", StepInputType.Game) + { + Placeholder = "NPUA80662", + }; +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index a7db403..71f948e 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -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; @@ -10,9 +11,12 @@ public abstract class Pipeline public abstract string Id { get; } public abstract string Name { get; } - public Dictionary Inputs = []; + public readonly Dictionary Inputs = []; public FrozenSet RequiredInputs { get; private set; } + public PatchAccessor? Accessor { get; internal set; } + public GameInformation? GameInformation { get; internal set; } + public PipelineState State { get; private set; } = PipelineState.NotStarted; public float Progress @@ -65,6 +69,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; @@ -93,6 +98,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = default) i++; } + GlobalState.Logger.LogInfo(LogType.Pipeline, $"Pipeline {this.GetType().Name} finished!"); this.State = PipelineState.Finished; } } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs new file mode 100644 index 0000000..31a040c --- /dev/null +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -0,0 +1,14 @@ +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 StepTypes => + [ + typeof(SetupEmulatorAccessorStep), + typeof(DownloadParamSfoStep), + ]; +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/StepInput.cs b/Refresher.Core/Pipelines/StepInput.cs index 8ec03ba..1382db1 100644 --- a/Refresher.Core/Pipelines/StepInput.cs +++ b/Refresher.Core/Pipelines/StepInput.cs @@ -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? DetermineDefaultValue { get; init; } + + public StepInput(string id, string name, StepInputType type = StepInputType.Text) { this.Id = id; this.Name = name; + this.Type = type; } public string GetValueFromPipeline(Pipeline pipeline) diff --git a/Refresher.Core/Pipelines/StepInputType.cs b/Refresher.Core/Pipelines/StepInputType.cs new file mode 100644 index 0000000..7f494ca --- /dev/null +++ b/Refresher.Core/Pipelines/StepInputType.cs @@ -0,0 +1,10 @@ +namespace Refresher.Core.Pipelines; + +public enum StepInputType : byte +{ + Text, + Directory, + OpenFile, + SaveFile, + Game, +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs b/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs new file mode 100644 index 0000000..e7d24b8 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs @@ -0,0 +1,98 @@ +using Refresher.Core.Accessors; +using Refresher.Core.Patching; +using Refresher.Core.Verification; + +namespace Refresher.Core.Pipelines.Steps; + +public class DownloadParamSfoStep : Step +{ + public DownloadParamSfoStep(Pipeline pipeline) : base(pipeline) + { + } + + public override List Inputs { get; } = + [ + CommonStepInputs.TitleId, + ]; + + public override float Progress { get; protected set; } + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string titleId = CommonStepInputs.TitleId.GetValueFromPipeline(this.Pipeline).Trim(); + string gamePath = $"game/{titleId}"; + + // sanity check. UI will not allow this to happen, but CLI will + if (titleId.Length != "NPUA80662".Length) + throw new InvalidOperationException("Title ID does not match expected length. Did you type the ID in correctly?"); + + if(!this.Pipeline.Accessor!.DirectoryExists(gamePath)) + throw new FileNotFoundException("The game directory does not exist. This usually means you haven't installed any updates for your game."); + + GameInformation game = this.Pipeline.GameInformation = new GameInformation + { + TitleId = titleId, + }; + + Stream? sfoStream = null; + PatchAccessor.Try(() => + { + string sfoLocation = $"{gamePath}/PARAM.SFO"; + + if(this.Pipeline.Accessor.FileExists(sfoLocation)) + sfoStream = this.Pipeline.Accessor.OpenRead(sfoLocation); + }); + + ParamSfo? sfo = null; + try + { + if (sfoStream == null) + { + throw new FileNotFoundException("The game directory does not exist. This usually means you haven't installed any updates for your game."); + } + this.ParseSfoStream(sfoStream, out sfo); + } + catch (EndOfStreamException) + { + State.Logger.LogError(InfoRetrieval, $"Couldn't load {game}'s PARAM.SFO because the file was incomplete."); + } + catch(Exception e) + { + game.Name = $"Unknown PARAM.SFO [{game}]"; + + State.Logger.LogError(InfoRetrieval, $"Couldn't load {game}'s PARAM.SFO: {e}"); + if (sfo != null) + { + State.Logger.LogDebug(InfoRetrieval, $"PARAM.SFO version:{sfo.Version} dump:"); + foreach ((string? key, object? value) in sfo.Table) + { + State.Logger.LogDebug(InfoRetrieval, $" '{key}' = '{value}'"); + } + } + else + { + State.Logger.LogWarning(InfoRetrieval, "PARAM.SFO was not read, can't dump"); + } + + SentrySdk.CaptureException(e); + } + + State.Logger.LogInfo(InfoRetrieval, "Parsed PARAM.SFO: " + game); + return Task.CompletedTask; + } + + private void ParseSfoStream(Stream sfoStream, out ParamSfo sfo) + { + sfo = new ParamSfo(sfoStream); + GameInformation info = this.Pipeline.GameInformation!; + + info.Version = "01.00"; + if (sfo.Table.TryGetValue("APP_VER", out object? value)) + { + string? appVersion = value.ToString(); + if (appVersion != null) + info.Version = appVersion; + } + + info.Name = sfo.Table["TITLE"].ToString(); + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs b/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs new file mode 100644 index 0000000..47a6dc3 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs @@ -0,0 +1,46 @@ +using Refresher.Core.Accessors; + +namespace Refresher.Core.Pipelines.Steps; + +public class SetupEmulatorAccessorStep : Step +{ + public SetupEmulatorAccessorStep(Pipeline pipeline) : base(pipeline) + {} + + public override float Progress { get; protected set; } + + public override List Inputs { get; } = [ + 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 = DetermineDefaultPath, + }, + ]; + + // TODO: Cache the last used location for easier entry + private static string? DetermineDefaultPath() + { + // 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; + } + + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string path = this.Inputs[0].GetValueFromPipeline(this.Pipeline); + State.Logger.LogDebug(RPCS3, $"Using RPCS3 path {path}"); + this.Pipeline.Accessor = new EmulatorPatchAccessor(path); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Refresher.Core/Refresher.Core.csproj b/Refresher.Core/Refresher.Core.csproj index 7b67f98..dd7d997 100644 --- a/Refresher.Core/Refresher.Core.csproj +++ b/Refresher.Core/Refresher.Core.csproj @@ -13,5 +13,11 @@ + + + + ..\..\..\.nuget\packages\eto.forms\2.8.3\lib\net6.0\Eto.dll + + diff --git a/Refresher/UI/ConsolePatchForm.cs b/Refresher/UI/ConsolePatchForm.cs index b1787ac..413fb1b 100644 --- a/Refresher/UI/ConsolePatchForm.cs +++ b/Refresher/UI/ConsolePatchForm.cs @@ -55,7 +55,7 @@ protected override void RevertToOriginalExecutable(object? sender, EventArgs e) private bool InitializePatchAccessor() { this.DisposePatchAccessor(); - State.Logger.LogTrace(LogType.PatchAccessor, "Making a new patch accessor"); + State.Logger.LogTrace(LogType.Accessor, "Making a new patch accessor"); try { this.Accessor = new ConsolePatchAccessor(this._remoteAddress.Text.Trim()); @@ -86,7 +86,7 @@ private bool InitializePatchAccessor() private void DisposePatchAccessor() { - State.Logger.LogTrace(LogType.PatchAccessor, "Disposing patch accessor"); + State.Logger.LogTrace(LogType.Accessor, "Disposing patch accessor"); if (this.Accessor is IDisposable disposable) disposable.Dispose(); } diff --git a/Refresher/UI/MainForm.cs b/Refresher/UI/MainForm.cs index 0d4c00d..bacb830 100644 --- a/Refresher/UI/MainForm.cs +++ b/Refresher/UI/MainForm.cs @@ -20,8 +20,9 @@ public class MainForm : RefresherForm new Button((_, _) => this.ShowChild()) { Text = "PS3 Patch" }, new Button((_, _) => this.ShowChild()) { Text = "PSP Setup" } #if DEBUG - ,this.PipelineButton() + ,this.PipelineButton(), #endif + this.PipelineButton() ); layout.Spacing = 5; diff --git a/Refresher/UI/PipelineForm.cs b/Refresher/UI/PipelineForm.cs index 764301d..2a25426 100644 --- a/Refresher/UI/PipelineForm.cs +++ b/Refresher/UI/PipelineForm.cs @@ -1,6 +1,8 @@ +using Eto; using Eto.Drawing; using Eto.Forms; using Refresher.Core; +using Refresher.Core.Extensions; using Refresher.Core.Logging; using Refresher.Core.Pipelines; @@ -74,7 +76,21 @@ private void InitializePipeline() this._formLayout.Rows.Clear(); foreach (StepInput input in this._pipeline.RequiredInputs) { - this._formLayout.Rows.Add(AddField(input)); + TableRow row; + switch (input.Type) + { + case StepInputType.Game: + case StepInputType.Text: + row = AddField(input); + break; + case StepInputType.Directory: + row = AddField(input); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + this._formLayout.Rows.Add(row); } this.UpdateSubtitle(this._pipeline.Name); @@ -109,7 +125,7 @@ private void OnButtonClick(object? sender, EventArgs e) foreach (TableRow row in this._formLayout.Rows) { string id = row.Cells[0].Control.ToolTip; - string value = ((TextControl)row.Cells[1].Control).Text; + string value = row.Cells[1].Control.GetUserInput(); this._pipeline.Inputs.Add(id, value); } @@ -139,6 +155,29 @@ private void OnButtonClick(object? sender, EventArgs e) }; Control control = new TControl(); + TextBox? textBox = control as TextBox; + + string? newValue = input.DetermineDefaultValue?.Invoke(); + + if (textBox != null) + { + textBox.Text = newValue; + textBox.PlaceholderText = input.Placeholder; + } + else if (control is FilePicker filePicker) + { + filePicker.FilePath = newValue; + + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + filePicker.FileAction = input.Type switch + { + StepInputType.Directory => FileAction.SelectFolder, + StepInputType.OpenFile => FileAction.OpenFile, + StepInputType.SaveFile => FileAction.SaveFile, + _ => throw new ArgumentOutOfRangeException(), + }; + } + return new TableRow(label, control); } From 54b545d72b61ba0e1774d628ae185dd251051273 Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 03:26:58 -0400 Subject: [PATCH 02/20] ExampleStep -> DelayOneSecondStep --- Refresher.Core/Pipelines/ExamplePipeline.cs | 10 +++++----- .../Steps/{ExampleStep.cs => DelayOneSecondStep.cs} | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) rename Refresher.Core/Pipelines/Steps/{ExampleStep.cs => DelayOneSecondStep.cs} (72%) diff --git a/Refresher.Core/Pipelines/ExamplePipeline.cs b/Refresher.Core/Pipelines/ExamplePipeline.cs index 53531e5..454b994 100644 --- a/Refresher.Core/Pipelines/ExamplePipeline.cs +++ b/Refresher.Core/Pipelines/ExamplePipeline.cs @@ -9,10 +9,10 @@ public class ExamplePipeline : Pipeline protected override List StepTypes { get; } = [ typeof(ExampleInputStep), - typeof(ExampleStep), - typeof(ExampleStep), - typeof(ExampleStep), - typeof(ExampleStep), - typeof(ExampleStep), + typeof(DelayOneSecondStep), + typeof(DelayOneSecondStep), + typeof(DelayOneSecondStep), + typeof(DelayOneSecondStep), + typeof(DelayOneSecondStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/ExampleStep.cs b/Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs similarity index 72% rename from Refresher.Core/Pipelines/Steps/ExampleStep.cs rename to Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs index 79bd6b2..e461782 100644 --- a/Refresher.Core/Pipelines/Steps/ExampleStep.cs +++ b/Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs @@ -1,8 +1,8 @@ namespace Refresher.Core.Pipelines.Steps; -public class ExampleStep : Step +public class DelayOneSecondStep : Step { - public ExampleStep(Pipeline pipeline) : base(pipeline) + public DelayOneSecondStep(Pipeline pipeline) : base(pipeline) { } From 9016cb0683d085ecaf4731e6dd31cb738980f7d1 Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 03:29:17 -0400 Subject: [PATCH 03/20] Smooth progress for delay step --- Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs b/Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs index e461782..f7c69f7 100644 --- a/Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs +++ b/Refresher.Core/Pipelines/Steps/DelayOneSecondStep.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Refresher.Core.Pipelines.Steps; public class DelayOneSecondStep : Step @@ -10,6 +12,11 @@ public DelayOneSecondStep(Pipeline pipeline) : base(pipeline) public override async Task ExecuteAsync(CancellationToken cancellationToken = default) { - await Task.Delay(1000, cancellationToken); + Stopwatch stopwatch = Stopwatch.StartNew(); + while (stopwatch.ElapsedMilliseconds <= 1000) + { + this.Progress = stopwatch.ElapsedMilliseconds / 1000.0f; + await Task.Delay(10, cancellationToken); + } } } \ No newline at end of file From 8a8ad5ab9a007246ab745078951bea4b5d88952c Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 03:30:50 -0400 Subject: [PATCH 04/20] Fix progress being off by one step --- Refresher.Core/Pipelines/Pipeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index 71f948e..a4ede4a 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -23,7 +23,7 @@ public float Progress { get { - float completed = this._currentStepIndex / (float)this._steps.Count; + float completed = (this._currentStepIndex - 1) / (float)this._steps.Count; float currentStep = this._currentStep?.Progress ?? 0f; float stepWeight = 1f / this._steps.Count; From 1b636ffbb5b31055aeab8d98e1ffdf84281b56de Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 03:33:24 -0400 Subject: [PATCH 05/20] Show current progress --- Refresher.Core/Pipelines/Pipeline.cs | 2 ++ Refresher/UI/PipelineForm.cs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index a4ede4a..87677b4 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -31,6 +31,8 @@ public float Progress } } + public float CurrentProgress => this._currentStep?.Progress ?? 0; + protected abstract List StepTypes { get; } private List _steps = []; diff --git a/Refresher/UI/PipelineForm.cs b/Refresher/UI/PipelineForm.cs index 2a25426..ed323ac 100644 --- a/Refresher/UI/PipelineForm.cs +++ b/Refresher/UI/PipelineForm.cs @@ -13,6 +13,7 @@ namespace Refresher.UI; private TPipeline? _pipeline; private readonly Button _button; + private readonly ProgressBar _currentProgressBar; private readonly ProgressBar _progressBar; private readonly ListBox _messages; @@ -33,6 +34,7 @@ namespace Refresher.UI; Panel2 = new StackLayout([ this._messages = new ListBox { Height = 200 }, this._button = new Button(this.OnButtonClick) { Text = "Execute" }, + this._currentProgressBar = new ProgressBar(), this._progressBar = new ProgressBar(), ]) { @@ -52,7 +54,8 @@ namespace Refresher.UI; private void UpdateFormState() { this._progressBar.Value = (int)((this._pipeline?.Progress ?? 0) * 100); - this._progressBar.Enabled = this._pipeline?.State == PipelineState.Running; + this._currentProgressBar.Value = (int)(this._pipeline?.CurrentProgress * 100 ?? 0); + this._currentProgressBar.Enabled = this._progressBar.Enabled = this._pipeline?.State == PipelineState.Running; this._progressBar.ToolTip = this._pipeline?.State.ToString() ?? "Uninitialized"; this._button.Text = this._pipeline?.State switch From 710df2b4002887de54f80cb1a1938c7ae488a764 Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 21:14:25 -0400 Subject: [PATCH 06/20] Fix accidental Eto.Forms usage in Refresher.Core --- Refresher.Core/Refresher.Core.csproj | 6 ------ .../Extensions/ControlExtensions.cs | 0 2 files changed, 6 deletions(-) rename {Refresher.Core => Refresher}/Extensions/ControlExtensions.cs (100%) diff --git a/Refresher.Core/Refresher.Core.csproj b/Refresher.Core/Refresher.Core.csproj index dd7d997..7b67f98 100644 --- a/Refresher.Core/Refresher.Core.csproj +++ b/Refresher.Core/Refresher.Core.csproj @@ -13,11 +13,5 @@ - - - - ..\..\..\.nuget\packages\eto.forms\2.8.3\lib\net6.0\Eto.dll - - diff --git a/Refresher.Core/Extensions/ControlExtensions.cs b/Refresher/Extensions/ControlExtensions.cs similarity index 100% rename from Refresher.Core/Extensions/ControlExtensions.cs rename to Refresher/Extensions/ControlExtensions.cs From d63f98bb1c322f8dbc515ef1280afca9ddd733c2 Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 20 Sep 2024 21:15:41 -0400 Subject: [PATCH 07/20] Fix release build errors --- Refresher/UI/MainForm.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Refresher/UI/MainForm.cs b/Refresher/UI/MainForm.cs index bacb830..95ec17a 100644 --- a/Refresher/UI/MainForm.cs +++ b/Refresher/UI/MainForm.cs @@ -13,17 +13,18 @@ public class MainForm : RefresherForm { StackLayout layout; this.Content = layout = new StackLayout - ( + // ReSharper disable once RedundantExplicitParamsArrayCreation + ([ new Label { Text = "Welcome to Refresher! Please pick a patching method to continue." }, new Button((_, _) => this.ShowChild()) { Text = "File Patch (using a .ELF)" }, new Button((_, _) => this.ShowChild()) { Text = "RPCS3 Patch" }, new Button((_, _) => this.ShowChild()) { Text = "PS3 Patch" }, - new Button((_, _) => this.ShowChild()) { Text = "PSP Setup" } + new Button((_, _) => this.ShowChild()) { Text = "PSP Setup" }, #if DEBUG - ,this.PipelineButton(), + this.PipelineButton(), #endif - this.PipelineButton() - ); + this.PipelineButton(), + ]); layout.Spacing = 5; layout.HorizontalContentAlignment = HorizontalAlignment.Stretch; From 7f53e65946f2da298c8d200254af58ef24809687 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 21:31:16 -0400 Subject: [PATCH 08/20] ValidateGameStep + DownloadGameEbootStep --- Refresher.Core/Accessors/PatchAccessor.cs | 3 +- Refresher.Core/Patching/GameInformation.cs | 2 + .../Pipelines/RPCS3PatchPipeline.cs | 2 + .../Pipelines/Steps/DownloadGameEbootStep.cs | 52 +++++++++++++++++++ .../Pipelines/Steps/DownloadParamSfoStep.cs | 28 ++-------- .../Pipelines/Steps/ValidateGameStep.cs | 36 +++++++++++++ 6 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs create mode 100644 Refresher.Core/Pipelines/Steps/ValidateGameStep.cs diff --git a/Refresher.Core/Accessors/PatchAccessor.cs b/Refresher.Core/Accessors/PatchAccessor.cs index 3fe478a..eb41a1c 100644 --- a/Refresher.Core/Accessors/PatchAccessor.cs +++ b/Refresher.Core/Accessors/PatchAccessor.cs @@ -42,7 +42,6 @@ public virtual void CopyFile(string inPath, string outPath) inStream.CopyTo(outStream); } - [Pure] public static void Try(Action action) { try @@ -63,6 +62,6 @@ public static void Try(Action action) private static void CatchAccessorException(Exception ex) { - + State.Logger.LogError(Accessor, ex.ToString()); } } \ No newline at end of file diff --git a/Refresher.Core/Patching/GameInformation.cs b/Refresher.Core/Patching/GameInformation.cs index b8dc04e..ad8ac30 100644 --- a/Refresher.Core/Patching/GameInformation.cs +++ b/Refresher.Core/Patching/GameInformation.cs @@ -7,6 +7,8 @@ public class GameInformation public string? Name { get; set; } public string? ContentId { get; set; } public string? Version { get; set; } + + public string? DownloadedEbootPath { get; set; } public override string ToString() { diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index 31a040c..ab727b9 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -9,6 +9,8 @@ public class RPCS3PatchPipeline : Pipeline protected override List StepTypes => [ typeof(SetupEmulatorAccessorStep), + typeof(ValidateGameStep), typeof(DownloadParamSfoStep), + typeof(DownloadGameEbootStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs new file mode 100644 index 0000000..db2f538 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs @@ -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.Pipeline.GameInformation!.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.Pipeline.GameInformation.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; + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs b/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs index e7d24b8..5d9e7bc 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs @@ -7,38 +7,20 @@ namespace Refresher.Core.Pipelines.Steps; public class DownloadParamSfoStep : Step { public DownloadParamSfoStep(Pipeline pipeline) : base(pipeline) - { - } - - public override List Inputs { get; } = - [ - CommonStepInputs.TitleId, - ]; + {} public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - string titleId = CommonStepInputs.TitleId.GetValueFromPipeline(this.Pipeline).Trim(); - string gamePath = $"game/{titleId}"; - - // sanity check. UI will not allow this to happen, but CLI will - if (titleId.Length != "NPUA80662".Length) - throw new InvalidOperationException("Title ID does not match expected length. Did you type the ID in correctly?"); - - if(!this.Pipeline.Accessor!.DirectoryExists(gamePath)) - throw new FileNotFoundException("The game directory does not exist. This usually means you haven't installed any updates for your game."); - - GameInformation game = this.Pipeline.GameInformation = new GameInformation - { - TitleId = titleId, - }; + GameInformation game = this.Pipeline.GameInformation!; + string gamePath = $"game/{game.TitleId}"; Stream? sfoStream = null; PatchAccessor.Try(() => { string sfoLocation = $"{gamePath}/PARAM.SFO"; - if(this.Pipeline.Accessor.FileExists(sfoLocation)) + if(this.Pipeline.Accessor!.FileExists(sfoLocation)) sfoStream = this.Pipeline.Accessor.OpenRead(sfoLocation); }); @@ -47,7 +29,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) { if (sfoStream == null) { - throw new FileNotFoundException("The game directory does not exist. This usually means you haven't installed any updates for your game."); + throw new FileNotFoundException("The PARAM.SFO file does not exist. This usually means you haven't installed any updates for your game."); } this.ParseSfoStream(sfoStream, out sfo); } diff --git a/Refresher.Core/Pipelines/Steps/ValidateGameStep.cs b/Refresher.Core/Pipelines/Steps/ValidateGameStep.cs new file mode 100644 index 0000000..a025801 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/ValidateGameStep.cs @@ -0,0 +1,36 @@ +using Refresher.Core.Patching; + +namespace Refresher.Core.Pipelines.Steps; + +public class ValidateGameStep : Step +{ + public ValidateGameStep(Pipeline pipeline) : base(pipeline) + {} + + public override float Progress { get; protected set; } + + public override List Inputs { get; } = + [ + CommonStepInputs.TitleId, + ]; + + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string titleId = CommonStepInputs.TitleId.GetValueFromPipeline(this.Pipeline).Trim(); + string gamePath = $"game/{titleId}"; + + // sanity check. UI will not allow this to happen, but CLI will + if (titleId.Length != "NPUA80662".Length) + throw new InvalidOperationException("Title ID does not match expected length. Did you type the ID in correctly?"); + + if(!this.Pipeline.Accessor!.DirectoryExists(gamePath)) + throw new FileNotFoundException("The game directory does not exist. This usually means you haven't installed any updates for your game."); + + this.Pipeline.GameInformation = new GameInformation + { + TitleId = titleId, + }; + + return Task.CompletedTask; + } +} \ No newline at end of file From 3df2b852d41f125d9c04d911228ed8372ceadaf2 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 21:40:02 -0400 Subject: [PATCH 09/20] ReadEbootContentIdStep --- .../Pipelines/RPCS3PatchPipeline.cs | 1 + .../Pipelines/Steps/ReadEbootContentIdStep.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index ab727b9..6e3da97 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -12,5 +12,6 @@ public class RPCS3PatchPipeline : Pipeline typeof(ValidateGameStep), typeof(DownloadParamSfoStep), typeof(DownloadGameEbootStep), + typeof(ReadEbootContentIdStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs new file mode 100644 index 0000000..8539588 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs @@ -0,0 +1,27 @@ +using SCEToolSharp; + +namespace Refresher.Core.Pipelines.Steps; + +public class ReadEbootContentIdStep : Step +{ + public ReadEbootContentIdStep(Pipeline pipeline) : base(pipeline) + {} + + public override float Progress { get; protected set; } + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string ebootPath = this.Pipeline.GameInformation!.DownloadedEbootPath!; + + LibSceToolSharp.Init(); + this.Progress = 0.5f; + + string? contentId = LibSceToolSharp.GetContentId(ebootPath)?.TrimEnd('\0'); + if (contentId == null) + throw new Exception("Unable to retrieve the content ID from the game's EBOOT."); + this.Progress = 1f; + + this.Pipeline.GameInformation.ContentId = contentId; + State.Logger.LogDebug(InfoRetrieval, "Got content ID from the game's EBOOT: {0}", contentId); + return Task.CompletedTask; + } +} \ No newline at end of file From 4bbfa19fbcd83a959d80b1f97a9cc8409527339d Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 22:02:27 -0400 Subject: [PATCH 10/20] DownloadGameLicenseStep --- Refresher.Core/Patching/GameInformation.cs | 2 - Refresher.Core/Pipelines/Pipeline.cs | 3 + .../Pipelines/RPCS3PatchPipeline.cs | 1 + .../Pipelines/Steps/DownloadGameEbootStep.cs | 2 +- .../Steps/DownloadGameLicenseStep.cs | 72 +++++++++++++++++++ .../Pipelines/Steps/ReadEbootContentIdStep.cs | 4 +- 6 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs diff --git a/Refresher.Core/Patching/GameInformation.cs b/Refresher.Core/Patching/GameInformation.cs index ad8ac30..b8dc04e 100644 --- a/Refresher.Core/Patching/GameInformation.cs +++ b/Refresher.Core/Patching/GameInformation.cs @@ -7,8 +7,6 @@ public class GameInformation public string? Name { get; set; } public string? ContentId { get; set; } public string? Version { get; set; } - - public string? DownloadedEbootPath { get; set; } public override string ToString() { diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index 87677b4..02d816b 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -16,6 +16,9 @@ public abstract class Pipeline public PatchAccessor? Accessor { get; internal set; } public GameInformation? GameInformation { get; internal set; } + public string? LicenseDirectory { get; internal set; } + public string? DownloadedEbootPath { get; set; } + public string? DownloadedActDatPath { get; internal set; } public PipelineState State { get; private set; } = PipelineState.NotStarted; diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index 6e3da97..706861a 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -13,5 +13,6 @@ public class RPCS3PatchPipeline : Pipeline typeof(DownloadParamSfoStep), typeof(DownloadGameEbootStep), typeof(ReadEbootContentIdStep), + typeof(DownloadGameLicenseStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs index db2f538..9ce84c5 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs @@ -38,7 +38,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) PatchAccessor.Try(() => { downloadedFile = this.Pipeline.Accessor!.DownloadFile(ebootPath); - this.Pipeline.GameInformation.DownloadedEbootPath = downloadedFile; + this.Pipeline.DownloadedEbootPath = downloadedFile; }); State.Logger.LogDebug(Accessor, $"Downloaded EBOOT Path: {downloadedFile}"); diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs new file mode 100644 index 0000000..656a254 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs @@ -0,0 +1,72 @@ +using Refresher.Core.Patching; +using SCEToolSharp; + +namespace Refresher.Core.Pipelines.Steps; + +public class DownloadGameLicenseStep : Step +{ + public DownloadGameLicenseStep(Pipeline pipeline) : base(pipeline) + {} + + public override float Progress { get; protected set; } + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + GameInformation game = this.Pipeline.GameInformation!; + string contentId = game.ContentId!; + + string licenseDir = Path.Join(Path.GetTempPath(), "refresher-" + Random.Shared.Next()); + Directory.CreateDirectory(licenseDir); + + this.Pipeline.LicenseDirectory = licenseDir; + + bool found = false; + foreach (string user in this.Pipeline.Accessor!.GetDirectoriesInDirectory(Path.Combine("home"))) + { + State.Logger.LogDebug(Crypto, $"Checking all license files in {user}"); + string exdataFolder = Path.Combine(user, "exdata"); + + if (!this.Pipeline.Accessor.DirectoryExists(exdataFolder)) + { + State.Logger.LogDebug(Crypto, $"Exdata folder doesn't exist for user {user}, skipping..."); + continue; + } + + foreach (string licenseFile in this.Pipeline.Accessor.GetFilesInDirectory(exdataFolder)) + { + //If the license file does not contain the content ID in its path, skip it + if (!licenseFile.Contains(contentId) && !licenseFile.Contains(game.TitleId)) + continue; + + State.Logger.LogDebug(Crypto, $"Found compatible rap: {licenseFile}"); + + string actDatPath = Path.Combine(user, "exdata", "act.dat"); + + //If it is a valid content id, lets download that user's act.dat, if its there + if (!found && this.Pipeline.Accessor.FileExists(actDatPath)) + { + string downloadedActDat = this.Pipeline.Accessor.DownloadFile(actDatPath); + this.Pipeline.DownloadedActDatPath = downloadedActDat; + } + + //And the license file + string downloadedLicenseFile = this.Pipeline.Accessor.DownloadFile(licenseFile); + File.Move(downloadedLicenseFile, Path.Join(licenseDir, Path.GetFileName(licenseFile))); + + State.Logger.LogInfo(Crypto, $"Downloaded compatible license file {licenseFile}."); + + found = true; + } + + if (found) + break; + } + + if (!found) + { + State.Logger.LogWarning(Crypto, "Couldn't find a license file for {0}. For disc copies, this is normal." + + "For digital copies, this may present problems. Attempting to continue without it...", game.TitleId); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs index 8539588..f8873c3 100644 --- a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs +++ b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs @@ -10,7 +10,7 @@ public ReadEbootContentIdStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - string ebootPath = this.Pipeline.GameInformation!.DownloadedEbootPath!; + string ebootPath = this.Pipeline.DownloadedEbootPath!; LibSceToolSharp.Init(); this.Progress = 0.5f; @@ -20,7 +20,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) throw new Exception("Unable to retrieve the content ID from the game's EBOOT."); this.Progress = 1f; - this.Pipeline.GameInformation.ContentId = contentId; + this.Pipeline.GameInformation!.ContentId = contentId; State.Logger.LogDebug(InfoRetrieval, "Got content ID from the game's EBOOT: {0}", contentId); return Task.CompletedTask; } From 5d91c633fa32b44c23060deecf5e870e135dab0a Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 22:20:06 -0400 Subject: [PATCH 11/20] PrepareSceToolStep, DecryptGameEbootStep --- Refresher.Core/Pipelines/Pipeline.cs | 1 + .../Pipelines/RPCS3PatchPipeline.cs | 2 ++ .../Pipelines/Steps/DecryptGameEbootStep.cs | 24 +++++++++++++++++++ .../Pipelines/Steps/PrepareSceToolStep.cs | 24 +++++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs create mode 100644 Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index 02d816b..c18f584 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -19,6 +19,7 @@ public abstract class Pipeline public string? LicenseDirectory { get; internal set; } public string? DownloadedEbootPath { get; set; } public string? DownloadedActDatPath { get; internal set; } + public string? DecryptedEbootPath { get; internal set; } public PipelineState State { get; private set; } = PipelineState.NotStarted; diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index 706861a..a55f72e 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -14,5 +14,7 @@ public class RPCS3PatchPipeline : Pipeline typeof(DownloadGameEbootStep), typeof(ReadEbootContentIdStep), typeof(DownloadGameLicenseStep), + typeof(PrepareSceToolStep), + typeof(DecryptGameEbootStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs b/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs new file mode 100644 index 0000000..8e4d05e --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs @@ -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.Pipeline.DecryptedEbootPath = Path.GetTempFileName(); + LibSceToolSharp.Decrypt(this.Pipeline.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; + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs b/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs new file mode 100644 index 0000000..c0fe4ff --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs @@ -0,0 +1,24 @@ +using static SCEToolSharp.LibSceToolSharp; + +namespace Refresher.Core.Pipelines.Steps; + +public class PrepareSceToolStep : Step +{ + public PrepareSceToolStep(Pipeline pipeline) : base(pipeline) + { + } + + public override float Progress { get; protected set; } + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + Init(); + + SetRapDirectory(this.Pipeline.LicenseDirectory!); + SetRifPath(this.Pipeline.LicenseDirectory!); + + if(this.Pipeline.DownloadedActDatPath != null) + SetActDatFilePath(this.Pipeline.DownloadedActDatPath); + + return Task.CompletedTask; + } +} \ No newline at end of file From 4baed9af44d6fbb20acc03374bd86dcd33534ad7 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 22:39:21 -0400 Subject: [PATCH 12/20] PrepareEbootPatcherAndVerifyStep --- Refresher.Core/Pipelines/CommonStepInputs.cs | 5 +++ Refresher.Core/Pipelines/Pipeline.cs | 1 + .../Pipelines/RPCS3PatchPipeline.cs | 4 ++ .../Steps/PrepareEbootPatcherAndVerifyStep.cs | 39 +++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs diff --git a/Refresher.Core/Pipelines/CommonStepInputs.cs b/Refresher.Core/Pipelines/CommonStepInputs.cs index b60536d..5d24dc0 100644 --- a/Refresher.Core/Pipelines/CommonStepInputs.cs +++ b/Refresher.Core/Pipelines/CommonStepInputs.cs @@ -6,4 +6,9 @@ internal class CommonStepInputs { Placeholder = "NPUA80662", }; + + internal static readonly StepInput ServerUrl = new("url", "Server URL to patch to") + { + Placeholder = "https://lbp.littlebigrefresh.com", + }; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index c18f584..1700160 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -14,6 +14,7 @@ public abstract class Pipeline public readonly Dictionary Inputs = []; public FrozenSet RequiredInputs { get; private set; } + public IPatcher? Patcher { get; set; } public PatchAccessor? Accessor { get; internal set; } public GameInformation? GameInformation { get; internal set; } public string? LicenseDirectory { get; internal set; } diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index a55f72e..41a934f 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -8,13 +8,17 @@ public class RPCS3PatchPipeline : Pipeline public override string Name => "RPCS3 Patch"; protected override List 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(PrepareEbootPatcherAndVerifyStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs b/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs new file mode 100644 index 0000000..2b53723 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs @@ -0,0 +1,39 @@ +using Refresher.Core.Patching; +using Refresher.Core.Verification; + +namespace Refresher.Core.Pipelines.Steps; + +public class PrepareEbootPatcherAndVerifyStep : Step +{ + public PrepareEbootPatcherAndVerifyStep(Pipeline pipeline) : base(pipeline) + {} + + public override List Inputs => + [ + CommonStepInputs.ServerUrl, + ]; + + public override float Progress { get; protected set; } + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string url = this.Pipeline.Inputs["url"]; + + using Stream stream = File.Open(this.Pipeline.DecryptedEbootPath!, FileMode.Open); + EbootPatcher patcher = new(stream); + + this.Pipeline.Patcher = patcher; + + List messages = patcher.Verify(url, true); // TODO: handle autodiscover in pipelines + foreach (Message message in messages) + { + State.Logger.LogInfo(Patcher, message.ToString()); + } + + if (messages.Any(m => m.Level == MessageLevel.Error)) + { + throw new Exception("There were errors while verifying the patch details against the EBOOT. Check the log for more information."); + } + + return Task.CompletedTask; + } +} \ No newline at end of file From 19d78b10ac3e8931733108a4216275bb02840d61 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 22:55:55 -0400 Subject: [PATCH 13/20] Add shortcut in `Step` for GameInformation --- Refresher.Core/Pipelines/Step.cs | 4 ++++ Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs | 2 +- Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs | 2 +- Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs | 4 ++-- Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Refresher.Core/Pipelines/Step.cs b/Refresher.Core/Pipelines/Step.cs index a55e0ae..0027446 100644 --- a/Refresher.Core/Pipelines/Step.cs +++ b/Refresher.Core/Pipelines/Step.cs @@ -1,3 +1,5 @@ +using Refresher.Core.Patching; + namespace Refresher.Core.Pipelines; public abstract class Step @@ -7,6 +9,8 @@ public abstract class Step public virtual List Inputs { get; } = []; + protected GameInformation Game => this.Pipeline.GameInformation!; + protected Step(Pipeline pipeline) { this.Pipeline = pipeline; diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs index 9ce84c5..d06b108 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs @@ -10,7 +10,7 @@ public DownloadGameEbootStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - string titleId = this.Pipeline.GameInformation!.TitleId; + string titleId = this.Game.TitleId; string usrDir = $"game/{titleId}/USRDIR"; string ebootPath = Path.Combine(usrDir, "EBOOT.BIN.ORIG"); // Prefer original backup over active copy diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs index 656a254..e19df61 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs @@ -11,7 +11,7 @@ public DownloadGameLicenseStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - GameInformation game = this.Pipeline.GameInformation!; + GameInformation game = this.Game; string contentId = game.ContentId!; string licenseDir = Path.Join(Path.GetTempPath(), "refresher-" + Random.Shared.Next()); diff --git a/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs b/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs index 5d9e7bc..0420cf4 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadParamSfoStep.cs @@ -12,7 +12,7 @@ public DownloadParamSfoStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - GameInformation game = this.Pipeline.GameInformation!; + GameInformation game = this.Game; string gamePath = $"game/{game.TitleId}"; Stream? sfoStream = null; @@ -65,7 +65,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) private void ParseSfoStream(Stream sfoStream, out ParamSfo sfo) { sfo = new ParamSfo(sfoStream); - GameInformation info = this.Pipeline.GameInformation!; + GameInformation info = this.Game; info.Version = "01.00"; if (sfo.Table.TryGetValue("APP_VER", out object? value)) diff --git a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs index f8873c3..f060ce8 100644 --- a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs +++ b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs @@ -20,7 +20,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) throw new Exception("Unable to retrieve the content ID from the game's EBOOT."); this.Progress = 1f; - this.Pipeline.GameInformation!.ContentId = contentId; + this.Game.ContentId = contentId; State.Logger.LogDebug(InfoRetrieval, "Got content ID from the game's EBOOT: {0}", contentId); return Task.CompletedTask; } From a58f51d72300377380453c9ff88ff9b5bb8db5a1 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 23:06:44 -0400 Subject: [PATCH 14/20] Move weird fields in Pipeline to GameInformation and EncryptionDetails --- Refresher.Core/Patching/EncryptionDetails.cs | 7 +++++++ Refresher.Core/Patching/GameInformation.cs | 3 +++ Refresher.Core/Pipelines/Pipeline.cs | 5 +---- Refresher.Core/Pipelines/Step.cs | 1 + Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs | 4 ++-- Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs | 2 +- Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs | 4 ++-- .../Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs | 2 +- Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs | 8 ++++---- Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs | 2 +- 10 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 Refresher.Core/Patching/EncryptionDetails.cs diff --git a/Refresher.Core/Patching/EncryptionDetails.cs b/Refresher.Core/Patching/EncryptionDetails.cs new file mode 100644 index 0000000..2ba80ba --- /dev/null +++ b/Refresher.Core/Patching/EncryptionDetails.cs @@ -0,0 +1,7 @@ +namespace Refresher.Core.Patching; + +public class EncryptionDetails +{ + public string? LicenseDirectory { get; internal set; } + public string? DownloadedActDatPath { get; internal set; } +} \ No newline at end of file diff --git a/Refresher.Core/Patching/GameInformation.cs b/Refresher.Core/Patching/GameInformation.cs index b8dc04e..b20818a 100644 --- a/Refresher.Core/Patching/GameInformation.cs +++ b/Refresher.Core/Patching/GameInformation.cs @@ -7,6 +7,9 @@ public class GameInformation 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() { diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index 1700160..88f15e9 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -17,10 +17,7 @@ public abstract class Pipeline public IPatcher? Patcher { get; set; } public PatchAccessor? Accessor { get; internal set; } public GameInformation? GameInformation { get; internal set; } - public string? LicenseDirectory { get; internal set; } - public string? DownloadedEbootPath { get; set; } - public string? DownloadedActDatPath { get; internal set; } - public string? DecryptedEbootPath { get; internal set; } + public EncryptionDetails? EncryptionDetails { get; internal set; } public PipelineState State { get; private set; } = PipelineState.NotStarted; diff --git a/Refresher.Core/Pipelines/Step.cs b/Refresher.Core/Pipelines/Step.cs index 0027446..2b3032d 100644 --- a/Refresher.Core/Pipelines/Step.cs +++ b/Refresher.Core/Pipelines/Step.cs @@ -10,6 +10,7 @@ public abstract class Step public virtual List Inputs { get; } = []; protected GameInformation Game => this.Pipeline.GameInformation!; + protected EncryptionDetails Encryption => this.Pipeline.EncryptionDetails!; protected Step(Pipeline pipeline) { diff --git a/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs b/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs index 8e4d05e..1a585b0 100644 --- a/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs +++ b/Refresher.Core/Pipelines/Steps/DecryptGameEbootStep.cs @@ -10,8 +10,8 @@ public DecryptGameEbootStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - string tempFile = this.Pipeline.DecryptedEbootPath = Path.GetTempFileName(); - LibSceToolSharp.Decrypt(this.Pipeline.DownloadedEbootPath!, tempFile); + 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) diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs index d06b108..4f3e05a 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadGameEbootStep.cs @@ -38,7 +38,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) PatchAccessor.Try(() => { downloadedFile = this.Pipeline.Accessor!.DownloadFile(ebootPath); - this.Pipeline.DownloadedEbootPath = downloadedFile; + this.Game.DownloadedEbootPath = downloadedFile; }); State.Logger.LogDebug(Accessor, $"Downloaded EBOOT Path: {downloadedFile}"); diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs index e19df61..8499c97 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs @@ -17,7 +17,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) string licenseDir = Path.Join(Path.GetTempPath(), "refresher-" + Random.Shared.Next()); Directory.CreateDirectory(licenseDir); - this.Pipeline.LicenseDirectory = licenseDir; + this.Encryption.LicenseDirectory = licenseDir; bool found = false; foreach (string user in this.Pipeline.Accessor!.GetDirectoriesInDirectory(Path.Combine("home"))) @@ -45,7 +45,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) if (!found && this.Pipeline.Accessor.FileExists(actDatPath)) { string downloadedActDat = this.Pipeline.Accessor.DownloadFile(actDatPath); - this.Pipeline.DownloadedActDatPath = downloadedActDat; + this.Encryption.DownloadedActDatPath = downloadedActDat; } //And the license file diff --git a/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs b/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs index 2b53723..07bdce0 100644 --- a/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs +++ b/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs @@ -18,7 +18,7 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) { string url = this.Pipeline.Inputs["url"]; - using Stream stream = File.Open(this.Pipeline.DecryptedEbootPath!, FileMode.Open); + using Stream stream = File.Open(this.Game.DecryptedEbootPath!, FileMode.Open); EbootPatcher patcher = new(stream); this.Pipeline.Patcher = patcher; diff --git a/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs b/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs index c0fe4ff..e63707a 100644 --- a/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs +++ b/Refresher.Core/Pipelines/Steps/PrepareSceToolStep.cs @@ -13,11 +13,11 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) { Init(); - SetRapDirectory(this.Pipeline.LicenseDirectory!); - SetRifPath(this.Pipeline.LicenseDirectory!); + SetRapDirectory(this.Encryption.LicenseDirectory!); + SetRifPath(this.Encryption.LicenseDirectory!); - if(this.Pipeline.DownloadedActDatPath != null) - SetActDatFilePath(this.Pipeline.DownloadedActDatPath); + if(this.Encryption.DownloadedActDatPath != null) + SetActDatFilePath(this.Encryption.DownloadedActDatPath); return Task.CompletedTask; } diff --git a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs index f060ce8..2777cdf 100644 --- a/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs +++ b/Refresher.Core/Pipelines/Steps/ReadEbootContentIdStep.cs @@ -10,7 +10,7 @@ public ReadEbootContentIdStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override Task ExecuteAsync(CancellationToken cancellationToken = default) { - string ebootPath = this.Pipeline.DownloadedEbootPath!; + string ebootPath = this.Game.DownloadedEbootPath!; LibSceToolSharp.Init(); this.Progress = 0.5f; From b31ca336b18d4443e3fbe273c29a1910223b3f93 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 23:18:36 -0400 Subject: [PATCH 15/20] oopsie --- Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs | 5 ++++- Refresher/UI/PipelineForm.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs index 8499c97..a5d54d4 100644 --- a/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs +++ b/Refresher.Core/Pipelines/Steps/DownloadGameLicenseStep.cs @@ -17,7 +17,10 @@ public override Task ExecuteAsync(CancellationToken cancellationToken = default) string licenseDir = Path.Join(Path.GetTempPath(), "refresher-" + Random.Shared.Next()); Directory.CreateDirectory(licenseDir); - this.Encryption.LicenseDirectory = licenseDir; + this.Pipeline.EncryptionDetails = new EncryptionDetails() + { + LicenseDirectory = licenseDir, + }; bool found = false; foreach (string user in this.Pipeline.Accessor!.GetDirectoriesInDirectory(Path.Combine("home"))) diff --git a/Refresher/UI/PipelineForm.cs b/Refresher/UI/PipelineForm.cs index c471865..469e3a2 100644 --- a/Refresher/UI/PipelineForm.cs +++ b/Refresher/UI/PipelineForm.cs @@ -146,7 +146,7 @@ private void OnButtonClick(object? sender, EventArgs e) } catch (Exception ex) { - State.Logger.LogError(LogType.Pipeline, $"Error while running pipeline {this._pipeline.Name}: {ex.Message}"); + State.Logger.LogError(LogType.Pipeline, $"Error while running pipeline {this._pipeline.Name}: {ex}"); } }, this._cts?.Token ?? default); From 619c6d974f33d9680f3b6bd7b9a289d4469751d2 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 12 Oct 2024 23:34:50 -0400 Subject: [PATCH 16/20] ApplyPatchToEbootStep --- .../Pipelines/RPCS3PatchPipeline.cs | 1 + .../Pipelines/Steps/ApplyPatchToEbootStep.cs | 20 +++++++++++++++++++ .../Steps/PrepareEbootPatcherAndVerifyStep.cs | 5 ++--- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 Refresher.Core/Pipelines/Steps/ApplyPatchToEbootStep.cs diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index 41a934f..d0c5f6b 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -20,5 +20,6 @@ public class RPCS3PatchPipeline : Pipeline typeof(PrepareSceToolStep), typeof(DecryptGameEbootStep), typeof(PrepareEbootPatcherAndVerifyStep), + typeof(ApplyPatchToEbootStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/ApplyPatchToEbootStep.cs b/Refresher.Core/Pipelines/Steps/ApplyPatchToEbootStep.cs new file mode 100644 index 0000000..9418c72 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/ApplyPatchToEbootStep.cs @@ -0,0 +1,20 @@ +namespace Refresher.Core.Pipelines.Steps; + +public class ApplyPatchToEbootStep : Step +{ + public ApplyPatchToEbootStep(Pipeline pipeline) : base(pipeline) + {} + + public override List 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; + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs b/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs index 07bdce0..e7d7f6f 100644 --- a/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs +++ b/Refresher.Core/Pipelines/Steps/PrepareEbootPatcherAndVerifyStep.cs @@ -17,9 +17,8 @@ public PrepareEbootPatcherAndVerifyStep(Pipeline pipeline) : base(pipeline) public override Task ExecuteAsync(CancellationToken cancellationToken = default) { string url = this.Pipeline.Inputs["url"]; - - using Stream stream = File.Open(this.Game.DecryptedEbootPath!, FileMode.Open); - EbootPatcher patcher = new(stream); + + EbootPatcher patcher = new(File.Open(this.Game.DecryptedEbootPath!, FileMode.Open, FileAccess.ReadWrite)); this.Pipeline.Patcher = patcher; From b65e2c6264caac6fa18551d360226777ff88de51 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sun, 13 Oct 2024 19:38:41 -0400 Subject: [PATCH 17/20] Initialize a patcher with RPCS3 patch creation options --- Refresher.Core/Pipelines/CommonStepInputs.cs | 30 ++++++++++-- .../Pipelines/RPCS3PatchPipeline.cs | 2 +- .../PrepareEbootPatchCreatorAndVerifyStep.cs | 46 +++++++++++++++++++ .../Steps/SetupEmulatorAccessorStep.cs | 24 +--------- 4 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 Refresher.Core/Pipelines/Steps/PrepareEbootPatchCreatorAndVerifyStep.cs diff --git a/Refresher.Core/Pipelines/CommonStepInputs.cs b/Refresher.Core/Pipelines/CommonStepInputs.cs index 5d24dc0..fd17d2a 100644 --- a/Refresher.Core/Pipelines/CommonStepInputs.cs +++ b/Refresher.Core/Pipelines/CommonStepInputs.cs @@ -1,14 +1,38 @@ namespace Refresher.Core.Pipelines; -internal class CommonStepInputs +internal static class CommonStepInputs { - internal static readonly StepInput TitleId = new("title-id", "Game to patch", StepInputType.Game) + internal static readonly StepInput TitleId = new("title-id", "Game", StepInputType.Game) { Placeholder = "NPUA80662", }; - internal static readonly StepInput ServerUrl = new("url", "Server URL to patch to") + 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; + } } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index d0c5f6b..b24e366 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -19,7 +19,7 @@ public class RPCS3PatchPipeline : Pipeline // Decryption and patch stage typeof(PrepareSceToolStep), typeof(DecryptGameEbootStep), - typeof(PrepareEbootPatcherAndVerifyStep), + typeof(PrepareEbootPatchCreatorAndVerifyStep), typeof(ApplyPatchToEbootStep), ]; } \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/PrepareEbootPatchCreatorAndVerifyStep.cs b/Refresher.Core/Pipelines/Steps/PrepareEbootPatchCreatorAndVerifyStep.cs new file mode 100644 index 0000000..97d2572 --- /dev/null +++ b/Refresher.Core/Pipelines/Steps/PrepareEbootPatchCreatorAndVerifyStep.cs @@ -0,0 +1,46 @@ +using Refresher.Core.Patching; +using Refresher.Core.Verification; + +namespace Refresher.Core.Pipelines.Steps; + +public class PrepareEbootPatchCreatorAndVerifyStep : Step +{ + public PrepareEbootPatchCreatorAndVerifyStep(Pipeline pipeline) : base(pipeline) + {} + + public override List Inputs => + [ + CommonStepInputs.RPCS3Folder, + CommonStepInputs.ServerUrl, + ]; + + public override float Progress { get; protected set; } + public override Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string url = this.Pipeline.Inputs["url"]; + + EbootPatcher patcher = new(File.Open(this.Game.DecryptedEbootPath!, FileMode.Open, FileAccess.ReadWrite)); + patcher.GenerateRpcs3Patch = true; + patcher.Rpcs3PatchFolder = Path.GetFullPath(Path.Combine(this.Pipeline.Inputs["hdd0-path"], "..", "patches")); + patcher.TitleId = this.Game.TitleId; + patcher.GameName = this.Game.Name; + patcher.GameVersion = this.Game.Version; + + State.Logger.LogDebug(RPCS3, $"RPCS3 patches folder: {patcher.Rpcs3PatchFolder}"); + + this.Pipeline.Patcher = patcher; + + List messages = patcher.Verify(url, true); // TODO: handle autodiscover in pipelines + foreach (Message message in messages) + { + State.Logger.LogInfo(Patcher, message.ToString()); + } + + if (messages.Any(m => m.Level == MessageLevel.Error)) + { + throw new Exception("There were errors while verifying the patch details against the EBOOT. Check the log for more information."); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs b/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs index 47a6dc3..dce914f 100644 --- a/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs +++ b/Refresher.Core/Pipelines/Steps/SetupEmulatorAccessorStep.cs @@ -10,31 +10,9 @@ public SetupEmulatorAccessorStep(Pipeline pipeline) : base(pipeline) public override float Progress { get; protected set; } public override List Inputs { get; } = [ - 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 = DetermineDefaultPath, - }, + CommonStepInputs.RPCS3Folder, ]; - // TODO: Cache the last used location for easier entry - private static string? DetermineDefaultPath() - { - // 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; - } - public override Task ExecuteAsync(CancellationToken cancellationToken = default) { string path = this.Inputs[0].GetValueFromPipeline(this.Pipeline); From bee490250d8ce0ddbe1fdd31fd4ec04a54c7c9bd Mon Sep 17 00:00:00 2001 From: jvyden Date: Sun, 13 Oct 2024 19:43:28 -0400 Subject: [PATCH 18/20] Don't allow newlines in the game name Closes #69 --- Refresher.Core/Patching/EbootPatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Refresher.Core/Patching/EbootPatcher.cs b/Refresher.Core/Patching/EbootPatcher.cs index 6d10205..78d4c2f 100644 --- a/Refresher.Core/Patching/EbootPatcher.cs +++ b/Refresher.Core/Patching/EbootPatcher.cs @@ -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}" From 32c9ce1abfc244af2ff69ebe989ddb972abda0e3 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sun, 13 Oct 2024 19:45:52 -0400 Subject: [PATCH 19/20] Fix patcher setter allowing too much access --- Refresher.Core/Pipelines/Pipeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Refresher.Core/Pipelines/Pipeline.cs b/Refresher.Core/Pipelines/Pipeline.cs index 88f15e9..de27034 100644 --- a/Refresher.Core/Pipelines/Pipeline.cs +++ b/Refresher.Core/Pipelines/Pipeline.cs @@ -14,7 +14,7 @@ public abstract class Pipeline public readonly Dictionary Inputs = []; public FrozenSet RequiredInputs { get; private set; } - public IPatcher? Patcher { get; set; } + public IPatcher? Patcher { get; internal set; } public PatchAccessor? Accessor { get; internal set; } public GameInformation? GameInformation { get; internal set; } public EncryptionDetails? EncryptionDetails { get; internal set; } From 8c092a95b4401d8ce55002691af6cbcac58d3750 Mon Sep 17 00:00:00 2001 From: jvyden Date: Sun, 13 Oct 2024 19:46:48 -0400 Subject: [PATCH 20/20] Clarify patch creator behavior in patch pipeline --- Refresher.Core/Pipelines/RPCS3PatchPipeline.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs index b24e366..ccfcf46 100644 --- a/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs +++ b/Refresher.Core/Pipelines/RPCS3PatchPipeline.cs @@ -21,5 +21,6 @@ public class RPCS3PatchPipeline : Pipeline typeof(DecryptGameEbootStep), typeof(PrepareEbootPatchCreatorAndVerifyStep), typeof(ApplyPatchToEbootStep), + // The patch creator will automatically write to the patch file. No upload steps are required. ]; } \ No newline at end of file