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 { /// /// 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 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(); // 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 // ═══════════════════════════════════════════════════════════════ /// /// The main pipeline: Validate → Export all scenes → Compile → Launch. /// public void BuildAndRun() { if (_isBuilding) return; _isBuilding = true; // Open the PSX Console so build output is visible immediately 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 { // 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 ───── /// /// Exports all scenes in the scene list to splashpack files in PSXBuild/. /// 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(); 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 ───── /// /// Runs make in the native project directory. /// 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; } } /// /// 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(); 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(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); } } }