2053 lines
82 KiB
C#
2053 lines
82 KiB
C#
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
|
||
{
|
||
/// <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 %#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<SceneEntry> _sceneList = new List<SceneEntry>();
|
||
|
||
// ───── Memory Reports ─────
|
||
private List<SceneMemoryReport> _memoryReports = new List<SceneMemoryReport>();
|
||
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<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;
|
||
}
|
||
|
||
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);
|
||
|
||
SplashSettings.FpsOverlay = EditorGUILayout.Toggle(
|
||
new GUIContent("FPS Overlay", "Show an FPS counter at top-left during gameplay"),
|
||
SplashSettings.FpsOverlay);
|
||
|
||
// 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(
|
||
$"<b>{report.gameObjectCount}</b> objects | " +
|
||
$"<b>{report.triangleCount}</b> tris | " +
|
||
$"<b>{report.atlasCount}</b> atlases | " +
|
||
$"<b>{report.clutCount}</b> 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
|
||
// ═══════════════════════════════════════════════════════════════
|
||
|
||
/// <summary>
|
||
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
|
||
/// </summary>
|
||
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<PSXConsoleWindow>();
|
||
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 ─────
|
||
|
||
/// <summary>
|
||
/// Exports all scenes in the scene list to splashpack files in PSXBuild/.
|
||
/// </summary>
|
||
public bool ExportAllScenes()
|
||
{
|
||
SplashBuildPaths.EnsureDirectories();
|
||
_loaderPackCache = new Dictionary<string, string>();
|
||
_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<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);
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
private Dictionary<string, string> _loaderPackCache = new Dictionary<string, string>();
|
||
|
||
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<ProhibitedArea> 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<PSXUIImage>(true);
|
||
if (uiImages != null && uiImages.Length > 0)
|
||
{
|
||
List<PSXTexture2D> uiTextures = new List<PSXTexture2D>();
|
||
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<Rect> framebuffers = new List<Rect> { 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<bool> 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";
|
||
|
||
if (SplashSettings.FpsOverlay)
|
||
buildArg += " FPSOVERLAY=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<int>();
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Derive the executable name on disc from the volume label.
|
||
/// Uppercase, no extension, trimmed to 12 characters (ISO9660 limit).
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Generates the mkpsxiso XML catalog describing the ISO filesystem layout.
|
||
/// Includes SYSTEM.CNF, the executable, all splashpacks, loading packs, and manifest.
|
||
/// </summary>
|
||
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 version=\"1.0\" encoding=\"UTF-8\"?>");
|
||
xml.AppendLine("<iso_project image_name=\"psxsplash.bin\" cue_sheet=\"psxsplash.cue\">");
|
||
xml.AppendLine(" <track type=\"data\">");
|
||
xml.AppendLine(" <identifiers");
|
||
xml.AppendLine(" system=\"PLAYSTATION\"");
|
||
xml.AppendLine(" application=\"PLAYSTATION\"");
|
||
xml.AppendLine($" volume=\"{EscapeXml(volumeLabel)}\"");
|
||
xml.AppendLine($" volume_set=\"{EscapeXml(volumeLabel)}\"");
|
||
xml.AppendLine(" publisher=\"SPLASHEDIT\"");
|
||
xml.AppendLine(" data_preparer=\"MKPSXISO\"");
|
||
xml.AppendLine(" />");
|
||
|
||
// License file (optional)
|
||
string licensePath = SplashSettings.LicenseFilePath;
|
||
if (!string.IsNullOrEmpty(licensePath) && File.Exists(licensePath))
|
||
{
|
||
xml.AppendLine($" <license file=\"{EscapeXml(licensePath)}\"/>");
|
||
}
|
||
|
||
xml.AppendLine(" <directory_tree>");
|
||
|
||
// SYSTEM.CNF — must be first for BIOS to find it
|
||
string cnfPath = SplashBuildPaths.SystemCnfPath;
|
||
xml.AppendLine($" <file name=\"SYSTEM.CNF\" source=\"{EscapeXml(cnfPath)}\"/>");
|
||
|
||
// The executable — renamed to match what SYSTEM.CNF points to
|
||
string exePath = SplashBuildPaths.CompiledExePath;
|
||
string isoExeName = GetISOExeName();
|
||
xml.AppendLine($" <file name=\"{isoExeName}\" source=\"{EscapeXml(exePath)}\"/>");
|
||
|
||
// Manifest
|
||
string manifestPath = SplashBuildPaths.ManifestPath;
|
||
if (File.Exists(manifestPath))
|
||
{
|
||
xml.AppendLine($" <file name=\"MANIFEST.BIN\" source=\"{EscapeXml(manifestPath)}\"/>");
|
||
}
|
||
|
||
// 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($" <file name=\"{isoName}\" source=\"{EscapeXml(splashpack)}\"/>");
|
||
}
|
||
|
||
string loadingPack = SplashBuildPaths.GetSceneLoaderPackPath(i, _sceneList[i].name);
|
||
if (File.Exists(loadingPack))
|
||
{
|
||
string isoName = $"SCENE_{i}.LDG";
|
||
xml.AppendLine($" <file name=\"{isoName}\" source=\"{EscapeXml(loadingPack)}\"/>");
|
||
}
|
||
}
|
||
|
||
// Trailing dummy sectors to prevent drive runaway
|
||
xml.AppendLine(" <dummy sectors=\"128\"/>");
|
||
xml.AppendLine(" </directory_tree>");
|
||
xml.AppendLine(" </track>");
|
||
xml.AppendLine("</iso_project>");
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <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();
|
||
|
||
_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<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);
|
||
}
|
||
}
|
||
}
|