using System; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; namespace SplashEdit.EditorCode { /// /// Downloads and manages mkpsxiso — the tool that builds PlayStation CD images /// from an XML catalog. Used for the ISO build target. /// https://github.com/Lameguy64/mkpsxiso /// public static class MkpsxisoDownloader { private const string MKPSXISO_VERSION = "2.20"; private const string MKPSXISO_RELEASE_BASE = "https://github.com/Lameguy64/mkpsxiso/releases/download/v" + MKPSXISO_VERSION + "/"; private static readonly HttpClient _http = new HttpClient(); /// /// Install directory for mkpsxiso inside .tools/ /// public static string MkpsxisoDir => Path.Combine(SplashBuildPaths.ToolsDir, "mkpsxiso"); /// /// Path to the mkpsxiso binary. /// public static string MkpsxisoBinary { get { if (Application.platform == RuntimePlatform.WindowsEditor) return Path.Combine(MkpsxisoDir, "mkpsxiso.exe"); return Path.Combine(MkpsxisoDir, "mkpsxiso"); } } /// /// Returns true if mkpsxiso is installed and ready to use. /// public static bool IsInstalled() => File.Exists(MkpsxisoBinary); /// /// Downloads and installs mkpsxiso from the official GitHub releases. /// public static async Task DownloadAndInstall(Action log = null) { string archiveName; switch (Application.platform) { case RuntimePlatform.WindowsEditor: archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-win64.zip"; break; case RuntimePlatform.LinuxEditor: archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-Linux.zip"; break; case RuntimePlatform.OSXEditor: archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-Darwin.zip"; break; default: log?.Invoke("Unsupported platform for mkpsxiso."); return false; } string downloadUrl = $"{MKPSXISO_RELEASE_BASE}{archiveName}"; log?.Invoke($"Downloading mkpsxiso: {downloadUrl}"); try { string tempFile = Path.Combine(Path.GetTempPath(), archiveName); EditorUtility.DisplayProgressBar("Downloading mkpsxiso", "Downloading...", 0.1f); using (var response = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); long? totalBytes = response.Content.Headers.ContentLength; long downloaded = 0; using (var fs = File.Create(tempFile)) using (var stream = await response.Content.ReadAsStreamAsync()) { byte[] buffer = new byte[81920]; int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await fs.WriteAsync(buffer, 0, bytesRead); downloaded += bytesRead; if (totalBytes.HasValue) { float progress = (float)downloaded / totalBytes.Value; EditorUtility.DisplayProgressBar("Downloading mkpsxiso", $"{downloaded / 1024}/{totalBytes.Value / 1024} KB", progress); } } } } log?.Invoke("Extracting..."); EditorUtility.DisplayProgressBar("Installing mkpsxiso", "Extracting...", 0.9f); string installDir = MkpsxisoDir; if (Directory.Exists(installDir)) Directory.Delete(installDir, true); Directory.CreateDirectory(installDir); System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir); // Fix nested directory (archives often have one extra level) SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir); try { File.Delete(tempFile); } catch { } EditorUtility.ClearProgressBar(); if (IsInstalled()) { // Make executable on Linux/Mac if (Application.platform != RuntimePlatform.WindowsEditor) { var chmod = Process.Start("chmod", $"+x \"{MkpsxisoBinary}\""); chmod?.WaitForExit(); } log?.Invoke("mkpsxiso installed successfully!"); return true; } log?.Invoke($"mkpsxiso binary not found at: {MkpsxisoBinary}"); return false; } catch (Exception ex) { log?.Invoke($"mkpsxiso download failed: {ex.Message}"); EditorUtility.ClearProgressBar(); return false; } } /// /// Runs mkpsxiso with the given XML catalog to produce a BIN/CUE image. /// /// Path to the mkpsxiso XML catalog. /// Override output .bin path (optional, uses XML default if null). /// Override output .cue path (optional, uses XML default if null). /// Logging callback. /// True if mkpsxiso succeeded. public static bool BuildISO(string xmlPath, string outputBin = null, string outputCue = null, Action log = null) { if (!IsInstalled()) { log?.Invoke("mkpsxiso is not installed."); return false; } // Build arguments string args = $"-y \"{xmlPath}\""; if (!string.IsNullOrEmpty(outputBin)) args += $" -o \"{outputBin}\""; if (!string.IsNullOrEmpty(outputCue)) args += $" -c \"{outputCue}\""; log?.Invoke($"Running: mkpsxiso {args}"); var psi = new ProcessStartInfo { FileName = MkpsxisoBinary, Arguments = args, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; try { var process = Process.Start(psi); string stdout = process.StandardOutput.ReadToEnd(); string stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); if (!string.IsNullOrEmpty(stdout)) log?.Invoke(stdout.Trim()); if (process.ExitCode != 0) { if (!string.IsNullOrEmpty(stderr)) log?.Invoke($"mkpsxiso error: {stderr.Trim()}"); log?.Invoke($"mkpsxiso exited with code {process.ExitCode}"); return false; } log?.Invoke("ISO image built successfully."); return true; } catch (Exception ex) { log?.Invoke($"mkpsxiso execution failed: {ex.Message}"); return false; } } } }