Broken RUntime
This commit is contained in:
230
Editor/Core/MkpsxisoDownloader.cs
Normal file
230
Editor/Core/MkpsxisoDownloader.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// Install directory for mkpsxiso inside .tools/
|
||||
/// </summary>
|
||||
public static string MkpsxisoDir =>
|
||||
Path.Combine(SplashBuildPaths.ToolsDir, "mkpsxiso");
|
||||
|
||||
/// <summary>
|
||||
/// Path to the mkpsxiso binary.
|
||||
/// </summary>
|
||||
public static string MkpsxisoBinary
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||
return Path.Combine(MkpsxisoDir, "mkpsxiso.exe");
|
||||
return Path.Combine(MkpsxisoDir, "mkpsxiso");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if mkpsxiso is installed and ready to use.
|
||||
/// </summary>
|
||||
public static bool IsInstalled() => File.Exists(MkpsxisoBinary);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads and installs mkpsxiso from the official GitHub releases.
|
||||
/// </summary>
|
||||
public static async Task<bool> DownloadAndInstall(Action<string> 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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FixNestedDirectory(string dir)
|
||||
{
|
||||
// If extraction created exactly one subdirectory, flatten it
|
||||
var subdirs = Directory.GetDirectories(dir);
|
||||
if (subdirs.Length == 1)
|
||||
{
|
||||
string nested = subdirs[0];
|
||||
foreach (string file in Directory.GetFiles(nested))
|
||||
{
|
||||
string dest = Path.Combine(dir, Path.GetFileName(file));
|
||||
if (!File.Exists(dest)) File.Move(file, dest);
|
||||
}
|
||||
foreach (string sub in Directory.GetDirectories(nested))
|
||||
{
|
||||
string dest = Path.Combine(dir, Path.GetFileName(sub));
|
||||
if (!Directory.Exists(dest)) Directory.Move(sub, dest);
|
||||
}
|
||||
try { Directory.Delete(nested, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs mkpsxiso with the given XML catalog to produce a BIN/CUE image.
|
||||
/// </summary>
|
||||
/// <param name="xmlPath">Path to the mkpsxiso XML catalog.</param>
|
||||
/// <param name="outputBin">Override output .bin path (optional, uses XML default if null).</param>
|
||||
/// <param name="outputCue">Override output .cue path (optional, uses XML default if null).</param>
|
||||
/// <param name="log">Logging callback.</param>
|
||||
/// <returns>True if mkpsxiso succeeded.</returns>
|
||||
public static bool BuildISO(string xmlPath, string outputBin = null,
|
||||
string outputCue = null, Action<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/MkpsxisoDownloader.cs.meta
Normal file
2
Editor/Core/MkpsxisoDownloader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45aea686b641c474dba05b83956d8947
|
||||
@@ -52,17 +52,14 @@ namespace SplashEdit.EditorCode
|
||||
/// </summary>
|
||||
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
|
||||
{
|
||||
string platformSuffix;
|
||||
string archiveName;
|
||||
switch (Application.platform)
|
||||
{
|
||||
case RuntimePlatform.WindowsEditor:
|
||||
platformSuffix = "x86_64-pc-windows-msvc";
|
||||
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.zip";
|
||||
archiveName = $"psxavenc-windows.zip";
|
||||
break;
|
||||
case RuntimePlatform.LinuxEditor:
|
||||
platformSuffix = "x86_64-unknown-linux-gnu";
|
||||
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.tar.gz";
|
||||
archiveName = $"psxavenc-linux.zip";
|
||||
break;
|
||||
default:
|
||||
log?.Invoke("Only Windows and Linux are supported.");
|
||||
|
||||
@@ -130,6 +130,24 @@ namespace SplashEdit.EditorCode
|
||||
public static string CUEOutputPath =>
|
||||
Path.Combine(BuildOutputDir, "psxsplash.cue");
|
||||
|
||||
/// <summary>
|
||||
/// XML catalog path used by mkpsxiso to build the ISO image.
|
||||
/// </summary>
|
||||
public static string ISOCatalogPath =>
|
||||
Path.Combine(BuildOutputDir, "psxsplash.xml");
|
||||
|
||||
/// <summary>
|
||||
/// SYSTEM.CNF file path generated for the ISO image.
|
||||
/// The PS1 BIOS reads this to find and launch the executable.
|
||||
/// </summary>
|
||||
public static string SystemCnfPath =>
|
||||
Path.Combine(BuildOutputDir, "SYSTEM.CNF");
|
||||
|
||||
/// <summary>
|
||||
/// Checks if mkpsxiso is installed in the tools directory.
|
||||
/// </summary>
|
||||
public static bool IsMkpsxisoInstalled() => MkpsxisoDownloader.IsInstalled();
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the build output and tools directories exist.
|
||||
/// Also appends entries to the project .gitignore if not present.
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
@@ -46,6 +47,7 @@ namespace SplashEdit.EditorCode
|
||||
private bool _hasRedux;
|
||||
private bool _hasNativeProject;
|
||||
private bool _hasPsxavenc;
|
||||
private bool _hasMkpsxiso;
|
||||
private string _reduxVersion = "";
|
||||
|
||||
// ───── Native project installer ─────
|
||||
@@ -486,6 +488,22 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// mkpsxiso (ISO builder)
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
DrawStatusIcon(_hasMkpsxiso);
|
||||
GUILayout.Label("mkpsxiso (ISO)", GUILayout.Width(160));
|
||||
GUILayout.FlexibleSpace();
|
||||
if (!_hasMkpsxiso)
|
||||
{
|
||||
if (GUILayout.Button("Download", GUILayout.Width(80)))
|
||||
DownloadMkpsxiso();
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("Installed", EditorStyles.miniLabel);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// Refresh button
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
@@ -653,14 +671,11 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// GTE Scaling
|
||||
EditorGUILayout.LabelField("Export Settings", EditorStyles.boldLabel);
|
||||
SplashSettings.DefaultGTEScaling = EditorGUILayout.FloatField("Default GTE Scaling", SplashSettings.DefaultGTEScaling);
|
||||
SplashSettings.AutoValidateOnExport = EditorGUILayout.Toggle("Auto-Validate on Export", SplashSettings.AutoValidateOnExport);
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// Open dedicated VRAM windows
|
||||
EditorGUILayout.LabelField("Advanced Tools", EditorStyles.boldLabel);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Open VRAM Editor", GUILayout.Height(24)))
|
||||
@@ -673,11 +688,6 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (GUILayout.Button("Open Scene Validator", EditorStyles.miniButton))
|
||||
{
|
||||
PSXSceneValidatorWindow.ShowWindow();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
@@ -711,6 +721,34 @@ namespace SplashEdit.EditorCode
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ISO settings (only for ISO build target)
|
||||
if (SplashSettings.Target == BuildTarget.ISO)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Volume Label:", GUILayout.Width(80));
|
||||
SplashSettings.ISOVolumeLabel = EditorGUILayout.TextField(SplashSettings.ISOVolumeLabel);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("License File:", GUILayout.Width(80));
|
||||
string licensePath = SplashSettings.LicenseFilePath;
|
||||
string displayPath = string.IsNullOrEmpty(licensePath) ? "(none — homebrew)" : Path.GetFileName(licensePath);
|
||||
GUILayout.Label(displayPath, EditorStyles.miniLabel, GUILayout.ExpandWidth(true));
|
||||
if (GUILayout.Button("Browse", EditorStyles.miniButton, GUILayout.Width(60)))
|
||||
{
|
||||
string path = EditorUtility.OpenFilePanel(
|
||||
"Select Sony License File", "", "dat");
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
SplashSettings.LicenseFilePath = path;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(licensePath) &&
|
||||
GUILayout.Button("Clear", EditorStyles.miniButton, GUILayout.Width(40)))
|
||||
{
|
||||
SplashSettings.LicenseFilePath = "";
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
// Big Build & Run button
|
||||
@@ -759,7 +797,7 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
if (GUILayout.Button("Compile Only", EditorStyles.miniButton, GUILayout.Width(100)))
|
||||
{
|
||||
CompileNative();
|
||||
CompileOnly();
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
@@ -774,12 +812,11 @@ namespace SplashEdit.EditorCode
|
||||
/// <summary>
|
||||
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
|
||||
/// </summary>
|
||||
public void BuildAndRun()
|
||||
public async void BuildAndRun()
|
||||
{
|
||||
if (_isBuilding) return;
|
||||
_isBuilding = true;
|
||||
|
||||
// Open the PSX Console so build output is visible immediately
|
||||
var console = EditorWindow.GetWindow<PSXConsoleWindow>();
|
||||
console.titleContent = new GUIContent("PSX Console", EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
|
||||
console.minSize = new Vector2(400, 200);
|
||||
@@ -787,7 +824,6 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: Validate
|
||||
Log("Validating toolchain...", LogType.Log);
|
||||
if (!ValidateToolchain())
|
||||
{
|
||||
@@ -796,7 +832,6 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
Log("Toolchain OK.", LogType.Log);
|
||||
|
||||
// Step 2: Export all scenes
|
||||
Log("Exporting scenes...", LogType.Log);
|
||||
if (!ExportAllScenes())
|
||||
{
|
||||
@@ -805,16 +840,15 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
|
||||
|
||||
// Step 3: Compile native
|
||||
Log("Compiling native code...", LogType.Log);
|
||||
if (!CompileNative())
|
||||
EditorUtility.DisplayProgressBar("SplashEdit", "Compiling native code...", 0.6f);
|
||||
if (!await CompileNativeAsync())
|
||||
{
|
||||
Log("Compilation failed. Check build log.", LogType.Error);
|
||||
return;
|
||||
}
|
||||
Log("Compile succeeded.", LogType.Log);
|
||||
|
||||
// Step 4: Launch
|
||||
Log("Launching...", LogType.Log);
|
||||
Launch();
|
||||
}
|
||||
@@ -852,6 +886,11 @@ namespace SplashEdit.EditorCode
|
||||
Log("PCSX-Redux not found. Click Download in the Toolchain section.", LogType.Error);
|
||||
return false;
|
||||
}
|
||||
if (SplashSettings.Target == BuildTarget.ISO && !_hasMkpsxiso)
|
||||
{
|
||||
Log("mkpsxiso not found. Click Download in the Toolchain section.", LogType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
string nativeDir = SplashBuildPaths.NativeSourceDir;
|
||||
if (string.IsNullOrEmpty(nativeDir) || !Directory.Exists(nativeDir))
|
||||
@@ -902,7 +941,7 @@ namespace SplashEdit.EditorCode
|
||||
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single);
|
||||
|
||||
// Find the exporter
|
||||
var exporter = UnityEngine.Object.FindObjectOfType<PSXSceneExporter>();
|
||||
var exporter = UnityEngine.Object.FindFirstObjectByType<PSXSceneExporter>();
|
||||
if (exporter == null)
|
||||
{
|
||||
Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning);
|
||||
@@ -1051,10 +1090,29 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
// ───── Step 3: Compile ─────
|
||||
|
||||
/// <summary>
|
||||
/// Runs make in the native project directory.
|
||||
/// </summary>
|
||||
public bool CompileNative()
|
||||
private async void CompileOnly()
|
||||
{
|
||||
if (_isBuilding) return;
|
||||
_isBuilding = true;
|
||||
Repaint();
|
||||
try
|
||||
{
|
||||
Log("Compiling native code...", LogType.Log);
|
||||
EditorUtility.DisplayProgressBar("SplashEdit", "Compiling native code...", 0.5f);
|
||||
if (await CompileNativeAsync())
|
||||
Log("Compile succeeded.", LogType.Log);
|
||||
else
|
||||
Log("Compilation failed. Check build log.", LogType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBuilding = false;
|
||||
EditorUtility.ClearProgressBar();
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> CompileNativeAsync()
|
||||
{
|
||||
string nativeDir = SplashBuildPaths.NativeSourceDir;
|
||||
if (string.IsNullOrEmpty(nativeDir))
|
||||
@@ -1064,9 +1122,11 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
|
||||
string buildArg = SplashSettings.Mode == BuildMode.Debug ? "BUILD=Debug" : "";
|
||||
// Run clean first, THEN build — "make clean all -jN" races clean vs build in sub-makes
|
||||
string makeCmd = $"make clean && make all -j{SystemInfo.processorCount} {buildArg}".Trim();
|
||||
|
||||
if (SplashSettings.Target == BuildTarget.ISO)
|
||||
buildArg += " LOADER=cdrom";
|
||||
|
||||
string makeCmd = $"make clean && make all -j{SystemInfo.processorCount} {buildArg}".Trim();
|
||||
Log($"Running: {makeCmd}", LogType.Log);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
@@ -1084,45 +1144,57 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
try
|
||||
{
|
||||
var process = Process.Start(psi);
|
||||
string stdout = process.StandardOutput.ReadToEnd();
|
||||
string stderr = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
||||
var stdoutBuf = new System.Text.StringBuilder();
|
||||
var stderrBuf = new System.Text.StringBuilder();
|
||||
|
||||
// Log output to panel only (no Unity console spam)
|
||||
if (!string.IsNullOrEmpty(stdout))
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
foreach (string line in stdout.Split('\n'))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
LogToPanel(line.Trim(), LogType.Log);
|
||||
}
|
||||
if (e.Data != null) stdoutBuf.AppendLine(e.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
if (e.Data != null) stderrBuf.AppendLine(e.Data);
|
||||
};
|
||||
|
||||
var tcs = new TaskCompletionSource<int>();
|
||||
process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
int exitCode = await tcs.Task;
|
||||
process.Dispose();
|
||||
|
||||
string stdout = stdoutBuf.ToString();
|
||||
string stderr = stderrBuf.ToString();
|
||||
|
||||
foreach (string line in stdout.Split('\n'))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
LogToPanel(line.Trim(), LogType.Log);
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stderr))
|
||||
foreach (string line in stderr.Split('\n'))
|
||||
{
|
||||
foreach (string line in stderr.Split('\n'))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
LogToPanel(line.Trim(), LogType.Error);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
LogToPanel(line.Trim(), LogType.Error);
|
||||
}
|
||||
Log($"Make exited with code {process.ExitCode}", LogType.Error);
|
||||
Log($"Make exited with code {exitCode}", LogType.Error);
|
||||
|
||||
// Write build log file
|
||||
File.WriteAllText(SplashBuildPaths.BuildLogPath,
|
||||
$"=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy the compiled exe to PSXBuild/
|
||||
string exeSource = FindCompiledExe(nativeDir);
|
||||
if (!string.IsNullOrEmpty(exeSource))
|
||||
{
|
||||
File.Copy(exeSource, SplashBuildPaths.CompiledExePath, true);
|
||||
Log($"Copied .ps-exe to PSXBuild/", LogType.Log);
|
||||
Log("Copied .ps-exe to PSXBuild/", LogType.Log);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1172,7 +1244,7 @@ namespace SplashEdit.EditorCode
|
||||
LaunchToHardware();
|
||||
break;
|
||||
case BuildTarget.ISO:
|
||||
Log("ISO build not yet implemented.", LogType.Warning);
|
||||
BuildAndLaunchISO();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1274,6 +1346,235 @@ namespace SplashEdit.EditorCode
|
||||
}
|
||||
}
|
||||
|
||||
// ───── ISO Build ─────
|
||||
|
||||
private void BuildAndLaunchISO()
|
||||
{
|
||||
if (!_hasMkpsxiso)
|
||||
{
|
||||
Log("mkpsxiso not installed. Click Download in the Toolchain section.", LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
string exePath = SplashBuildPaths.CompiledExePath;
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
Log("Compiled .ps-exe not found in PSXBuild/.", LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask user for output location
|
||||
string defaultDir = SplashBuildPaths.BuildOutputDir;
|
||||
string savePath = EditorUtility.SaveFilePanel(
|
||||
"Save ISO Image", defaultDir, "psxsplash", "bin");
|
||||
if (string.IsNullOrEmpty(savePath))
|
||||
{
|
||||
Log("ISO build cancelled.", LogType.Log);
|
||||
return;
|
||||
}
|
||||
|
||||
string outputBin = savePath;
|
||||
string outputCue = Path.ChangeExtension(savePath, ".cue");
|
||||
|
||||
// Step 1: Generate SYSTEM.CNF
|
||||
Log("Generating SYSTEM.CNF...", LogType.Log);
|
||||
if (!GenerateSystemCnf())
|
||||
{
|
||||
Log("Failed to generate SYSTEM.CNF.", LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Generate XML catalog for mkpsxiso
|
||||
Log("Generating ISO catalog...", LogType.Log);
|
||||
string xmlPath = GenerateISOCatalog(outputBin, outputCue);
|
||||
if (string.IsNullOrEmpty(xmlPath))
|
||||
{
|
||||
Log("Failed to generate ISO catalog.", LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Delete existing .bin/.cue — mkpsxiso won't overwrite them
|
||||
try
|
||||
{
|
||||
if (File.Exists(outputBin)) File.Delete(outputBin);
|
||||
if (File.Exists(outputCue)) File.Delete(outputCue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Could not remove old ISO files: {ex.Message}", LogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: Run mkpsxiso
|
||||
Log("Building ISO image...", LogType.Log);
|
||||
bool success = MkpsxisoDownloader.BuildISO(xmlPath, outputBin, outputCue,
|
||||
msg => Log(msg, LogType.Log));
|
||||
|
||||
if (success)
|
||||
{
|
||||
long fileSize = new FileInfo(outputBin).Length;
|
||||
Log($"ISO image written: {outputBin} ({fileSize:N0} bytes)", LogType.Log);
|
||||
Log($"CUE sheet written: {outputCue}", LogType.Log);
|
||||
|
||||
// Offer to reveal in explorer
|
||||
EditorUtility.RevealInFinder(outputBin);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("ISO build failed.", LogType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derive the executable name on disc from the volume label.
|
||||
/// Uppercase, no extension, trimmed to 12 characters (ISO9660 limit).
|
||||
/// </summary>
|
||||
private static string GetISOExeName()
|
||||
{
|
||||
string label = SplashSettings.ISOVolumeLabel;
|
||||
if (string.IsNullOrEmpty(label)) label = "PSXSPLASH";
|
||||
|
||||
// Uppercase, strip anything not A-Z / 0-9 / underscore
|
||||
label = label.ToUpperInvariant();
|
||||
var sb = new System.Text.StringBuilder(label.Length);
|
||||
foreach (char c in label)
|
||||
{
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')
|
||||
sb.Append(c);
|
||||
}
|
||||
string name = sb.ToString();
|
||||
if (name.Length == 0) name = "PSXSPLASH";
|
||||
if (name.Length > 12) name = name.Substring(0, 12);
|
||||
return name;
|
||||
}
|
||||
|
||||
private bool GenerateSystemCnf()
|
||||
{
|
||||
try
|
||||
{
|
||||
string cnfPath = SplashBuildPaths.SystemCnfPath;
|
||||
|
||||
// The executable name on disc — no extension, max 12 chars
|
||||
string exeName = GetISOExeName();
|
||||
|
||||
// SYSTEM.CNF content — the BIOS reads this to launch the executable.
|
||||
// BOOT: path to the executable on disc (cdrom:\path;1)
|
||||
// TCB: number of thread control blocks (4 is standard)
|
||||
// EVENT: number of event control blocks (10 is standard)
|
||||
// STACK: initial stack pointer address (top of RAM minus a small margin)
|
||||
string content =
|
||||
$"BOOT = cdrom:\\{exeName};1\r\n" +
|
||||
"TCB = 4\r\n" +
|
||||
"EVENT = 10\r\n" +
|
||||
"STACK = 801FFF00\r\n";
|
||||
|
||||
File.WriteAllText(cnfPath, content, new System.Text.UTF8Encoding(false));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"SYSTEM.CNF generation error: {ex.Message}", LogType.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the mkpsxiso XML catalog describing the ISO filesystem layout.
|
||||
/// Includes SYSTEM.CNF, the executable, all splashpacks, loading packs, and manifest.
|
||||
/// </summary>
|
||||
private string GenerateISOCatalog(string outputBin, string outputCue)
|
||||
{
|
||||
try
|
||||
{
|
||||
string xmlPath = SplashBuildPaths.ISOCatalogPath;
|
||||
string buildDir = SplashBuildPaths.BuildOutputDir;
|
||||
string volumeLabel = SplashSettings.ISOVolumeLabel;
|
||||
if (string.IsNullOrEmpty(volumeLabel)) volumeLabel = "PSXSPLASH";
|
||||
|
||||
// Sanitize volume label (ISO9660: uppercase, max 31 chars)
|
||||
volumeLabel = volumeLabel.ToUpperInvariant();
|
||||
if (volumeLabel.Length > 31) volumeLabel = volumeLabel.Substring(0, 31);
|
||||
|
||||
var xml = new System.Text.StringBuilder();
|
||||
xml.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
xml.AppendLine("<iso_project image_name=\"psxsplash.bin\" cue_sheet=\"psxsplash.cue\">");
|
||||
xml.AppendLine(" <track type=\"data\">");
|
||||
xml.AppendLine(" <identifiers");
|
||||
xml.AppendLine(" system=\"PLAYSTATION\"");
|
||||
xml.AppendLine(" application=\"PLAYSTATION\"");
|
||||
xml.AppendLine($" volume=\"{EscapeXml(volumeLabel)}\"");
|
||||
xml.AppendLine($" volume_set=\"{EscapeXml(volumeLabel)}\"");
|
||||
xml.AppendLine(" publisher=\"SPLASHEDIT\"");
|
||||
xml.AppendLine(" data_preparer=\"MKPSXISO\"");
|
||||
xml.AppendLine(" />");
|
||||
|
||||
// License file (optional)
|
||||
string licensePath = SplashSettings.LicenseFilePath;
|
||||
if (!string.IsNullOrEmpty(licensePath) && File.Exists(licensePath))
|
||||
{
|
||||
xml.AppendLine($" <license file=\"{EscapeXml(licensePath)}\"/>");
|
||||
}
|
||||
|
||||
xml.AppendLine(" <directory_tree>");
|
||||
|
||||
// SYSTEM.CNF — must be first for BIOS to find it
|
||||
string cnfPath = SplashBuildPaths.SystemCnfPath;
|
||||
xml.AppendLine($" <file name=\"SYSTEM.CNF\" source=\"{EscapeXml(cnfPath)}\"/>");
|
||||
|
||||
// The executable — renamed to match what SYSTEM.CNF points to
|
||||
string exePath = SplashBuildPaths.CompiledExePath;
|
||||
string isoExeName = GetISOExeName();
|
||||
xml.AppendLine($" <file name=\"{isoExeName}\" source=\"{EscapeXml(exePath)}\"/>");
|
||||
|
||||
// Manifest
|
||||
string manifestPath = SplashBuildPaths.ManifestPath;
|
||||
if (File.Exists(manifestPath))
|
||||
{
|
||||
xml.AppendLine($" <file name=\"MANIFEST.BIN\" source=\"{EscapeXml(manifestPath)}\"/>");
|
||||
}
|
||||
|
||||
// Scene splashpacks and loading packs
|
||||
for (int i = 0; i < _sceneList.Count; i++)
|
||||
{
|
||||
string splashpack = SplashBuildPaths.GetSceneSplashpackPath(i, _sceneList[i].name);
|
||||
if (File.Exists(splashpack))
|
||||
{
|
||||
string isoName = $"SCENE_{i}.SPK";
|
||||
xml.AppendLine($" <file name=\"{isoName}\" source=\"{EscapeXml(splashpack)}\"/>");
|
||||
}
|
||||
|
||||
string loadingPack = SplashBuildPaths.GetSceneLoaderPackPath(i, _sceneList[i].name);
|
||||
if (File.Exists(loadingPack))
|
||||
{
|
||||
string isoName = $"SCENE_{i}.LDG";
|
||||
xml.AppendLine($" <file name=\"{isoName}\" source=\"{EscapeXml(loadingPack)}\"/>");
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing dummy sectors to prevent drive runaway
|
||||
xml.AppendLine(" <dummy sectors=\"128\"/>");
|
||||
xml.AppendLine(" </directory_tree>");
|
||||
xml.AppendLine(" </track>");
|
||||
xml.AppendLine("</iso_project>");
|
||||
|
||||
File.WriteAllText(xmlPath, xml.ToString(), new System.Text.UTF8Encoding(false));
|
||||
Log($"ISO catalog written: {xmlPath}", LogType.Log);
|
||||
return xmlPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"ISO catalog generation error: {ex.Message}", LogType.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeXml(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return "";
|
||||
return s.Replace("&", "&").Replace("<", "<")
|
||||
.Replace(">", ">").Replace("\"", """);
|
||||
}
|
||||
|
||||
private void StopPCdrvHost()
|
||||
{
|
||||
if (_pcdrvHost != null)
|
||||
@@ -1339,6 +1640,8 @@ namespace SplashEdit.EditorCode
|
||||
|
||||
_hasPsxavenc = PSXAudioConverter.IsInstalled();
|
||||
|
||||
_hasMkpsxiso = MkpsxisoDownloader.IsInstalled();
|
||||
|
||||
string nativeDir = SplashBuildPaths.NativeSourceDir;
|
||||
_hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir);
|
||||
}
|
||||
@@ -1418,6 +1721,22 @@ namespace SplashEdit.EditorCode
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private async void DownloadMkpsxiso()
|
||||
{
|
||||
Log("Downloading mkpsxiso ISO builder...", LogType.Log);
|
||||
bool success = await MkpsxisoDownloader.DownloadAndInstall(msg => Log(msg, LogType.Log));
|
||||
if (success)
|
||||
{
|
||||
RefreshToolchainStatus();
|
||||
Log("mkpsxiso ready!", LogType.Log);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("mkpsxiso download failed. ISO builds will not work.", LogType.Error);
|
||||
}
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void ScanSerialPorts()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -124,12 +124,6 @@ namespace SplashEdit.EditorCode
|
||||
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", value);
|
||||
}
|
||||
|
||||
public static bool AutoValidateOnExport
|
||||
{
|
||||
get => EditorPrefs.GetBool(Prefix + "AutoValidate", true);
|
||||
set => EditorPrefs.SetBool(Prefix + "AutoValidate", value);
|
||||
}
|
||||
|
||||
// --- Play Mode Intercept ---
|
||||
public static bool InterceptPlayMode
|
||||
{
|
||||
@@ -137,6 +131,27 @@ namespace SplashEdit.EditorCode
|
||||
set => EditorPrefs.SetBool(Prefix + "InterceptPlayMode", value);
|
||||
}
|
||||
|
||||
// --- ISO Build ---
|
||||
/// <summary>
|
||||
/// Optional path to a Sony license file (.dat) for the ISO image.
|
||||
/// If empty, the ISO will be built without license data (homebrew-only).
|
||||
/// The file must be in raw 2336-byte sector format (from PsyQ SDK LCNSFILE).
|
||||
/// </summary>
|
||||
public static string LicenseFilePath
|
||||
{
|
||||
get => EditorPrefs.GetString(Prefix + "LicenseFilePath", "");
|
||||
set => EditorPrefs.SetString(Prefix + "LicenseFilePath", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Volume label for the ISO image (up to 31 characters, uppercase).
|
||||
/// </summary>
|
||||
public static string ISOVolumeLabel
|
||||
{
|
||||
get => EditorPrefs.GetString(Prefix + "ISOVolumeLabel", "PSXSPLASH");
|
||||
set => EditorPrefs.SetString(Prefix + "ISOVolumeLabel", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all settings to defaults by deleting all prefixed keys.
|
||||
/// </summary>
|
||||
@@ -147,7 +162,8 @@ namespace SplashEdit.EditorCode
|
||||
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
|
||||
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
|
||||
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
|
||||
"GTEScaling", "AutoValidate", "InterceptPlayMode"
|
||||
"GTEScaling", "AutoValidate", "InterceptPlayMode",
|
||||
"LicenseFilePath", "ISOVolumeLabel"
|
||||
};
|
||||
|
||||
foreach (string key in keys)
|
||||
|
||||
Reference in New Issue
Block a user