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 %#l"; // ───── UI State ───── private Vector2 _scrollPos; private int _selectedTab = 0; private static readonly string[] _tabNames = { "Dependencies", "Scenes", "Build" }; 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(); // ───── Memory Reports ───── private List _memoryReports = new List(); private bool _showMemoryReport = true; // ───── 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; } private void OnDisable() { } private void OnFocus() { RefreshToolchainStatus(); } // ═══════════════════════════════════════════════════════════════ // Main GUI // ═══════════════════════════════════════════════════════════════ private void OnGUI() { if (_isRunning && _pcdrvHost != null && !_pcdrvHost.IsRunning) { StopAll(); Log("PCdrv host connection lost.", LogType.Warning); } DrawHeader(); EditorGUILayout.Space(4); _selectedTab = PSXEditorStyles.DrawButtonGroup(_tabNames, _selectedTab, 28); EditorGUILayout.Space(4); _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos); switch (_selectedTab) { case 0: // Dependencies DrawToolchainSection(); EditorGUILayout.Space(2); DrawNativeProjectSection(); break; case 1: // Scenes DrawScenesSection(); break; case 2: // Build DrawBuildSection(); break; } EditorGUILayout.EndScrollView(); } // ═══════════════════════════════════════════════════════════════ // Header // ═══════════════════════════════════════════════════════════════ private void DrawHeader() { EditorGUILayout.BeginHorizontal(PSXEditorStyles.ToolbarStyle); GUILayout.Label("SplashEdit", PSXEditorStyles.WindowHeader); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); // Status bar { 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; } EditorGUILayout.BeginHorizontal(PSXEditorStyles.InfoBox); PSXEditorStyles.DrawStatusBadge(statusColor == PSXEditorStyles.Success ? "OK" : statusColor == PSXEditorStyles.Warning ? "WARN" : statusColor == PSXEditorStyles.Info ? "INFO" : "RUN", statusColor, 50); GUILayout.Label(statusText, PSXEditorStyles.RichLabel); EditorGUILayout.EndHorizontal(); } } // ═══════════════════════════════════════════════════════════════ // 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 ── PSXEditorStyles.DrawSeparator(4, 4); GUILayout.Label("Clone from GitHub", PSXEditorStyles.SectionHeader); 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) { GUILayout.Label(_nativeInstallStatus, PSXEditorStyles.InfoBox); } // 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 ── PSXEditorStyles.DrawSeparator(4, 4); GUILayout.Label("Or set path manually", PSXEditorStyles.SectionHeader); 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) { GUILayout.Label("Invalid path. The directory must contain a Makefile.", PSXEditorStyles.InfoBox); } 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", PSXEditorStyles.SecondaryButton, GUILayout.Width(70))) InstallMIPS(); } else { PSXEditorStyles.DrawStatusBadge("Ready", PSXEditorStyles.Success); } EditorGUILayout.EndHorizontal(); PSXEditorStyles.DrawSeparator(2, 2); // GNU Make EditorGUILayout.BeginHorizontal(); DrawStatusIcon(_hasMake); GUILayout.Label("GNU Make", GUILayout.Width(160)); GUILayout.FlexibleSpace(); if (!_hasMake) { if (GUILayout.Button("Install", PSXEditorStyles.SecondaryButton, GUILayout.Width(70))) InstallMake(); } else { PSXEditorStyles.DrawStatusBadge("Ready", PSXEditorStyles.Success); } EditorGUILayout.EndHorizontal(); PSXEditorStyles.DrawSeparator(2, 2); // PCSX-Redux EditorGUILayout.BeginHorizontal(); DrawStatusIcon(_hasRedux); GUILayout.Label("PCSX-Redux", GUILayout.Width(160)); GUILayout.FlexibleSpace(); if (!_hasRedux) { if (GUILayout.Button("Download", PSXEditorStyles.SecondaryButton, GUILayout.Width(80))) DownloadRedux(); } else { PSXEditorStyles.DrawStatusBadge(_reduxVersion, PSXEditorStyles.Success); } EditorGUILayout.EndHorizontal(); PSXEditorStyles.DrawSeparator(2, 2); // psxavenc (audio encoder) EditorGUILayout.BeginHorizontal(); DrawStatusIcon(_hasPsxavenc); GUILayout.Label("psxavenc (Audio)", GUILayout.Width(160)); GUILayout.FlexibleSpace(); if (!_hasPsxavenc) { if (GUILayout.Button("Download", PSXEditorStyles.SecondaryButton, GUILayout.Width(80))) DownloadPsxavenc(); } else { PSXEditorStyles.DrawStatusBadge("Installed", PSXEditorStyles.Success); } EditorGUILayout.EndHorizontal(); PSXEditorStyles.DrawSeparator(2, 2); // mkpsxiso (ISO builder) EditorGUILayout.BeginHorizontal(); DrawStatusIcon(_hasMkpsxiso); GUILayout.Label("mkpsxiso (ISO)", GUILayout.Width(160)); GUILayout.FlexibleSpace(); if (!_hasMkpsxiso) { if (GUILayout.Button("Download", PSXEditorStyles.SecondaryButton, GUILayout.Width(80))) DownloadMkpsxiso(); } else { PSXEditorStyles.DrawStatusBadge("Installed", PSXEditorStyles.Success); } 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) { GUILayout.Label( "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.", PSXEditorStyles.InfoBox); } // 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); // Framebuffer: hardcoded 320x240, vertical, dual-buffered GUILayout.Label("Framebuffer", PSXEditorStyles.SectionHeader); GUILayout.Label("Resolution: 320x240 (dual-buffered, vertical layout)", PSXEditorStyles.InfoBox); PSXEditorStyles.DrawSeparator(4, 4); GUILayout.Label("Advanced Tools", PSXEditorStyles.SectionHeader); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Open VRAM Editor", PSXEditorStyles.PrimaryButton, GUILayout.Height(26))) { VRAMEditorWindow.ShowWindow(); } if (GUILayout.Button("Quantized Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(26))) { 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 GUILayout.Label("Configuration", PSXEditorStyles.SectionHeader); 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(); // Clean Build toggle SplashSettings.CleanBuild = EditorGUILayout.Toggle("Clean Build", SplashSettings.CleanBuild); // Memory Overlay toggle SplashSettings.MemoryOverlay = EditorGUILayout.Toggle( new GUIContent("Memory Overlay", "Show heap/RAM usage bar at top-right during gameplay"), SplashSettings.MemoryOverlay); // 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(); } PSXEditorStyles.DrawSeparator(6, 6); // Big Build & Run button EditorGUI.BeginDisabledGroup(_isBuilding); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); var largeBuildButton = new GUIStyle(PSXEditorStyles.SuccessButton) { fontSize = 14, fontStyle = FontStyle.Bold, padding = new RectOffset(20, 20, 10, 10) }; string buttonLabel = _isBuilding ? "Building..." : (SplashSettings.Target == BuildTarget.ISO ? "BUILD" : "BUILD & RUN"); if (GUILayout.Button(buttonLabel, largeBuildButton, GUILayout.Width(200), GUILayout.Height(38))) { BuildAndRun(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUI.EndDisabledGroup(); // Stop button (if running - emulator or hardware PCdrv host) if (_isRunning) { EditorGUILayout.Space(4); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); string stopLabel = _emulatorProcess != null ? "STOP EMULATOR" : "STOP PCdrv HOST"; if (GUILayout.Button(stopLabel, PSXEditorStyles.DangerButton, GUILayout.Width(200), GUILayout.Height(26))) { StopAll(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } // Export-only / Compile-only PSXEditorStyles.DrawSeparator(4, 4); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Export Only", PSXEditorStyles.SecondaryButton, GUILayout.Width(100))) { ExportAllScenes(); } if (GUILayout.Button("Compile Only", PSXEditorStyles.SecondaryButton, GUILayout.Width(100))) { CompileOnly(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); // Memory report (shown after export) if (_memoryReports.Count > 0) { EditorGUILayout.Space(8); DrawMemoryReports(); } } // ═══════════════════════════════════════════════════════════════ // Memory Reports // ═══════════════════════════════════════════════════════════════ private void DrawMemoryReports() { _showMemoryReport = DrawSectionFoldout("Memory Report", _showMemoryReport); if (!_showMemoryReport) return; foreach (var report in _memoryReports) { EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle); GUILayout.Label($"Scene: {report.sceneName}", PSXEditorStyles.SectionHeader); EditorGUILayout.Space(4); // Main RAM bar DrawMemoryBar("Main RAM", report.TotalRamUsage, SceneMemoryReport.USABLE_RAM, report.RamPercent, new Color(0.3f, 0.6f, 1f), $"Scene: {FormatBytes(report.SceneRamUsage)} | " + $"Fixed: {FormatBytes(report.FixedOverhead)} | " + $"Free: {FormatBytes(report.RamFree)}"); EditorGUILayout.Space(4); // VRAM bar DrawMemoryBar("VRAM", report.TotalVramUsed, SceneMemoryReport.TOTAL_VRAM, report.VramPercent, new Color(0.9f, 0.5f, 0.2f), $"FB: {FormatBytes(report.framebufferSize)} | " + $"Tex: {FormatBytes(report.textureAtlasSize)} | " + $"CLUT: {FormatBytes(report.clutSize)} | " + $"Free: {FormatBytes(report.VramFree)}"); EditorGUILayout.Space(4); // SPU RAM bar DrawMemoryBar("SPU RAM", report.TotalSpuUsed, SceneMemoryReport.USABLE_SPU, report.SpuPercent, new Color(0.6f, 0.3f, 0.9f), report.audioClipCount > 0 ? $"{report.audioClipCount} clips | {FormatBytes(report.audioDataSize)} | Free: {FormatBytes(report.SpuFree)}" : "No audio clips"); EditorGUILayout.Space(4); // CD Storage (no bar, just info) EditorGUILayout.BeginHorizontal(); GUILayout.Label("CD Storage:", EditorStyles.miniLabel, GUILayout.Width(70)); GUILayout.Label( $"Scene: {FormatBytes(report.splashpackFileSize)}" + (report.loaderPackSize > 0 ? $" | Loader: {FormatBytes(report.loaderPackSize)}" : "") + $" | Total: {FormatBytes(report.TotalDiscSize)}", EditorStyles.miniLabel); EditorGUILayout.EndHorizontal(); // Summary stats PSXEditorStyles.DrawSeparator(4, 4); EditorGUILayout.LabelField( $"{report.gameObjectCount} objects | " + $"{report.triangleCount} tris | " + $"{report.atlasCount} atlases | " + $"{report.clutCount} CLUTs", PSXEditorStyles.RichLabel); EditorGUILayout.EndVertical(); EditorGUILayout.Space(4); } } private void DrawMemoryBar(string label, long used, long total, float percent, Color barColor, string details) { // Label row EditorGUILayout.BeginHorizontal(); GUILayout.Label(label, EditorStyles.boldLabel, GUILayout.Width(70)); GUILayout.Label($"{FormatBytes(used)} / {FormatBytes(total)} ({percent:F1}%)", EditorStyles.miniLabel); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); // Progress bar Rect barRect = GUILayoutUtility.GetRect(0, 16, GUILayout.ExpandWidth(true)); // Background EditorGUI.DrawRect(barRect, new Color(0.15f, 0.15f, 0.17f)); // Fill float fillFraction = Mathf.Clamp01((float)used / total); Rect fillRect = new Rect(barRect.x, barRect.y, barRect.width * fillFraction, barRect.height); // Color shifts toward red when over 80% Color fillColor = barColor; if (percent > 90f) fillColor = Color.Lerp(PSXEditorStyles.Warning, PSXEditorStyles.Error, (percent - 90f) / 10f); else if (percent > 80f) fillColor = Color.Lerp(barColor, PSXEditorStyles.Warning, (percent - 80f) / 10f); EditorGUI.DrawRect(fillRect, fillColor); // Border DrawRectOutline(barRect, new Color(0.3f, 0.3f, 0.35f)); // Percent text overlay var style = new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleCenter, normal = { textColor = Color.white } }; GUI.Label(barRect, $"{percent:F1}%", style); // Details row GUILayout.Label(details, EditorStyles.miniLabel); } private static void DrawRectOutline(Rect rect, Color color) { EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), color); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), color); EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1, rect.height), color); EditorGUI.DrawRect(new Rect(rect.xMax - 1, rect.y, 1, rect.height), color); } private static string FormatBytes(long bytes) { if (bytes < 0) return "N/A"; if (bytes < 1024) return $"{bytes} B"; if (bytes < 1024 * 1024) return $"{bytes / 1024f:F1} KB"; return $"{bytes / (1024f * 1024f):F2} MB"; } // ═══════════════════════════════════════════════════════════════ // Pipeline Actions // ═══════════════════════════════════════════════════════════════ /// /// The main pipeline: Validate → Export all scenes → Compile → Launch. /// public async void BuildAndRun() { if (_isBuilding) return; if (UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().isDirty) { int choice = EditorUtility.DisplayDialogComplex( "Unsaved Changes", "The current scene has unsaved changes. Save before building?", "Save and Build", // 0 "Cancel", // 1 "Build Without Saving" // 2 ); if (choice == 1) return; // Cancel if (choice == 0) EditorSceneManager.SaveOpenScenes(); } _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(); _memoryReports.Clear(); // 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); string loaderPath = null; exporter.ExportToPath(outputPath); Log($"Exported '{scene.name}' → {Path.GetFileName(outputPath)}", LogType.Log); // Export loading screen if assigned if (exporter.LoadingScreenPrefab != null) { loaderPath = SplashBuildPaths.GetSceneLoaderPackPath(i, scene.name); ExportLoaderPack(exporter.LoadingScreenPrefab, loaderPath, i, scene.name); } // Generate memory report for this scene try { var report = SceneMemoryAnalyzer.Analyze( scene.name, outputPath, loaderPath, exporter.LastExportAtlases, exporter.LastExportAudioSizes, exporter.LastExportFonts, exporter.LastExportTriangleCount); _memoryReports.Add(report); } catch (Exception reportEx) { Log($"Memory report for '{scene.name}' failed: {reportEx.Message}", LogType.Warning); } } 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"; if (SplashSettings.MemoryOverlay) buildArg += " MEMOVERLAY=1"; int jobCount = Math.Max(1, SystemInfo.processorCount - 1); string cleanPrefix = SplashSettings.CleanBuild ? "make clean && " : ""; string makeCmd = $"{cleanPrefix}make all -j{jobCount} {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 \"cd /d \"{nativeDir}\" && {makeCmd}\"" : $"-c \"cd \\\"{nativeDir}\\\" && {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}\" -pad1type dualshock -stdout -interpreter"; 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); _emulatorProcess.EnableRaisingEvents = true; _emulatorProcess.Exited += (s, e) => { EditorApplication.delayCall += () => { _isRunning = false; _emulatorProcess = null; PSXConsoleWindow.Detach(); Repaint(); }; }; _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(" "); // License file (optional) string licensePath = SplashSettings.LicenseFilePath; if (!string.IsNullOrEmpty(licensePath) && File.Exists(licensePath)) { xml.AppendLine($" "); } xml.AppendLine(" "); // SYSTEM.CNF — must be first for BIOS to find it string cnfPath = SplashBuildPaths.SystemCnfPath; xml.AppendLine($" "); // The executable — renamed to match what SYSTEM.CNF points to string exePath = SplashBuildPaths.CompiledExePath; string isoExeName = GetISOExeName(); xml.AppendLine($" "); // Manifest string manifestPath = SplashBuildPaths.ManifestPath; if (File.Exists(manifestPath)) { xml.AppendLine($" "); } // Scene splashpacks and loading packs for (int i = 0; i < _sceneList.Count; i++) { string splashpack = SplashBuildPaths.GetSceneSplashpackPath(i, _sceneList[i].name); if (File.Exists(splashpack)) { string isoName = $"SCENE_{i}.SPK"; xml.AppendLine($" "); } string loadingPack = SplashBuildPaths.GetSceneLoaderPackPath(i, _sceneList[i].name); if (File.Exists(loadingPack)) { string isoName = $"SCENE_{i}.LDG"; xml.AppendLine($" "); } } // Trailing dummy sectors to prevent drive runaway 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); } } }