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