using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using SplashEdit.RuntimeCode;
using Debug = UnityEngine.Debug;
namespace SplashEdit.EditorCode
{
///
/// SplashEdit Control Panel — the single unified window for the entire pipeline.
/// One window. One button. Everything works.
///
public class SplashControlPanel : EditorWindow
{
// ───── Constants ─────
private const string WINDOW_TITLE = "SplashEdit Control Panel";
private const string MENU_PATH = "PlayStation 1/SplashEdit Control Panel %#p";
// ───── UI State ─────
private Vector2 _scrollPos;
private bool _showQuickStart = true;
private bool _showNativeProject = true;
private bool _showToolchainSection = true;
private bool _showScenesSection = true;
private bool _showVRAMSection = true;
private bool _showBuildSection = true;
// ───── Build State ─────
private static bool _isBuilding;
private static bool _isRunning;
private static Process _emulatorProcess;
// ───── Scene List ─────
private List _sceneList = new List();
// ───── Toolchain Cache ─────
private bool _hasMIPS;
private bool _hasMake;
private bool _hasRedux;
private bool _hasNativeProject;
private bool _hasPsxavenc;
private bool _hasMkpsxiso;
private string _reduxVersion = "";
// ───── Native project installer ─────
private bool _isInstallingNative;
private string _nativeInstallStatus = "";
private string _manualNativePath = "";
// PCdrv serial host instance (for real hardware file serving)
private static PCdrvSerialHost _pcdrvHost;
private struct SceneEntry
{
public SceneAsset asset;
public string path;
public string name;
}
// ═══════════════════════════════════════════════════════════════
// Menu & Window Lifecycle
// ═══════════════════════════════════════════════════════════════
[MenuItem(MENU_PATH, false, 0)]
public static void ShowWindow()
{
var window = GetWindow();
window.titleContent = new GUIContent(WINDOW_TITLE, EditorGUIUtility.IconContent("d_BuildSettings.PSP2.Small").image);
window.minSize = new Vector2(420, 600);
window.Show();
}
private void OnEnable()
{
SplashBuildPaths.EnsureDirectories();
RefreshToolchainStatus();
LoadSceneList();
_manualNativePath = SplashSettings.NativeProjectPath;
EditorApplication.playModeStateChanged += OnPlayModeChanged;
}
private void OnDisable()
{
EditorApplication.playModeStateChanged -= OnPlayModeChanged;
}
private void OnFocus()
{
RefreshToolchainStatus();
}
// ═══════════════════════════════════════════════════════════════
// Play Mode Intercept
// ═══════════════════════════════════════════════════════════════
private void OnPlayModeChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingEditMode && SplashSettings.InterceptPlayMode)
{
EditorApplication.isPlaying = false;
Log("Play Mode intercepted — starting Build & Run instead.", LogType.Log);
BuildAndRun();
}
}
// ═══════════════════════════════════════════════════════════════
// Main GUI
// ═══════════════════════════════════════════════════════════════
private void OnGUI()
{
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
DrawHeader();
EditorGUILayout.Space(4);
// Show Quick Start prominently if toolchain is not ready
if (!_hasMIPS || !_hasNativeProject)
{
DrawQuickStartSection();
EditorGUILayout.Space(2);
}
else
{
// Collapsed quick start for experienced users
_showQuickStart = DrawSectionFoldout("Quick Start Guide", _showQuickStart);
if (_showQuickStart)
{
DrawQuickStartContent();
}
EditorGUILayout.Space(2);
}
DrawNativeProjectSection();
EditorGUILayout.Space(2);
DrawToolchainSection();
EditorGUILayout.Space(2);
DrawScenesSection();
EditorGUILayout.Space(2);
DrawVRAMSection();
EditorGUILayout.Space(2);
DrawBuildSection();
EditorGUILayout.EndScrollView();
}
// ═══════════════════════════════════════════════════════════════
// Header
// ═══════════════════════════════════════════════════════════════
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(PSXEditorStyles.ToolbarStyle);
GUILayout.Label("SplashEdit", PSXEditorStyles.WindowHeader);
GUILayout.FlexibleSpace();
// Play mode intercept toggle
bool intercept = SplashSettings.InterceptPlayMode;
var toggleContent = new GUIContent(
intercept ? "▶ Intercept ON" : "▶ Intercept OFF",
"When enabled, pressing Play in Unity triggers Build & Run instead.");
bool newIntercept = GUILayout.Toggle(intercept, toggleContent, EditorStyles.toolbarButton, GUILayout.Width(120));
if (newIntercept != intercept)
SplashSettings.InterceptPlayMode = newIntercept;
EditorGUILayout.EndHorizontal();
// Status bar
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
{
string statusText;
Color statusColor;
if (!_hasMIPS)
{
statusText = "Setup required — install the MIPS toolchain to get started";
statusColor = PSXEditorStyles.Warning;
}
else if (!_hasNativeProject)
{
statusText = "Native project not found — clone or set path below";
statusColor = PSXEditorStyles.Warning;
}
else if (_isBuilding)
{
statusText = "Building...";
statusColor = PSXEditorStyles.Info;
}
else if (_isRunning)
{
statusText = "Running on " + (SplashSettings.Target == BuildTarget.Emulator ? "emulator" : "hardware");
statusColor = PSXEditorStyles.AccentGreen;
}
else
{
statusText = "Ready";
statusColor = PSXEditorStyles.Success;
}
var prevColor = GUI.contentColor;
GUI.contentColor = statusColor;
GUILayout.Label(statusText, EditorStyles.miniLabel);
GUI.contentColor = prevColor;
}
EditorGUILayout.EndHorizontal();
}
// ═══════════════════════════════════════════════════════════════
// Quick Start Guide
// ═══════════════════════════════════════════════════════════════
private void DrawQuickStartSection()
{
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
var prevColor = GUI.contentColor;
GUI.contentColor = PSXEditorStyles.AccentGold;
GUILayout.Label("Getting Started with SplashEdit", PSXEditorStyles.CardHeaderStyle);
GUI.contentColor = prevColor;
EditorGUILayout.Space(4);
DrawQuickStartContent();
EditorGUILayout.EndVertical();
}
private void DrawQuickStartContent()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
DrawQuickStartStep(1, "Install Toolchain",
"Install the MIPS cross-compiler and GNU Make below.",
_hasMIPS && _hasMake);
DrawQuickStartStep(2, "Get Native Project",
"Clone the psxsplash runtime or set a path to your local copy.",
_hasNativeProject);
DrawQuickStartStep(3, "Add Scenes",
"Add Unity scenes containing a PSXSceneExporter to the scene list.",
_sceneList.Count > 0);
DrawQuickStartStep(4, "Configure VRAM",
"Set the framebuffer resolution and texture packing settings.",
true); // Always "done" since defaults are fine
DrawQuickStartStep(5, "Build & Run",
"Click BUILD & RUN to export, compile, and launch on the emulator or real hardware.",
false);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Open Documentation", EditorStyles.miniButton, GUILayout.Width(140)))
{
Application.OpenURL("https://github.com/psxsplash/splashedit");
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawQuickStartStep(int step, string title, string description, bool done)
{
EditorGUILayout.BeginHorizontal();
// Checkbox/step indicator
string prefix = done ? "✓" : $"{step}.";
var style = done ? EditorStyles.miniLabel : EditorStyles.boldLabel;
var prevColor = GUI.contentColor;
GUI.contentColor = done ? PSXEditorStyles.Success : PSXEditorStyles.TextPrimary;
GUILayout.Label(prefix, style, GUILayout.Width(20));
GUI.contentColor = prevColor;
EditorGUILayout.BeginVertical();
GUILayout.Label(title, EditorStyles.boldLabel);
GUILayout.Label(description, EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(2);
}
// ═══════════════════════════════════════════════════════════════
// Native Project Section
// ═══════════════════════════════════════════════════════════════
private void DrawNativeProjectSection()
{
_showNativeProject = DrawSectionFoldout("Native Project (psxsplash)", _showNativeProject);
if (!_showNativeProject) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
string currentPath = SplashBuildPaths.NativeSourceDir;
bool hasProject = !string.IsNullOrEmpty(currentPath) && Directory.Exists(currentPath);
// Status
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(hasProject);
if (hasProject)
{
GUILayout.Label("Found at:", GUILayout.Width(60));
GUILayout.Label(TruncatePath(currentPath, 50), EditorStyles.miniLabel);
}
else
{
GUILayout.Label("Not found — clone from GitHub or set path manually", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(6);
// ── Option 1: Auto-clone from GitHub ──
EditorGUILayout.LabelField("Clone from GitHub", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Label(PSXSplashInstaller.RepoUrl, EditorStyles.miniLabel);
GUILayout.FlexibleSpace();
EditorGUI.BeginDisabledGroup(_isInstallingNative || PSXSplashInstaller.IsInstalled());
if (GUILayout.Button(PSXSplashInstaller.IsInstalled() ? "Installed" : "Clone", GUILayout.Width(80)))
{
CloneNativeProject();
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();
if (_isInstallingNative)
{
EditorGUILayout.HelpBox(_nativeInstallStatus, MessageType.Info);
}
// If already cloned, show version management
if (PSXSplashInstaller.IsInstalled())
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Fetch Latest", EditorStyles.miniButton, GUILayout.Width(90)))
{
FetchNativeLatest();
}
if (GUILayout.Button("Open Folder", EditorStyles.miniButton, GUILayout.Width(90)))
{
EditorUtility.RevealInFinder(PSXSplashInstaller.FullInstallPath);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(6);
// ── Option 2: Manual path ──
EditorGUILayout.LabelField("Or set path manually", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
string newPath = EditorGUILayout.TextField(_manualNativePath);
if (newPath != _manualNativePath)
{
_manualNativePath = newPath;
}
if (GUILayout.Button("...", GUILayout.Width(30)))
{
string selected = EditorUtility.OpenFolderPanel("Select psxsplash Source Directory", _manualNativePath, "");
if (!string.IsNullOrEmpty(selected))
{
_manualNativePath = selected;
}
}
EditorGUILayout.EndHorizontal();
// Validate & apply the path
bool manualPathValid = !string.IsNullOrEmpty(_manualNativePath) &&
Directory.Exists(_manualNativePath) &&
File.Exists(Path.Combine(_manualNativePath, "Makefile"));
EditorGUILayout.BeginHorizontal();
if (!string.IsNullOrEmpty(_manualNativePath) && !manualPathValid)
{
EditorGUILayout.HelpBox("Invalid path. The directory must contain a Makefile.", MessageType.Warning);
}
else if (manualPathValid && _manualNativePath != SplashSettings.NativeProjectPath)
{
if (GUILayout.Button("Apply", GUILayout.Width(60)))
{
SplashSettings.NativeProjectPath = _manualNativePath;
RefreshToolchainStatus();
Log($"Native project path set: {_manualNativePath}", LogType.Log);
}
}
EditorGUILayout.EndHorizontal();
if (manualPathValid && _manualNativePath == SplashSettings.NativeProjectPath)
{
var prevColor = GUI.contentColor;
GUI.contentColor = PSXEditorStyles.Success;
GUILayout.Label("✓ Path is set and valid", EditorStyles.miniLabel);
GUI.contentColor = prevColor;
}
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Toolchain Section
// ═══════════════════════════════════════════════════════════════
private void DrawToolchainSection()
{
_showToolchainSection = DrawSectionFoldout("Toolchain", _showToolchainSection);
if (!_showToolchainSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
// MIPS Compiler
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMIPS);
GUILayout.Label("MIPS Cross-Compiler", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMIPS)
{
if (GUILayout.Button("Install", GUILayout.Width(60)))
InstallMIPS();
}
else
{
GUILayout.Label("Ready", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
// GNU Make
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMake);
GUILayout.Label("GNU Make", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMake)
{
if (GUILayout.Button("Install", GUILayout.Width(60)))
InstallMake();
}
else
{
GUILayout.Label("Ready", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
// PCSX-Redux
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasRedux);
GUILayout.Label("PCSX-Redux", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasRedux)
{
if (GUILayout.Button("Download", GUILayout.Width(80)))
DownloadRedux();
}
else
{
GUILayout.Label(_reduxVersion, EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
// psxavenc (audio encoder)
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasPsxavenc);
GUILayout.Label("psxavenc (Audio)", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasPsxavenc)
{
if (GUILayout.Button("Download", GUILayout.Width(80)))
DownloadPsxavenc();
}
else
{
GUILayout.Label("Installed", EditorStyles.miniLabel);
}
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();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Refresh", EditorStyles.miniButton, GUILayout.Width(60)))
RefreshToolchainStatus();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Scenes Section
// ═══════════════════════════════════════════════════════════════
private void DrawScenesSection()
{
_showScenesSection = DrawSectionFoldout("Scenes", _showScenesSection);
if (!_showScenesSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
if (_sceneList.Count == 0)
{
EditorGUILayout.HelpBox(
"No scenes added yet.\n" +
"Each scene needs a GameObject with a PSXSceneExporter component.\n" +
"Drag scene assets here, or use the buttons below to add them.",
MessageType.Info);
}
// Draw scene list
int removeIndex = -1;
int moveUp = -1;
int moveDown = -1;
for (int i = 0; i < _sceneList.Count; i++)
{
EditorGUILayout.BeginHorizontal();
// Index badge
GUILayout.Label($"[{i}]", EditorStyles.miniLabel, GUILayout.Width(24));
// Scene asset field
var newAsset = (SceneAsset)EditorGUILayout.ObjectField(
_sceneList[i].asset, typeof(SceneAsset), false);
if (newAsset != _sceneList[i].asset)
{
var entry = _sceneList[i];
entry.asset = newAsset;
if (newAsset != null)
{
entry.path = AssetDatabase.GetAssetPath(newAsset);
entry.name = newAsset.name;
}
_sceneList[i] = entry;
SaveSceneList();
}
// Move buttons
EditorGUI.BeginDisabledGroup(i == 0);
if (GUILayout.Button("▲", EditorStyles.miniButtonLeft, GUILayout.Width(22)))
moveUp = i;
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(i == _sceneList.Count - 1);
if (GUILayout.Button("▼", EditorStyles.miniButtonRight, GUILayout.Width(22)))
moveDown = i;
EditorGUI.EndDisabledGroup();
// Remove
if (GUILayout.Button("×", EditorStyles.miniButton, GUILayout.Width(20)))
removeIndex = i;
EditorGUILayout.EndHorizontal();
}
// Apply deferred operations
if (removeIndex >= 0)
{
_sceneList.RemoveAt(removeIndex);
SaveSceneList();
}
if (moveUp >= 1)
{
var temp = _sceneList[moveUp];
_sceneList[moveUp] = _sceneList[moveUp - 1];
_sceneList[moveUp - 1] = temp;
SaveSceneList();
}
if (moveDown >= 0 && moveDown < _sceneList.Count - 1)
{
var temp = _sceneList[moveDown];
_sceneList[moveDown] = _sceneList[moveDown + 1];
_sceneList[moveDown + 1] = temp;
SaveSceneList();
}
// Add scene buttons
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Add Current Scene", EditorStyles.miniButton))
{
AddCurrentScene();
}
if (GUILayout.Button("+ Add Scene...", EditorStyles.miniButton))
{
string path = EditorUtility.OpenFilePanel("Select Scene", "Assets", "unity");
if (!string.IsNullOrEmpty(path))
{
// Convert to project-relative path
string projectPath = Application.dataPath;
if (path.StartsWith(projectPath))
path = "Assets" + path.Substring(projectPath.Length);
AddSceneByPath(path);
}
}
EditorGUILayout.EndHorizontal();
// Handle drag & drop
HandleSceneDragDrop();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// VRAM & Textures Section
// ═══════════════════════════════════════════════════════════════
private void DrawVRAMSection()
{
_showVRAMSection = DrawSectionFoldout("VRAM & Textures", _showVRAMSection);
if (!_showVRAMSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
// Resolution settings
EditorGUILayout.LabelField("Framebuffer Settings", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Resolution:", GUILayout.Width(75));
SplashSettings.ResolutionWidth = EditorGUILayout.IntField(SplashSettings.ResolutionWidth, GUILayout.Width(50));
GUILayout.Label("×", GUILayout.Width(14));
SplashSettings.ResolutionHeight = EditorGUILayout.IntField(SplashSettings.ResolutionHeight, GUILayout.Width(50));
// Common resolutions dropdown
if (GUILayout.Button("Presets", EditorStyles.miniButton, GUILayout.Width(55)))
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("256×240"), false, () => { SplashSettings.ResolutionWidth = 256; SplashSettings.ResolutionHeight = 240; Repaint(); });
menu.AddItem(new GUIContent("320×240"), false, () => { SplashSettings.ResolutionWidth = 320; SplashSettings.ResolutionHeight = 240; Repaint(); });
menu.AddItem(new GUIContent("368×240"), false, () => { SplashSettings.ResolutionWidth = 368; SplashSettings.ResolutionHeight = 240; Repaint(); });
menu.AddItem(new GUIContent("512×240"), false, () => { SplashSettings.ResolutionWidth = 512; SplashSettings.ResolutionHeight = 240; Repaint(); });
menu.AddItem(new GUIContent("640×240"), false, () => { SplashSettings.ResolutionWidth = 640; SplashSettings.ResolutionHeight = 240; Repaint(); });
menu.AddItem(new GUIContent("256×480"), false, () => { SplashSettings.ResolutionWidth = 256; SplashSettings.ResolutionHeight = 480; Repaint(); });
menu.AddItem(new GUIContent("320×480"), false, () => { SplashSettings.ResolutionWidth = 320; SplashSettings.ResolutionHeight = 480; Repaint(); });
menu.AddItem(new GUIContent("512×480"), false, () => { SplashSettings.ResolutionWidth = 512; SplashSettings.ResolutionHeight = 480; Repaint(); });
menu.ShowAsContext();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
SplashSettings.DualBuffering = EditorGUILayout.Toggle("Dual Buffering", SplashSettings.DualBuffering);
SplashSettings.VerticalLayout = EditorGUILayout.Toggle("Vertical Layout", SplashSettings.VerticalLayout);
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Export Settings", EditorStyles.boldLabel);
SplashSettings.DefaultGTEScaling = EditorGUILayout.FloatField("Default GTE Scaling", SplashSettings.DefaultGTEScaling);
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("Advanced Tools", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Open VRAM Editor", GUILayout.Height(24)))
{
VRAMEditorWindow.ShowWindow();
}
if (GUILayout.Button("Quantized Preview", GUILayout.Height(24)))
{
QuantizedPreviewWindow.ShowWindow();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Build & Run Section
// ═══════════════════════════════════════════════════════════════
private void DrawBuildSection()
{
_showBuildSection = DrawSectionFoldout("Build && Run", _showBuildSection);
if (!_showBuildSection) return;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
// Target & Mode
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Target:", GUILayout.Width(50));
SplashSettings.Target = (BuildTarget)EditorGUILayout.EnumPopup(SplashSettings.Target);
GUILayout.Label("Mode:", GUILayout.Width(40));
SplashSettings.Mode = (BuildMode)EditorGUILayout.EnumPopup(SplashSettings.Mode);
EditorGUILayout.EndHorizontal();
// Serial port (only for Real Hardware)
if (SplashSettings.Target == BuildTarget.RealHardware)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Serial Port:", GUILayout.Width(80));
SplashSettings.SerialPort = EditorGUILayout.TextField(SplashSettings.SerialPort);
if (GUILayout.Button("Scan", EditorStyles.miniButton, GUILayout.Width(40)))
ScanSerialPorts();
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
EditorGUI.BeginDisabledGroup(_isBuilding);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
var buildColor = GUI.backgroundColor;
GUI.backgroundColor = _isBuilding ? Color.gray : new Color(0.3f, 0.8f, 0.4f);
string buttonLabel = _isBuilding ? "Building..." : "BUILD & RUN";
if (GUILayout.Button(buttonLabel, GUILayout.Width(200), GUILayout.Height(36)))
{
BuildAndRun();
}
GUI.backgroundColor = buildColor;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUI.EndDisabledGroup();
// Stop button (if running — emulator or hardware PCdrv host)
if (_isRunning)
{
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = new Color(0.9f, 0.3f, 0.3f);
string stopLabel = _emulatorProcess != null ? "■ STOP EMULATOR" : "■ STOP PCdrv HOST";
if (GUILayout.Button(stopLabel, GUILayout.Width(200), GUILayout.Height(24)))
{
StopAll();
}
GUI.backgroundColor = buildColor;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
// Export-only / Compile-only
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Export Only", EditorStyles.miniButton, GUILayout.Width(100)))
{
ExportAllScenes();
}
if (GUILayout.Button("Compile Only", EditorStyles.miniButton, GUILayout.Width(100)))
{
CompileOnly();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Pipeline Actions
// ═══════════════════════════════════════════════════════════════
///
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
///
public async void BuildAndRun()
{
if (_isBuilding) return;
_isBuilding = true;
var console = EditorWindow.GetWindow();
console.titleContent = new GUIContent("PSX Console", EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
console.minSize = new Vector2(400, 200);
console.Show();
try
{
Log("Validating toolchain...", LogType.Log);
if (!ValidateToolchain())
{
Log("Toolchain validation failed. Fix issues above.", LogType.Error);
return;
}
Log("Toolchain OK.", LogType.Log);
Log("Exporting scenes...", LogType.Log);
if (!ExportAllScenes())
{
Log("Export failed.", LogType.Error);
return;
}
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
Log("Compiling native code...", LogType.Log);
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);
Log("Launching...", LogType.Log);
Launch();
}
catch (Exception ex)
{
Log($"Pipeline error: {ex.Message}", LogType.Error);
Debug.LogException(ex);
}
finally
{
_isBuilding = false;
EditorUtility.ClearProgressBar();
Repaint();
}
}
// ───── Step 1: Validate ─────
private bool ValidateToolchain()
{
RefreshToolchainStatus();
if (!_hasMIPS)
{
Log("MIPS cross-compiler not found. Click Install in the Toolchain section.", LogType.Error);
return false;
}
if (!_hasMake)
{
Log("GNU Make not found. Click Install in the Toolchain section.", LogType.Error);
return false;
}
if (SplashSettings.Target == BuildTarget.Emulator && !_hasRedux)
{
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))
{
Log("Native project directory not found. Set it in the Toolchain section.", LogType.Error);
return false;
}
if (_sceneList.Count == 0)
{
Log("No scenes in the scene list. Add at least one scene.", LogType.Error);
return false;
}
return true;
}
// ───── Step 2: Export ─────
///
/// Exports all scenes in the scene list to splashpack files in PSXBuild/.
///
public bool ExportAllScenes()
{
SplashBuildPaths.EnsureDirectories();
_loaderPackCache = new Dictionary();
// Save current scene
string currentScenePath = SceneManager.GetActiveScene().path;
bool success = true;
for (int i = 0; i < _sceneList.Count; i++)
{
var scene = _sceneList[i];
if (scene.asset == null)
{
Log($"Scene [{i}] is null, skipping.", LogType.Warning);
continue;
}
EditorUtility.DisplayProgressBar("SplashEdit Export",
$"Exporting scene {i + 1}/{_sceneList.Count}: {scene.name}",
(float)i / _sceneList.Count);
try
{
// Open the scene
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single);
// Find the exporter
var exporter = UnityEngine.Object.FindFirstObjectByType();
if (exporter == null)
{
Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning);
continue;
}
// Export to the build directory
string outputPath = SplashBuildPaths.GetSceneSplashpackPath(i, scene.name);
exporter.ExportToPath(outputPath);
Log($"Exported '{scene.name}' → {Path.GetFileName(outputPath)}", LogType.Log);
// Export loading screen if assigned
if (exporter.LoadingScreenPrefab != null)
{
string loaderPath = SplashBuildPaths.GetSceneLoaderPackPath(i, scene.name);
ExportLoaderPack(exporter.LoadingScreenPrefab, loaderPath, i, scene.name);
}
}
catch (Exception ex)
{
Log($"Error exporting '{scene.name}': {ex.Message}", LogType.Error);
success = false;
}
}
// Write manifest (simple binary: scene count + list of filenames)
WriteManifest();
EditorUtility.ClearProgressBar();
// Reopen orignal scene
if (!string.IsNullOrEmpty(currentScenePath))
{
EditorSceneManager.OpenScene(currentScenePath, OpenSceneMode.Single);
}
return success;
}
///
/// Cache of already-exported loader packs for deduplication.
/// Key = prefab asset GUID, Value = path of the written file.
/// If two scenes reference the same loading screen prefab, we copy the file
/// instead of regenerating it.
///
private Dictionary _loaderPackCache = new Dictionary();
private void ExportLoaderPack(GameObject prefab, string outputPath, int sceneIndex, string sceneName)
{
string prefabPath = AssetDatabase.GetAssetPath(prefab);
string guid = AssetDatabase.AssetPathToGUID(prefabPath);
// Dedup: if we already exported this exact prefab, just copy the file
if (!string.IsNullOrEmpty(guid) && _loaderPackCache.TryGetValue(guid, out string cachedPath))
{
if (File.Exists(cachedPath))
{
File.Copy(cachedPath, outputPath, true);
Log($"Loading screen for '{sceneName}' → {Path.GetFileName(outputPath)} (deduped from {Path.GetFileName(cachedPath)})", LogType.Log);
return;
}
}
// Need the PSXData resolution to pass to the writer
Vector2 resolution;
bool db, vl;
List pa;
DataStorage.LoadData(out resolution, out db, out vl, out pa);
// Instantiate the prefab temporarily so the components are live
// (GetComponentsInChildren needs active hierarchy)
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
try
{
// Pack UI image textures into VRAM (same flow as PSXSceneExporter)
TextureAtlas[] atlases = null;
PSXUIImage[] uiImages = instance.GetComponentsInChildren(true);
if (uiImages != null && uiImages.Length > 0)
{
List uiTextures = new List();
foreach (PSXUIImage img in uiImages)
{
if (img.SourceTexture != null)
{
Utils.SetTextureImporterFormat(img.SourceTexture, true);
PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(img.SourceTexture, img.BitDepth);
tex.OriginalTexture = img.SourceTexture;
img.PackedTexture = tex;
uiTextures.Add(tex);
}
}
if (uiTextures.Count > 0)
{
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(resolution, vl);
List framebuffers = new List { buffer1 };
if (db) framebuffers.Add(buffer2);
VRAMPacker packer = new VRAMPacker(framebuffers, pa);
var packed = packer.PackTexturesIntoVRAM(new PSXObjectExporter[0], uiTextures);
atlases = packed.atlases;
}
}
// CollectCanvasFromPrefab reads PackedTexture VRAM coords (set by packer above)
bool ok = PSXLoaderPackWriter.Write(outputPath, instance, resolution, atlases,
(msg, type) => Log(msg, type));
if (ok)
{
Log($"Loading screen for '{sceneName}' → {Path.GetFileName(outputPath)}", LogType.Log);
if (!string.IsNullOrEmpty(guid))
_loaderPackCache[guid] = outputPath;
}
}
finally
{
UnityEngine.Object.DestroyImmediate(instance);
}
}
private void WriteManifest()
{
string manifestPath = SplashBuildPaths.ManifestPath;
using (var writer = new BinaryWriter(File.Open(manifestPath, FileMode.Create)))
{
// Magic "SM" for Scene Manifest
writer.Write((byte)'S');
writer.Write((byte)'M');
// Version
writer.Write((ushort)1);
// Scene count
writer.Write((uint)_sceneList.Count);
for (int i = 0; i < _sceneList.Count; i++)
{
string filename = Path.GetFileName(
SplashBuildPaths.GetSceneSplashpackPath(i, _sceneList[i].name));
byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(filename);
// Length-prefixed string
writer.Write((byte)nameBytes.Length);
writer.Write(nameBytes);
}
}
Log("Wrote scene manifest.", LogType.Log);
}
// ───── Step 3: Compile ─────
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 CompileNativeAsync()
{
string nativeDir = SplashBuildPaths.NativeSourceDir;
if (string.IsNullOrEmpty(nativeDir))
{
Log("Native project directory not set.", LogType.Error);
return false;
}
string buildArg = SplashSettings.Mode == BuildMode.Debug ? "BUILD=Debug" : "";
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
{
FileName = Application.platform == RuntimePlatform.WindowsEditor ? "cmd.exe" : "/bin/bash",
Arguments = Application.platform == RuntimePlatform.WindowsEditor
? $"/c {makeCmd}"
: $"-c \"{makeCmd}\"",
WorkingDirectory = nativeDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
try
{
var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var stdoutBuf = new System.Text.StringBuilder();
var stderrBuf = new System.Text.StringBuilder();
process.OutputDataReceived += (s, e) =>
{
if (e.Data != null) stdoutBuf.AppendLine(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
if (e.Data != null) stderrBuf.AppendLine(e.Data);
};
var tcs = new TaskCompletionSource();
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 (exitCode != 0)
{
foreach (string line in stderr.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
Log($"Make exited with code {exitCode}", LogType.Error);
File.WriteAllText(SplashBuildPaths.BuildLogPath,
$"=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}");
return false;
}
string exeSource = FindCompiledExe(nativeDir);
if (!string.IsNullOrEmpty(exeSource))
{
File.Copy(exeSource, SplashBuildPaths.CompiledExePath, true);
Log("Copied .ps-exe to PSXBuild/", LogType.Log);
}
else
{
Log("Warning: Could not find compiled .ps-exe", LogType.Warning);
}
return true;
}
catch (Exception ex)
{
Log($"Compile error: {ex.Message}", LogType.Error);
return false;
}
}
private string FindCompiledExe(string nativeDir)
{
// Look for .ps-exe files in the native dir
var files = Directory.GetFiles(nativeDir, "*.ps-exe", SearchOption.TopDirectoryOnly);
if (files.Length > 0)
return files[0];
// Also check common build output locations
foreach (string subdir in new[] { "build", "bin", "." })
{
string dir = Path.Combine(nativeDir, subdir);
if (Directory.Exists(dir))
{
files = Directory.GetFiles(dir, "*.ps-exe", SearchOption.TopDirectoryOnly);
if (files.Length > 0)
return files[0];
}
}
return null;
}
// ───── Step 4: Launch ─────
private void Launch()
{
switch (SplashSettings.Target)
{
case BuildTarget.Emulator:
LaunchEmulator();
break;
case BuildTarget.RealHardware:
LaunchToHardware();
break;
case BuildTarget.ISO:
BuildAndLaunchISO();
break;
}
}
private void LaunchEmulator()
{
string reduxPath = SplashSettings.PCSXReduxPath;
if (string.IsNullOrEmpty(reduxPath) || !File.Exists(reduxPath))
{
Log("PCSX-Redux binary not found.", LogType.Error);
return;
}
string exePath = SplashBuildPaths.CompiledExePath;
if (!File.Exists(exePath))
{
Log("Compiled .ps-exe not found in PSXBuild/.", LogType.Error);
return;
}
// Kill previous instance without clearing the console
StopAllQuiet();
string pcdrvBase = SplashBuildPaths.BuildOutputDir;
string args = $"-exe \"{exePath}\" -run -fastboot -pcdrv -pcdrvbase \"{pcdrvBase}\" -stdout";
Log($"Launching: {Path.GetFileName(reduxPath)} {args}", LogType.Log);
var psi = new ProcessStartInfo
{
FileName = reduxPath,
Arguments = args,
UseShellExecute = false,
// CreateNoWindow = true prevents pcsx-redux's -stdout AllocConsole() from
// stealing stdout away from our pipe. pcsx-redux is a GUI app and doesn't
// need a console window - it creates its own OpenGL/SDL window.
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
try
{
_emulatorProcess = Process.Start(psi);
_isRunning = true;
Log("PCSX-Redux launched.", LogType.Log);
// Open the PSX Console window and attach to the process output
PSXConsoleWindow.Attach(_emulatorProcess);
}
catch (Exception ex)
{
Log($"Failed to launch emulator: {ex.Message}", LogType.Error);
}
}
private void LaunchToHardware()
{
string exePath = SplashBuildPaths.CompiledExePath;
if (!File.Exists(exePath))
{
Log("Compiled .ps-exe not found in PSXBuild/.", LogType.Error);
return;
}
string port = SplashSettings.SerialPort;
int baud = SplashSettings.SerialBaudRate;
// Stop any previous run (emulator or PCdrv) without clearing the console
StopAllQuiet();
// Upload the exe with debug hooks (DEBG → SEXE on the same port).
// DEBG installs kernel-resident break handlers BEFORE the exe auto-starts.
// The returned port stays open so PCDrv monitoring can begin immediately.
Log($"Uploading to {port}...", LogType.Log);
SerialPort serialPort = UniromUploader.UploadExeForPCdrv(port, baud, exePath,
msg => Log(msg, LogType.Log));
if (serialPort == null)
{
Log("Upload failed.", LogType.Error);
return;
}
// Start PCdrv host on the same open port — no re-open, no DEBG/CONT needed
try
{
_pcdrvHost = new PCdrvSerialHost(port, baud, SplashBuildPaths.BuildOutputDir,
msg => LogToPanel(msg, LogType.Log),
msg => PSXConsoleWindow.AddLine(msg));
_pcdrvHost.Start(serialPort);
_isRunning = true;
Log("PCdrv serial host started. Serving files to PS1.", LogType.Log);
}
catch (Exception ex)
{
Log($"PCdrv host error: {ex.Message}", LogType.Error);
try { serialPort.Close(); } catch { }
serialPort.Dispose();
}
}
// ───── 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);
}
}
///
/// Derive the executable name on disc from the volume label.
/// Uppercase, no extension, trimmed to 12 characters (ISO9660 limit).
///
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;
}
}
///
/// Generates the mkpsxiso XML catalog describing the ISO filesystem layout.
/// Includes SYSTEM.CNF, the executable, all splashpacks, loading packs, and manifest.
///
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.AppendLine("");
xml.AppendLine(" ");
xml.AppendLine("");
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)
{
_pcdrvHost.Dispose();
_pcdrvHost = null;
}
}
///
/// Stops everything (emulator, PCdrv host, console reader) — used by the STOP button.
///
private void StopAll()
{
PSXConsoleWindow.Detach();
StopEmulatorProcess();
StopPCdrvHost();
_isRunning = false;
Log("Stopped.", LogType.Log);
}
///
/// Stops emulator and PCdrv host without touching the console window.
/// Used before re-launching so the console keeps its history.
///
private void StopAllQuiet()
{
StopEmulatorProcess();
StopPCdrvHost();
_isRunning = false;
}
private void StopEmulatorProcess()
{
if (_emulatorProcess != null && !_emulatorProcess.HasExited)
{
try
{
_emulatorProcess.Kill();
_emulatorProcess.Dispose();
}
catch { }
}
_emulatorProcess = null;
}
// ═══════════════════════════════════════════════════════════════
// Toolchain Detection & Install
// ═══════════════════════════════════════════════════════════════
private void RefreshToolchainStatus()
{
_hasMIPS = ToolchainChecker.IsToolAvailable(
Application.platform == RuntimePlatform.WindowsEditor
? "mipsel-none-elf-gcc"
: "mipsel-linux-gnu-gcc");
_hasMake = ToolchainChecker.IsToolAvailable("make");
string reduxBin = SplashSettings.PCSXReduxPath;
_hasRedux = !string.IsNullOrEmpty(reduxBin) && File.Exists(reduxBin);
_reduxVersion = _hasRedux ? "Installed" : "";
_hasPsxavenc = PSXAudioConverter.IsInstalled();
_hasMkpsxiso = MkpsxisoDownloader.IsInstalled();
string nativeDir = SplashBuildPaths.NativeSourceDir;
_hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir);
}
private async void InstallMIPS()
{
Log("Installing MIPS toolchain...", LogType.Log);
try
{
await ToolchainInstaller.InstallToolchain();
Log("MIPS toolchain installation started. You may need to restart.", LogType.Log);
}
catch (Exception ex)
{
Log($"MIPS install error: {ex.Message}", LogType.Error);
}
RefreshToolchainStatus();
Repaint();
}
private async void InstallMake()
{
Log("Installing GNU Make...", LogType.Log);
try
{
await ToolchainInstaller.InstallMake();
Log("GNU Make installation complete.", LogType.Log);
}
catch (Exception ex)
{
Log($"Make install error: {ex.Message}", LogType.Error);
}
RefreshToolchainStatus();
Repaint();
}
private async void DownloadRedux()
{
Log("Downloading PCSX-Redux...", LogType.Log);
bool success = await PCSXReduxDownloader.DownloadAndInstall(msg => Log(msg, LogType.Log));
if (success)
{
// Clear any custom path so it uses the auto-downloaded one
SplashSettings.PCSXReduxPath = "";
RefreshToolchainStatus();
Log("PCSX-Redux ready!", LogType.Log);
}
else
{
// Fall back to manual selection
Log("Auto-download failed. Select binary manually.", LogType.Warning);
string path = EditorUtility.OpenFilePanel("Select PCSX-Redux Binary", "",
Application.platform == RuntimePlatform.WindowsEditor ? "exe" : "");
if (!string.IsNullOrEmpty(path))
{
SplashSettings.PCSXReduxPath = path;
RefreshToolchainStatus();
Log($"PCSX-Redux set: {path}", LogType.Log);
}
}
Repaint();
}
private async void DownloadPsxavenc()
{
Log("Downloading psxavenc audio encoder...", LogType.Log);
bool success = await PSXAudioConverter.DownloadAndInstall(msg => Log(msg, LogType.Log));
if (success)
{
RefreshToolchainStatus();
Log("psxavenc ready!", LogType.Log);
}
else
{
Log("psxavenc download failed. Audio export will not work.", LogType.Error);
}
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
{
string[] ports = System.IO.Ports.SerialPort.GetPortNames();
if (ports.Length == 0)
{
Log("No serial ports found.", LogType.Warning);
}
else
{
Log($"Available ports: {string.Join(", ", ports)}", LogType.Log);
// Auto-select first port if current is empty
if (string.IsNullOrEmpty(SplashSettings.SerialPort))
SplashSettings.SerialPort = ports[0];
}
}
catch (Exception ex)
{
Log($"Error scanning ports: {ex.Message}", LogType.Error);
}
}
// ───── Native Project Clone/Fetch ─────
private async void CloneNativeProject()
{
_isInstallingNative = true;
_nativeInstallStatus = "Cloning psxsplash repository (this may take a minute)...";
Repaint();
Log("Cloning psxsplash native project from GitHub...", LogType.Log);
try
{
bool success = await PSXSplashInstaller.Install();
if (success)
{
Log("psxsplash cloned successfully!", LogType.Log);
_nativeInstallStatus = "";
RefreshToolchainStatus();
}
else
{
Log("Clone failed. Check console for errors.", LogType.Error);
_nativeInstallStatus = "Clone failed — check console for details.";
}
}
catch (Exception ex)
{
Log($"Clone error: {ex.Message}", LogType.Error);
_nativeInstallStatus = $"Error: {ex.Message}";
}
finally
{
_isInstallingNative = false;
Repaint();
}
}
private async void FetchNativeLatest()
{
Log("Fetching latest changes...", LogType.Log);
try
{
bool success = await PSXSplashInstaller.FetchLatestAsync();
if (success)
Log("Fetch complete. Use 'git pull' to apply updates.", LogType.Log);
else
Log("Fetch failed.", LogType.Warning);
}
catch (Exception ex)
{
Log($"Fetch error: {ex.Message}", LogType.Error);
}
Repaint();
}
// ═══════════════════════════════════════════════════════════════
// Scene List Persistence (EditorPrefs)
// ═══════════════════════════════════════════════════════════════
private void LoadSceneList()
{
_sceneList.Clear();
string prefix = "SplashEdit_" + Application.dataPath.GetHashCode().ToString("X8") + "_";
int count = EditorPrefs.GetInt(prefix + "SceneCount", 0);
for (int i = 0; i < count; i++)
{
string path = EditorPrefs.GetString(prefix + $"Scene_{i}", "");
if (string.IsNullOrEmpty(path)) continue;
var asset = AssetDatabase.LoadAssetAtPath(path);
_sceneList.Add(new SceneEntry
{
asset = asset,
path = path,
name = asset != null ? asset.name : Path.GetFileNameWithoutExtension(path)
});
}
}
private void SaveSceneList()
{
string prefix = "SplashEdit_" + Application.dataPath.GetHashCode().ToString("X8") + "_";
EditorPrefs.SetInt(prefix + "SceneCount", _sceneList.Count);
for (int i = 0; i < _sceneList.Count; i++)
{
EditorPrefs.SetString(prefix + $"Scene_{i}", _sceneList[i].path);
}
}
private void AddCurrentScene()
{
string scenePath = SceneManager.GetActiveScene().path;
if (string.IsNullOrEmpty(scenePath))
{
Log("Current scene is not saved. Save it first.", LogType.Warning);
return;
}
AddSceneByPath(scenePath);
}
private void AddSceneByPath(string path)
{
// Check for duplicates
if (_sceneList.Any(s => s.path == path))
{
Log($"Scene already in list: {path}", LogType.Warning);
return;
}
var asset = AssetDatabase.LoadAssetAtPath(path);
_sceneList.Add(new SceneEntry
{
asset = asset,
path = path,
name = asset != null ? asset.name : Path.GetFileNameWithoutExtension(path)
});
SaveSceneList();
Log($"Added scene: {Path.GetFileNameWithoutExtension(path)}", LogType.Log);
}
private void HandleSceneDragDrop()
{
Event evt = Event.current;
Rect dropArea = GUILayoutUtility.GetLastRect();
if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform)
{
if (!dropArea.Contains(evt.mousePosition)) return;
bool hasScenes = DragAndDrop.objectReferences.Any(o => o is SceneAsset);
if (hasScenes)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is SceneAsset)
{
string path = AssetDatabase.GetAssetPath(obj);
AddSceneByPath(path);
}
}
}
evt.Use();
}
}
}
// ═══════════════════════════════════════════════════════════════
// Utilities
// ═══════════════════════════════════════════════════════════════
private static void Log(string message, LogType type)
{
bool isError = type == LogType.Error;
PSXConsoleWindow.AddLine(message, isError);
// Always log to Unity console as a fallback.
switch (type)
{
case LogType.Error:
Debug.LogError($"[SplashEdit] {message}");
break;
case LogType.Warning:
Debug.LogWarning($"[SplashEdit] {message}");
break;
default:
Debug.Log($"[SplashEdit] {message}");
break;
}
}
///
/// Writes make stdout/stderr to PSX Console and Unity console.
///
private static void LogToPanel(string message, LogType type)
{
PSXConsoleWindow.AddLine(message, type == LogType.Error);
Debug.Log($"[SplashEdit Build] {message}");
}
private bool DrawSectionFoldout(string title, bool isOpen)
{
EditorGUILayout.BeginHorizontal();
isOpen = EditorGUILayout.Foldout(isOpen, title, true, PSXEditorStyles.SectionHeader);
EditorGUILayout.EndHorizontal();
return isOpen;
}
private void DrawStatusIcon(bool ok)
{
var content = ok
? EditorGUIUtility.IconContent("d_greenLight")
: EditorGUIUtility.IconContent("d_redLight");
GUILayout.Label(content, GUILayout.Width(20), GUILayout.Height(20));
}
private string TruncatePath(string path, int maxLen)
{
if (path.Length <= maxLen) return path;
return "..." + path.Substring(path.Length - maxLen + 3);
}
}
}