Files
secretsplash/Editor/Core/SplashControlPanel.cs
Jan Racek 4aa4e49424 psst
2026-03-24 13:00:54 +01:00

1565 lines
62 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using SplashEdit.RuntimeCode;
using Debug = UnityEngine.Debug;
namespace SplashEdit.EditorCode
{
/// <summary>
/// SplashEdit Control Panel — the single unified window for the entire pipeline.
/// One window. One button. Everything works.
/// </summary>
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<SceneEntry> _sceneList = new List<SceneEntry>();
// ───── Toolchain Cache ─────
private bool _hasMIPS;
private bool _hasMake;
private bool _hasRedux;
private bool _hasNativeProject;
private bool _hasPsxavenc;
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<SplashControlPanel>();
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();
// 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);
// 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)))
{
VRAMEditorWindow.ShowWindow();
}
if (GUILayout.Button("Quantized Preview", GUILayout.Height(24)))
{
QuantizedPreviewWindow.ShowWindow();
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Open Scene Validator", EditorStyles.miniButton))
{
PSXSceneValidatorWindow.ShowWindow();
}
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();
}
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)))
{
CompileNative();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ═══════════════════════════════════════════════════════════════
// Pipeline Actions
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
/// </summary>
public 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);
console.Show();
try
{
// Step 1: Validate
Log("Validating toolchain...", LogType.Log);
if (!ValidateToolchain())
{
Log("Toolchain validation failed. Fix issues above.", LogType.Error);
return;
}
Log("Toolchain OK.", LogType.Log);
// Step 2: Export all scenes
Log("Exporting scenes...", LogType.Log);
if (!ExportAllScenes())
{
Log("Export failed.", LogType.Error);
return;
}
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
// Step 3: Compile native
Log("Compiling native code...", LogType.Log);
if (!CompileNative())
{
Log("Compilation failed. Check build log.", LogType.Error);
return;
}
Log("Compile succeeded.", LogType.Log);
// Step 4: Launch
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;
}
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 ─────
/// <summary>
/// Exports all scenes in the scene list to splashpack files in PSXBuild/.
/// </summary>
public bool ExportAllScenes()
{
SplashBuildPaths.EnsureDirectories();
// 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.FindObjectOfType<PSXSceneExporter>();
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);
}
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;
}
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 ─────
/// <summary>
/// Runs make in the native project directory.
/// </summary>
public bool CompileNative()
{
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" : "";
// 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();
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 = Process.Start(psi);
string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
// Log output to panel only (no Unity console spam)
if (!string.IsNullOrEmpty(stdout))
{
foreach (string line in stdout.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Log);
}
}
if (process.ExitCode != 0)
{
if (!string.IsNullOrEmpty(stderr))
{
foreach (string line in stderr.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
}
Log($"Make exited with code {process.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);
}
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:
Log("ISO build not yet implemented.", LogType.Warning);
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();
}
}
private void StopPCdrvHost()
{
if (_pcdrvHost != null)
{
_pcdrvHost.Dispose();
_pcdrvHost = null;
}
}
/// <summary>
/// Stops everything (emulator, PCdrv host, console reader) — used by the STOP button.
/// </summary>
private void StopAll()
{
PSXConsoleWindow.Detach();
StopEmulatorProcess();
StopPCdrvHost();
_isRunning = false;
Log("Stopped.", LogType.Log);
}
/// <summary>
/// Stops emulator and PCdrv host without touching the console window.
/// Used before re-launching so the console keeps its history.
/// </summary>
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();
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 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<SceneAsset>(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<SceneAsset>(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;
}
}
/// <summary>
/// Writes make stdout/stderr to PSX Console and Unity console.
/// </summary>
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);
}
}
}