diff --git a/.idea/.idea.Refresher/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.Refresher/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/.idea.Refresher/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/Refresher.sln.DotSettings b/Refresher.sln.DotSettings index 495d1ea..7357d64 100644 --- a/Refresher.sln.DotSettings +++ b/Refresher.sln.DotSettings @@ -8,7 +8,9 @@ UseExplicitType UseExplicitType LBP + PSP True + True True True True diff --git a/Refresher/CLI/CommandLine.cs b/Refresher/CLI/CommandLine.cs index fbc5cf6..151c307 100644 --- a/Refresher/CLI/CommandLine.cs +++ b/Refresher/CLI/CommandLine.cs @@ -64,8 +64,8 @@ void DeleteTempFile(string? s) } //Create a new patcher with the temp file stream - Patcher patcher = new(mappedFile.CreateViewStream()); - List messages = patcher.Verify(options.ServerUrl, options.Digest ?? false).ToList(); + EbootPatcher ebootPatcher = new(mappedFile.CreateViewStream()); + List messages = ebootPatcher.Verify(options.ServerUrl, options.Digest ?? false).ToList(); //Write the messages to the console foreach (Message message in messages) Console.WriteLine($"{message.Level}: {message.Content}"); @@ -103,7 +103,7 @@ void DeleteTempFile(string? s) try { //Patch the file - patcher.Patch(options.ServerUrl, options.Digest ?? false); + ebootPatcher.Patch(options.ServerUrl, options.Digest ?? false); //TODO: warn the user if they are overwriting the file File.Move(tempFile, options.OutputFile, true); diff --git a/Refresher/Patching/Patcher.cs b/Refresher/Patching/EbootPatcher.cs similarity index 99% rename from Refresher/Patching/Patcher.cs rename to Refresher/Patching/EbootPatcher.cs index b93d17c..dfa4a56 100644 --- a/Refresher/Patching/Patcher.cs +++ b/Refresher/Patching/EbootPatcher.cs @@ -8,11 +8,11 @@ namespace Refresher.Patching; -public partial class Patcher +public partial class EbootPatcher : IPatcher { private readonly Lazy> _targets; - public Patcher(Stream stream) + public EbootPatcher(Stream stream) { if (!stream.CanRead || !stream.CanSeek || !stream.CanWrite) throw new ArgumentException("Stream must be readable, seekable and writable", nameof(stream)); diff --git a/Refresher/Patching/IPatcher.cs b/Refresher/Patching/IPatcher.cs new file mode 100644 index 0000000..6efd914 --- /dev/null +++ b/Refresher/Patching/IPatcher.cs @@ -0,0 +1,10 @@ +using Refresher.Verification; + +namespace Refresher.Patching; + +public interface IPatcher +{ + public List Verify(string url, bool patchDigest); + + public void Patch(string url, bool patchDigest); +} \ No newline at end of file diff --git a/Refresher/Patching/PSP/PSPPluginListEntry.cs b/Refresher/Patching/PSP/PSPPluginListEntry.cs new file mode 100644 index 0000000..e2dcd7c --- /dev/null +++ b/Refresher/Patching/PSP/PSPPluginListEntry.cs @@ -0,0 +1,13 @@ +namespace Refresher.Patching.PSP; + +public class PSPPluginListEntry +{ + public string Path; + public int? Type; + + public PSPPluginListEntry(string path, int? type = null) + { + this.Path = path; + this.Type = type; + } +} \ No newline at end of file diff --git a/Refresher/Patching/PSP/PSPPluginListParser.cs b/Refresher/Patching/PSP/PSPPluginListParser.cs new file mode 100644 index 0000000..0f6f5ca --- /dev/null +++ b/Refresher/Patching/PSP/PSPPluginListParser.cs @@ -0,0 +1,44 @@ +namespace Refresher.Patching.PSP; + +public static class PSPPluginListParser +{ + public static List Parse(TextReader reader) + { + List list = new(); + + while (reader.ReadLine() is { } line) + { + //Skip blank lines + if (string.IsNullOrWhiteSpace(line)) continue; + + string[] parts = line.Split(" "); + + PSPPluginListEntry entry = new(parts[0]); + + if (parts.Length > 1) + { + entry.Type = int.Parse(parts[1]); + } + + list.Add(entry); + } + + return list; + } + + public static void Write(List list, TextWriter writer) + { + foreach (PSPPluginListEntry entry in list) + { + writer.Write(entry.Path); + if (entry.Type.HasValue) + { + writer.Write(' '); + writer.Write(entry.Type.Value); + } + writer.Write('\n'); + } + + writer.Flush(); + } +} \ No newline at end of file diff --git a/Refresher/Patching/PSPPatcher.cs b/Refresher/Patching/PSPPatcher.cs new file mode 100644 index 0000000..cf93df1 --- /dev/null +++ b/Refresher/Patching/PSPPatcher.cs @@ -0,0 +1,118 @@ +using System.Reflection; +using Refresher.Patching.PSP; +using Refresher.Verification; + +namespace Refresher.Patching; + +public class PSPPatcher : IPatcher +{ + public string? PSPDrivePath; + + private readonly Stream _allefresher; + + public PSPPatcher() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + this._allefresher = assembly.GetManifestResourceStream("Refresher.Resources.Allefresher.prx")!; + } + + public List Verify(string url, bool patchDigest) + { + List messages = new(); + + if (!Uri.TryCreate(url, UriKind.Absolute, out _)) + messages.Add(new Message(MessageLevel.Error, "URL failed to parse!")); + + if (string.IsNullOrEmpty(this.PSPDrivePath) || !Directory.Exists(this.PSPDrivePath)) + messages.Add(new Message(MessageLevel.Error, "Invalid PSP Drive path!")); + + return messages; + } + + public void Patch(string url, bool patchDigest) + { + Uri uri = new(url); + + string domain = uri.Host; + string format = $"{uri.Scheme}://%s:{uri.Port}{uri.AbsolutePath}%s"; + + string pluginsDir = Path.Combine(this.PSPDrivePath!, "SEPLUGINS"); + + //If the plugins directory does not exist + if (!Directory.Exists(pluginsDir)) + { + //Create it + Directory.CreateDirectory(pluginsDir); + } + + string domainPath = Path.Combine(pluginsDir, "Allefresher_domain.txt"); + string formatPath = Path.Combine(pluginsDir, "Allefresher_format.txt"); + + //Delete the existing domain and format configuration files + File.Delete(domainPath); + File.Delete(formatPath); + + //Write the new domain and format configuration + File.WriteAllText(domainPath, domain); + File.WriteAllText(formatPath, format); + + //Match for all files called "game.txt" in the plugins directory + //NOTE: we do this because the PSP filesystem is case insensitive, and the .NET STL is case sensitive on linux + List possibleMatches = Directory.EnumerateFiles(pluginsDir, "game.txt", new EnumerationOptions + { + MatchCasing = MatchCasing.CaseInsensitive, + }).ToList(); + + FileStream gamePluginsFileStream; + + const string allefresherPath = "ms0:/SEPLUGINS/Allefresher.prx"; + + List entries; + if (possibleMatches.Any()) + { + //Open the first match + FileStream stream = File.OpenRead(possibleMatches[0]); + + //Read out the matches + entries = PSPPluginListParser.Parse(new StreamReader(stream)); + + //If Allefresher is not in the list, + if (!entries.Any(entry => entry.Path.Contains("Allefresher.prx", StringComparison.InvariantCultureIgnoreCase))) + { + //Add Allefresher to the game plugin list + entries.Add(new PSPPluginListEntry(allefresherPath, 1)); + } + + //Dispose the read stream + stream.Dispose(); + + //Open a new write stream to the game.txt file + gamePluginsFileStream = File.Open(possibleMatches[0], FileMode.Truncate); + } + else + { + //Create a new list, with the only entry being Allefresher + entries = new List + { + new(allefresherPath, 1), + }; + + //Create a new game.txt file + gamePluginsFileStream = File.Open(Path.Combine(pluginsDir, "game.txt"), FileMode.CreateNew); + } + + //Write the plugin list to the file + PSPPluginListParser.Write(entries, new StreamWriter(gamePluginsFileStream)); + + //Flush and dispose the stream + gamePluginsFileStream.Flush(); + gamePluginsFileStream.Dispose(); + + using FileStream allefresherOutput = File.Open(Path.Combine(pluginsDir, "Allefresher.prx"), FileMode.Create); + + this._allefresher.Seek(0, SeekOrigin.Begin); + + //Copy the Allefresher embedded resource to the output file + this._allefresher.CopyTo(allefresherOutput); + } +} \ No newline at end of file diff --git a/Refresher/Refresher.csproj b/Refresher/Refresher.csproj index e6c6328..9859299 100644 --- a/Refresher/Refresher.csproj +++ b/Refresher/Refresher.csproj @@ -24,6 +24,7 @@ + diff --git a/Refresher/Resources/Allefresher.prx b/Refresher/Resources/Allefresher.prx new file mode 100644 index 0000000..e12e695 Binary files /dev/null and b/Refresher/Resources/Allefresher.prx differ diff --git a/Refresher/UI/FilePatchForm.cs b/Refresher/UI/FilePatchForm.cs index 910556a..b3c0d6a 100644 --- a/Refresher/UI/FilePatchForm.cs +++ b/Refresher/UI/FilePatchForm.cs @@ -5,7 +5,7 @@ namespace Refresher.UI; -public class FilePatchForm : PatchForm +public class FilePatchForm : PatchForm { private readonly FilePicker _inputFileField; private readonly FilePicker _outputFileField; @@ -92,7 +92,7 @@ private void FileUpdated(object? sender, EventArgs ev) { this._mappedFile?.Dispose(); this._mappedFile = MemoryMappedFile.CreateFromFile(this._tempFile, FileMode.Open, null, 0, MemoryMappedFileAccess.ReadWrite); - this.Patcher = new Patcher(this._mappedFile.CreateViewStream()); + this.Patcher = new EbootPatcher(this._mappedFile.CreateViewStream()); } catch(Exception e) { diff --git a/Refresher/UI/IntegratedPatchForm.cs b/Refresher/UI/IntegratedPatchForm.cs index 49011ae..5c0c6f3 100644 --- a/Refresher/UI/IntegratedPatchForm.cs +++ b/Refresher/UI/IntegratedPatchForm.cs @@ -10,7 +10,7 @@ namespace Refresher.UI; -public abstract class IntegratedPatchForm : PatchForm +public abstract class IntegratedPatchForm : PatchForm { private readonly DropDown _gameDropdown; private readonly TextBox? _outputField; @@ -29,8 +29,10 @@ protected IntegratedPatchForm(string subtitle) : base(subtitle) { this.AddRemoteField(), AddField("Game to patch", out this._gameDropdown, forceHeight: 56), - AddField("Server URL", out this.UrlField), + AddField("Server URL", out this.UrlField), }; + + this._gameDropdown.SelectedValueChanged += this.GameChanged; if (!this.ShouldReplaceExecutable) { @@ -40,8 +42,6 @@ protected IntegratedPatchForm(string subtitle) : base(subtitle) this.FormPanel = new TableLayout(rows); - this._gameDropdown.SelectedValueChanged += this.GameChanged; - this.InitializePatcher(); } @@ -148,7 +148,7 @@ protected virtual void GameChanged(object? sender, EventArgs ev) this.LogMessage($"The EBOOT has been successfully decrypted. It's stored at {this._tempFile}."); - this.Patcher = new Patcher(File.Open(this._tempFile, FileMode.Open, FileAccess.ReadWrite)); + this.Patcher = new EbootPatcher(File.Open(this._tempFile, FileMode.Open, FileAccess.ReadWrite)); this.Reverify(sender, ev); } @@ -191,7 +191,7 @@ public override void CompletePatch(object? sender, EventArgs e) { Thread.Sleep(1000); // TODO: don't. block. the. main. thread. this.Accessor.UploadFile(fileToUpload, destination); - MessageBox.Show($"Successfully patched EBOOT! It was saved to '{destination}'."); + MessageBox.Show(this, $"Successfully patched EBOOT! It was saved to '{destination}'.", "Success!"); // Re-initialize patcher so we can patch with the same parameters again // Probably slow but prevents crash @@ -228,6 +228,12 @@ protected virtual void RevertToOriginalExecutable(object? sender, EventArgs e) } protected abstract TableRow AddRemoteField(); + /// + /// Whether the target platform requires the executable to be resigned or not + /// protected abstract bool NeedsResign { get; } + /// + /// Whether the target platform requires the executable to be named EBOOT.BIN + /// protected abstract bool ShouldReplaceExecutable { get; } } \ No newline at end of file diff --git a/Refresher/UI/MainForm.cs b/Refresher/UI/MainForm.cs index fae01b0..b4c2a33 100644 --- a/Refresher/UI/MainForm.cs +++ b/Refresher/UI/MainForm.cs @@ -16,7 +16,8 @@ public class MainForm : RefresherForm 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 = "PS3 Patch" }, + new Button((_, _) => this.ShowChild()) { Text = "PSP Setup" } ); layout.Spacing = 5; diff --git a/Refresher/UI/PSPSetupForm.cs b/Refresher/UI/PSPSetupForm.cs new file mode 100644 index 0000000..941b141 --- /dev/null +++ b/Refresher/UI/PSPSetupForm.cs @@ -0,0 +1,74 @@ +using Eto.Forms; +using Refresher.Patching; + +namespace Refresher.UI; + +public class PSPSetupForm : PatchForm +{ + private DropDown _pspDrive; + + protected override TableLayout FormPanel { get; } + + public PSPSetupForm() : base("PSP Setup") + { + this.Patcher = new PSPPatcher(); + + this.FormPanel = new TableLayout(new List + { + AddField("PSP Drive", out this._pspDrive), + AddField("Server URL", out this.UrlField), + }); + + this._pspDrive.SelectedKeyChanged += this.Reverify; + this._pspDrive.SelectedKeyChanged += this.SelectedDriveChange; + + DriveInfo[] drives = DriveInfo.GetDrives(); + + foreach (DriveInfo drive in drives) + { + try + { + Console.WriteLine($"Checking drive {drive.Name}..."); + + //If theres no PSP folder, + if (!Directory.Exists(Path.Combine(drive.RootDirectory.FullName, "PSP"))) + { + Console.WriteLine($"Drive {drive.Name} has no PSP folder, ignoring..."); + + //Skip this drive + continue; + } + } + catch(Exception ex) + { + Console.WriteLine($"Checking drive failed due to exception, see below"); + Console.WriteLine(ex); + + //If we fail to check dir info, its probably not mounted in a safe/accessible way + continue; + } + + //If the drive has a PSP folder, add it to the list + this._pspDrive.Items.Add(drive.Name, drive.RootDirectory.FullName); + } + + // If there are any items in the dropdown... + if (this._pspDrive.Items.Count > 0) + { + // ...then select the first item. + this._pspDrive.SelectedIndex = 0; + } + + this.InitializePatcher(); + } + + private void SelectedDriveChange(object? sender, EventArgs e) + { + this.Patcher!.PSPDrivePath = this._pspDrive.SelectedKey; + } + + public override void CompletePatch(object? sender, EventArgs e) + { + MessageBox.Show(this, "Setup complete! *Safely* eject your Memory Stick or PSP in your OS, then open the game!", "Success!"); + } +} \ No newline at end of file diff --git a/Refresher/UI/PatchForm.cs b/Refresher/UI/PatchForm.cs index 65dd42d..c1b0555 100644 --- a/Refresher/UI/PatchForm.cs +++ b/Refresher/UI/PatchForm.cs @@ -11,7 +11,7 @@ namespace Refresher.UI; -public abstract class PatchForm : RefresherForm where TPatcher : Patcher +public abstract class PatchForm : RefresherForm where TPatcher : class, IPatcher { protected abstract TableLayout FormPanel { get; }