Broken RUntime

This commit is contained in:
Jan Racek
2026-03-27 13:47:18 +01:00
parent 6bf74fa929
commit d29ef569b3
16 changed files with 1168 additions and 1371 deletions

View File

@@ -0,0 +1,230 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Downloads and manages mkpsxiso — the tool that builds PlayStation CD images
/// from an XML catalog. Used for the ISO build target.
/// https://github.com/Lameguy64/mkpsxiso
/// </summary>
public static class MkpsxisoDownloader
{
private const string MKPSXISO_VERSION = "2.20";
private const string MKPSXISO_RELEASE_BASE =
"https://github.com/Lameguy64/mkpsxiso/releases/download/v" + MKPSXISO_VERSION + "/";
private static readonly HttpClient _http = new HttpClient();
/// <summary>
/// Install directory for mkpsxiso inside .tools/
/// </summary>
public static string MkpsxisoDir =>
Path.Combine(SplashBuildPaths.ToolsDir, "mkpsxiso");
/// <summary>
/// Path to the mkpsxiso binary.
/// </summary>
public static string MkpsxisoBinary
{
get
{
if (Application.platform == RuntimePlatform.WindowsEditor)
return Path.Combine(MkpsxisoDir, "mkpsxiso.exe");
return Path.Combine(MkpsxisoDir, "mkpsxiso");
}
}
/// <summary>
/// Returns true if mkpsxiso is installed and ready to use.
/// </summary>
public static bool IsInstalled() => File.Exists(MkpsxisoBinary);
/// <summary>
/// Downloads and installs mkpsxiso from the official GitHub releases.
/// </summary>
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
{
string archiveName;
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-win64.zip";
break;
case RuntimePlatform.LinuxEditor:
archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-Linux.zip";
break;
case RuntimePlatform.OSXEditor:
archiveName = $"mkpsxiso-{MKPSXISO_VERSION}-Darwin.zip";
break;
default:
log?.Invoke("Unsupported platform for mkpsxiso.");
return false;
}
string downloadUrl = $"{MKPSXISO_RELEASE_BASE}{archiveName}";
log?.Invoke($"Downloading mkpsxiso: {downloadUrl}");
try
{
string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
EditorUtility.DisplayProgressBar("Downloading mkpsxiso", "Downloading...", 0.1f);
using (var response = await _http.GetAsync(downloadUrl,
HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
long downloaded = 0;
using (var fs = File.Create(tempFile))
using (var stream = await response.Content.ReadAsStreamAsync())
{
byte[] buffer = new byte[81920];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fs.WriteAsync(buffer, 0, bytesRead);
downloaded += bytesRead;
if (totalBytes.HasValue)
{
float progress = (float)downloaded / totalBytes.Value;
EditorUtility.DisplayProgressBar("Downloading mkpsxiso",
$"{downloaded / 1024}/{totalBytes.Value / 1024} KB", progress);
}
}
}
}
log?.Invoke("Extracting...");
EditorUtility.DisplayProgressBar("Installing mkpsxiso", "Extracting...", 0.9f);
string installDir = MkpsxisoDir;
if (Directory.Exists(installDir))
Directory.Delete(installDir, true);
Directory.CreateDirectory(installDir);
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
// Fix nested directory (archives often have one extra level)
FixNestedDirectory(installDir);
try { File.Delete(tempFile); } catch { }
EditorUtility.ClearProgressBar();
if (IsInstalled())
{
// Make executable on Linux/Mac
if (Application.platform != RuntimePlatform.WindowsEditor)
{
var chmod = Process.Start("chmod", $"+x \"{MkpsxisoBinary}\"");
chmod?.WaitForExit();
}
log?.Invoke("mkpsxiso installed successfully!");
return true;
}
log?.Invoke($"mkpsxiso binary not found at: {MkpsxisoBinary}");
return false;
}
catch (Exception ex)
{
log?.Invoke($"mkpsxiso download failed: {ex.Message}");
EditorUtility.ClearProgressBar();
return false;
}
}
private static void FixNestedDirectory(string dir)
{
// If extraction created exactly one subdirectory, flatten it
var subdirs = Directory.GetDirectories(dir);
if (subdirs.Length == 1)
{
string nested = subdirs[0];
foreach (string file in Directory.GetFiles(nested))
{
string dest = Path.Combine(dir, Path.GetFileName(file));
if (!File.Exists(dest)) File.Move(file, dest);
}
foreach (string sub in Directory.GetDirectories(nested))
{
string dest = Path.Combine(dir, Path.GetFileName(sub));
if (!Directory.Exists(dest)) Directory.Move(sub, dest);
}
try { Directory.Delete(nested, true); } catch { }
}
}
/// <summary>
/// Runs mkpsxiso with the given XML catalog to produce a BIN/CUE image.
/// </summary>
/// <param name="xmlPath">Path to the mkpsxiso XML catalog.</param>
/// <param name="outputBin">Override output .bin path (optional, uses XML default if null).</param>
/// <param name="outputCue">Override output .cue path (optional, uses XML default if null).</param>
/// <param name="log">Logging callback.</param>
/// <returns>True if mkpsxiso succeeded.</returns>
public static bool BuildISO(string xmlPath, string outputBin = null,
string outputCue = null, Action<string> log = null)
{
if (!IsInstalled())
{
log?.Invoke("mkpsxiso is not installed.");
return false;
}
// Build arguments
string args = $"-y \"{xmlPath}\"";
if (!string.IsNullOrEmpty(outputBin))
args += $" -o \"{outputBin}\"";
if (!string.IsNullOrEmpty(outputCue))
args += $" -c \"{outputCue}\"";
log?.Invoke($"Running: mkpsxiso {args}");
var psi = new ProcessStartInfo
{
FileName = MkpsxisoBinary,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
try
{
var process = Process.Start(psi);
string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(stdout))
log?.Invoke(stdout.Trim());
if (process.ExitCode != 0)
{
if (!string.IsNullOrEmpty(stderr))
log?.Invoke($"mkpsxiso error: {stderr.Trim()}");
log?.Invoke($"mkpsxiso exited with code {process.ExitCode}");
return false;
}
log?.Invoke("ISO image built successfully.");
return true;
}
catch (Exception ex)
{
log?.Invoke($"mkpsxiso execution failed: {ex.Message}");
return false;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 45aea686b641c474dba05b83956d8947

View File

@@ -52,17 +52,14 @@ namespace SplashEdit.EditorCode
/// </summary>
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
{
string platformSuffix;
string archiveName;
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
platformSuffix = "x86_64-pc-windows-msvc";
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.zip";
archiveName = $"psxavenc-windows.zip";
break;
case RuntimePlatform.LinuxEditor:
platformSuffix = "x86_64-unknown-linux-gnu";
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.tar.gz";
archiveName = $"psxavenc-linux.zip";
break;
default:
log?.Invoke("Only Windows and Linux are supported.");

View File

@@ -130,6 +130,24 @@ namespace SplashEdit.EditorCode
public static string CUEOutputPath =>
Path.Combine(BuildOutputDir, "psxsplash.cue");
/// <summary>
/// XML catalog path used by mkpsxiso to build the ISO image.
/// </summary>
public static string ISOCatalogPath =>
Path.Combine(BuildOutputDir, "psxsplash.xml");
/// <summary>
/// SYSTEM.CNF file path generated for the ISO image.
/// The PS1 BIOS reads this to find and launch the executable.
/// </summary>
public static string SystemCnfPath =>
Path.Combine(BuildOutputDir, "SYSTEM.CNF");
/// <summary>
/// Checks if mkpsxiso is installed in the tools directory.
/// </summary>
public static bool IsMkpsxisoInstalled() => MkpsxisoDownloader.IsInstalled();
/// <summary>
/// Ensures the build output and tools directories exist.
/// Also appends entries to the project .gitignore if not present.

View File

@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
@@ -46,6 +47,7 @@ namespace SplashEdit.EditorCode
private bool _hasRedux;
private bool _hasNativeProject;
private bool _hasPsxavenc;
private bool _hasMkpsxiso;
private string _reduxVersion = "";
// ───── Native project installer ─────
@@ -486,6 +488,22 @@ namespace SplashEdit.EditorCode
}
EditorGUILayout.EndHorizontal();
// mkpsxiso (ISO builder)
EditorGUILayout.BeginHorizontal();
DrawStatusIcon(_hasMkpsxiso);
GUILayout.Label("mkpsxiso (ISO)", GUILayout.Width(160));
GUILayout.FlexibleSpace();
if (!_hasMkpsxiso)
{
if (GUILayout.Button("Download", GUILayout.Width(80)))
DownloadMkpsxiso();
}
else
{
GUILayout.Label("Installed", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
// Refresh button
EditorGUILayout.Space(2);
EditorGUILayout.BeginHorizontal();
@@ -653,14 +671,11 @@ namespace SplashEdit.EditorCode
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)))
@@ -673,11 +688,6 @@ namespace SplashEdit.EditorCode
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Open Scene Validator", EditorStyles.miniButton))
{
PSXSceneValidatorWindow.ShowWindow();
}
EditorGUILayout.EndVertical();
}
@@ -711,6 +721,34 @@ namespace SplashEdit.EditorCode
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();
}
EditorGUILayout.Space(8);
// Big Build & Run button
@@ -759,7 +797,7 @@ namespace SplashEdit.EditorCode
}
if (GUILayout.Button("Compile Only", EditorStyles.miniButton, GUILayout.Width(100)))
{
CompileNative();
CompileOnly();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
@@ -774,12 +812,11 @@ namespace SplashEdit.EditorCode
/// <summary>
/// The main pipeline: Validate → Export all scenes → Compile → Launch.
/// </summary>
public void BuildAndRun()
public async 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);
@@ -787,7 +824,6 @@ namespace SplashEdit.EditorCode
try
{
// Step 1: Validate
Log("Validating toolchain...", LogType.Log);
if (!ValidateToolchain())
{
@@ -796,7 +832,6 @@ namespace SplashEdit.EditorCode
}
Log("Toolchain OK.", LogType.Log);
// Step 2: Export all scenes
Log("Exporting scenes...", LogType.Log);
if (!ExportAllScenes())
{
@@ -805,16 +840,15 @@ namespace SplashEdit.EditorCode
}
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
// Step 3: Compile native
Log("Compiling native code...", LogType.Log);
if (!CompileNative())
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);
// Step 4: Launch
Log("Launching...", LogType.Log);
Launch();
}
@@ -852,6 +886,11 @@ namespace SplashEdit.EditorCode
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))
@@ -902,7 +941,7 @@ namespace SplashEdit.EditorCode
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single);
// Find the exporter
var exporter = UnityEngine.Object.FindObjectOfType<PSXSceneExporter>();
var exporter = UnityEngine.Object.FindFirstObjectByType<PSXSceneExporter>();
if (exporter == null)
{
Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning);
@@ -1051,10 +1090,29 @@ namespace SplashEdit.EditorCode
// ───── Step 3: Compile ─────
/// <summary>
/// Runs make in the native project directory.
/// </summary>
public bool CompileNative()
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))
@@ -1064,9 +1122,11 @@ namespace SplashEdit.EditorCode
}
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();
if (SplashSettings.Target == BuildTarget.ISO)
buildArg += " LOADER=cdrom";
string makeCmd = $"make clean && make all -j{SystemInfo.processorCount} {buildArg}".Trim();
Log($"Running: {makeCmd}", LogType.Log);
var psi = new ProcessStartInfo
@@ -1084,45 +1144,57 @@ namespace SplashEdit.EditorCode
try
{
var process = Process.Start(psi);
string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
var stdoutBuf = new System.Text.StringBuilder();
var stderrBuf = new System.Text.StringBuilder();
// Log output to panel only (no Unity console spam)
if (!string.IsNullOrEmpty(stdout))
process.OutputDataReceived += (s, e) =>
{
foreach (string line in stdout.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Log);
}
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 (process.ExitCode != 0)
if (exitCode != 0)
{
if (!string.IsNullOrEmpty(stderr))
foreach (string line in stderr.Split('\n'))
{
foreach (string line in stderr.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
if (!string.IsNullOrWhiteSpace(line))
LogToPanel(line.Trim(), LogType.Error);
}
Log($"Make exited with code {process.ExitCode}", LogType.Error);
Log($"Make exited with code {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);
Log("Copied .ps-exe to PSXBuild/", LogType.Log);
}
else
{
@@ -1172,7 +1244,7 @@ namespace SplashEdit.EditorCode
LaunchToHardware();
break;
case BuildTarget.ISO:
Log("ISO build not yet implemented.", LogType.Warning);
BuildAndLaunchISO();
break;
}
}
@@ -1274,6 +1346,235 @@ namespace SplashEdit.EditorCode
}
}
// ───── 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("&", "&amp;").Replace("<", "&lt;")
.Replace(">", "&gt;").Replace("\"", "&quot;");
}
private void StopPCdrvHost()
{
if (_pcdrvHost != null)
@@ -1339,6 +1640,8 @@ namespace SplashEdit.EditorCode
_hasPsxavenc = PSXAudioConverter.IsInstalled();
_hasMkpsxiso = MkpsxisoDownloader.IsInstalled();
string nativeDir = SplashBuildPaths.NativeSourceDir;
_hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir);
}
@@ -1418,6 +1721,22 @@ namespace SplashEdit.EditorCode
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

View File

@@ -124,12 +124,6 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", value);
}
public static bool AutoValidateOnExport
{
get => EditorPrefs.GetBool(Prefix + "AutoValidate", true);
set => EditorPrefs.SetBool(Prefix + "AutoValidate", value);
}
// --- Play Mode Intercept ---
public static bool InterceptPlayMode
{
@@ -137,6 +131,27 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetBool(Prefix + "InterceptPlayMode", value);
}
// --- ISO Build ---
/// <summary>
/// Optional path to a Sony license file (.dat) for the ISO image.
/// If empty, the ISO will be built without license data (homebrew-only).
/// The file must be in raw 2336-byte sector format (from PsyQ SDK LCNSFILE).
/// </summary>
public static string LicenseFilePath
{
get => EditorPrefs.GetString(Prefix + "LicenseFilePath", "");
set => EditorPrefs.SetString(Prefix + "LicenseFilePath", value);
}
/// <summary>
/// Volume label for the ISO image (up to 31 characters, uppercase).
/// </summary>
public static string ISOVolumeLabel
{
get => EditorPrefs.GetString(Prefix + "ISOVolumeLabel", "PSXSPLASH");
set => EditorPrefs.SetString(Prefix + "ISOVolumeLabel", value);
}
/// <summary>
/// Resets all settings to defaults by deleting all prefixed keys.
/// </summary>
@@ -147,7 +162,8 @@ namespace SplashEdit.EditorCode
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
"GTEScaling", "AutoValidate", "InterceptPlayMode"
"GTEScaling", "AutoValidate", "InterceptPlayMode",
"LicenseFilePath", "ISOVolumeLabel"
};
foreach (string key in keys)

View File

@@ -26,7 +26,7 @@ namespace SplashEdit.EditorCode
[MenuItem("GameObject/PlayStation 1/Scene Exporter", false, 10)]
public static void CreateSceneExporter(MenuCommand menuCommand)
{
var existing = Object.FindObjectOfType<PSXSceneExporter>();
var existing = Object.FindFirstObjectByType<PSXSceneExporter>();
if (existing != null)
{
EditorUtility.DisplayDialog(

View File

@@ -2,512 +2,233 @@ using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
using System.Collections.Generic;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXObjectExporter with enhanced UX.
/// Shows mesh info, texture preview, collision visualization, and validation.
/// </summary>
[CustomEditor(typeof(PSXObjectExporter))]
[CanEditMultipleObjects]
public class PSXObjectExporterEditor : UnityEditor.Editor
{
// Serialized properties
private SerializedProperty isActiveProp;
private SerializedProperty bitDepthProp;
private SerializedProperty luaFileProp;
private SerializedProperty objectFlagsProp;
private SerializedProperty collisionTypeProp;
private SerializedProperty staticColliderProp;
private SerializedProperty exportCollisionMeshProp;
private SerializedProperty customCollisionMeshProp;
private SerializedProperty collisionLayerProp;
private SerializedProperty previewNormalsProp;
private SerializedProperty normalPreviewLengthProp;
private SerializedProperty showCollisionBoundsProp;
private SerializedProperty textureProp;
// UI State
private bool showMeshInfo = true;
private bool showTextureInfo = true;
private bool showExportSettings = true;
private bool showCollisionSettings = true;
private bool showGizmoSettings = false;
private bool showValidation = true;
// Cached data
private SerializedProperty generateNavigationProp;
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private int triangleCount;
private int vertexCount;
private Bounds meshBounds;
private List<string> validationErrors = new List<string>();
private List<string> validationWarnings = new List<string>();
// Styles
private GUIStyle headerStyle;
private GUIStyle errorStyle;
private GUIStyle warningStyle;
// Validation
private bool _validationDirty = true;
private bool showExport = true;
private bool showCollision = true;
private void OnEnable()
{
// Get serialized properties
isActiveProp = serializedObject.FindProperty("isActive");
bitDepthProp = serializedObject.FindProperty("bitDepth");
luaFileProp = serializedObject.FindProperty("luaFile");
objectFlagsProp = serializedObject.FindProperty("objectFlags");
collisionTypeProp = serializedObject.FindProperty("collisionType");
staticColliderProp = serializedObject.FindProperty("staticCollider");
exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh");
customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh");
collisionLayerProp = serializedObject.FindProperty("collisionLayer");
previewNormalsProp = serializedObject.FindProperty("previewNormals");
normalPreviewLengthProp = serializedObject.FindProperty("normalPreviewLength");
showCollisionBoundsProp = serializedObject.FindProperty("showCollisionBounds");
textureProp = serializedObject.FindProperty("texture");
// Cache mesh info
generateNavigationProp = serializedObject.FindProperty("generateNavigation");
CacheMeshInfo();
// Defer validation to first inspector draw
_validationDirty = true;
}
private void CacheMeshInfo()
{
var exporter = target as PSXObjectExporter;
if (exporter == null) return;
meshFilter = exporter.GetComponent<MeshFilter>();
meshRenderer = exporter.GetComponent<MeshRenderer>();
if (meshFilter != null && meshFilter.sharedMesh != null)
{
var mesh = meshFilter.sharedMesh;
triangleCount = mesh.triangles.Length / 3;
vertexCount = mesh.vertexCount;
meshBounds = mesh.bounds;
triangleCount = meshFilter.sharedMesh.triangles.Length / 3;
vertexCount = meshFilter.sharedMesh.vertexCount;
}
}
private void RunValidation()
{
validationErrors.Clear();
validationWarnings.Clear();
var exporter = target as PSXObjectExporter;
if (exporter == null) return;
// Check mesh
if (meshFilter == null || meshFilter.sharedMesh == null)
{
validationErrors.Add("No mesh assigned to MeshFilter");
}
else
{
if (triangleCount > 100)
{
validationWarnings.Add($"High triangle count ({triangleCount}). PS1 recommended: <100 per object");
}
// Check vertex bounds
var mesh = meshFilter.sharedMesh;
var verts = mesh.vertices;
bool hasOutOfBounds = false;
foreach (var v in verts)
{
var world = exporter.transform.TransformPoint(v);
float scaled = Mathf.Max(Mathf.Abs(world.x), Mathf.Abs(world.y), Mathf.Abs(world.z)) * 4096f;
if (scaled > 32767f)
{
hasOutOfBounds = true;
break;
}
}
if (hasOutOfBounds)
{
validationErrors.Add("Vertices exceed PS1 coordinate limits (±8 units from origin)");
}
}
// Check renderer
if (meshRenderer == null)
{
validationWarnings.Add("No MeshRenderer - object will not be visible");
}
else if (meshRenderer.sharedMaterial == null)
{
validationWarnings.Add("No material assigned - will use default colors");
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Run deferred validation
if (_validationDirty)
{
RunValidation();
_validationDirty = false;
}
InitStyles();
// Active toggle at top
EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object"));
DrawHeader();
EditorGUILayout.Space(4);
if (!isActiveProp.boolValue)
{
EditorGUILayout.HelpBox("This object will be skipped during export.", MessageType.Info);
EditorGUILayout.LabelField("Object will be skipped during export.", PSXEditorStyles.InfoBox);
serializedObject.ApplyModifiedProperties();
return;
}
EditorGUILayout.Space(5);
// Mesh Info Section
DrawMeshInfoSection();
// Texture Section
DrawTextureSection();
// Export Settings Section
DrawExportSettingsSection();
// Collision Settings Section
DrawCollisionSettingsSection();
// Gizmo Settings Section
DrawGizmoSettingsSection();
// Validation Section
DrawValidationSection();
// Action Buttons
DrawActionButtons();
if (serializedObject.ApplyModifiedProperties())
{
_validationDirty = true;
}
DrawMeshSummary();
PSXEditorStyles.DrawSeparator(6, 6);
DrawExportSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawCollisionSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawActions();
serializedObject.ApplyModifiedProperties();
}
private void InitStyles()
private new void DrawHeader()
{
if (headerStyle == null)
{
headerStyle = new GUIStyle(EditorStyles.foldoutHeader);
}
if (errorStyle == null)
{
errorStyle = new GUIStyle(EditorStyles.label);
errorStyle.normal.textColor = Color.red;
}
if (warningStyle == null)
{
warningStyle = new GUIStyle(EditorStyles.label);
warningStyle.normal.textColor = new Color(1f, 0.7f, 0f);
}
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(isActiveProp, GUIContent.none, GUILayout.Width(18));
var exporter = target as PSXObjectExporter;
EditorGUILayout.LabelField(exporter.gameObject.name, PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawMeshInfoSection()
private void DrawMeshSummary()
{
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information");
if (showMeshInfo)
if (meshFilter == null || meshFilter.sharedMesh == null)
{
EditorGUI.indentLevel++;
if (meshFilter != null && meshFilter.sharedMesh != null)
{
EditorGUILayout.LabelField("Mesh", meshFilter.sharedMesh.name);
EditorGUILayout.LabelField("Triangles", triangleCount.ToString());
EditorGUILayout.LabelField("Vertices", vertexCount.ToString());
EditorGUILayout.LabelField("Bounds Size", meshBounds.size.ToString("F2"));
// Triangle budget bar
float budgetPercent = triangleCount / 100f;
Rect rect = EditorGUILayout.GetControlRect(false, 20);
EditorGUI.ProgressBar(rect, Mathf.Clamp01(budgetPercent), $"Triangle Budget: {triangleCount}/100");
}
else
{
EditorGUILayout.HelpBox("No mesh assigned", MessageType.Warning);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawTextureSection()
{
showTextureInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showTextureInfo, "Texture Settings");
if (showTextureInfo)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(textureProp, new GUIContent("Override Texture"));
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
// Show texture preview if assigned
var tex = textureProp.objectReferenceValue as Texture2D;
if (tex != null)
{
EditorGUILayout.Space(5);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
Rect previewRect = GUILayoutUtility.GetRect(64, 64, GUILayout.Width(64));
EditorGUI.DrawPreviewTexture(previewRect, tex);
GUILayout.FlexibleSpace();
}
EditorGUILayout.LabelField($"Size: {tex.width}x{tex.height}");
// VRAM estimate
int bpp = bitDepthProp.enumValueIndex == 0 ? 4 : (bitDepthProp.enumValueIndex == 1 ? 8 : 16);
int vramBytes = (tex.width * tex.height * bpp) / 8;
EditorGUILayout.LabelField($"Est. VRAM: {vramBytes} bytes ({bpp}bpp)");
}
else if (meshRenderer != null && meshRenderer.sharedMaterial != null)
{
var matTex = meshRenderer.sharedMaterial.mainTexture;
if (matTex != null)
{
EditorGUILayout.HelpBox($"Using material texture: {matTex.name}", MessageType.Info);
}
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawExportSettingsSection()
{
showExportSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showExportSettings, "Export Settings");
if (showExportSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(objectFlagsProp, new GUIContent("Object Flags"));
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
// Quick Lua file buttons
if (luaFileProp.objectReferenceValue != null)
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Edit Lua", GUILayout.Width(80)))
{
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
}
if (GUILayout.Button("Clear", GUILayout.Width(60)))
{
luaFileProp.objectReferenceValue = null;
}
}
}
else
{
if (GUILayout.Button("Create New Lua Script"))
{
CreateNewLuaScript();
}
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawCollisionSettingsSection()
{
showCollisionSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showCollisionSettings, "Collision Settings");
if (showCollisionSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Collision Type"));
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
if (collType != PSXCollisionType.None)
{
EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Collision Mesh"));
EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Collision Layer"));
// Collision info
EditorGUILayout.Space(5);
string collisionInfo = collType switch
{
PSXCollisionType.Solid => "Solid: Blocks movement, fires onCollision",
PSXCollisionType.Trigger => "Trigger: Fires onTriggerEnter/Exit, doesn't block",
PSXCollisionType.Platform => "Platform: Solid from above only",
_ => ""
};
EditorGUILayout.HelpBox(collisionInfo, MessageType.Info);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawGizmoSettingsSection()
{
showGizmoSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showGizmoSettings, "Gizmo Settings");
if (showGizmoSettings)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(previewNormalsProp, new GUIContent("Preview Normals"));
if (previewNormalsProp.boolValue)
{
EditorGUILayout.PropertyField(normalPreviewLengthProp, new GUIContent("Normal Length"));
}
EditorGUILayout.PropertyField(showCollisionBoundsProp, new GUIContent("Show Collision Bounds"));
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawValidationSection()
{
if (validationErrors.Count == 0 && validationWarnings.Count == 0)
EditorGUILayout.HelpBox("No mesh on this object.", MessageType.Warning);
return;
showValidation = EditorGUILayout.BeginFoldoutHeaderGroup(showValidation, "Validation");
if (showValidation)
{
foreach (var error in validationErrors)
{
EditorGUILayout.HelpBox(error, MessageType.Error);
}
foreach (var warning in validationWarnings)
{
EditorGUILayout.HelpBox(warning, MessageType.Warning);
}
if (GUILayout.Button("Refresh Validation"))
{
CacheMeshInfo();
RunValidation();
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"{triangleCount} tris", PSXEditorStyles.RichLabel, GUILayout.Width(60));
EditorGUILayout.LabelField($"{vertexCount} verts", PSXEditorStyles.RichLabel, GUILayout.Width(70));
int subMeshCount = meshFilter.sharedMesh.subMeshCount;
if (subMeshCount > 1)
EditorGUILayout.LabelField($"{subMeshCount} submeshes", PSXEditorStyles.RichLabel, GUILayout.Width(90));
int matCount = meshRenderer != null ? meshRenderer.sharedMaterials.Length : 0;
int textured = meshRenderer != null
? meshRenderer.sharedMaterials.Count(m => m != null && m.mainTexture != null)
: 0;
if (textured > 0)
EditorGUILayout.LabelField($"{textured}/{matCount} textured", PSXEditorStyles.RichLabel);
else
EditorGUILayout.LabelField("untextured", PSXEditorStyles.RichLabel);
EditorGUILayout.EndHorizontal();
}
private void DrawActionButtons()
private void DrawExportSection()
{
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
showExport = EditorGUILayout.Foldout(showExport, "Export", true, PSXEditorStyles.FoldoutHeader);
if (!showExport) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
if (luaFileProp.objectReferenceValue != null)
{
if (GUILayout.Button("Select Scene Exporter"))
{
var exporter = FindObjectOfType<PSXSceneExporter>();
if (exporter != null)
{
Selection.activeGameObject = exporter.gameObject;
}
else
{
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
}
if (GUILayout.Button("Open Scene Validator"))
{
PSXSceneValidatorWindow.ShowWindow();
}
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", EditorStyles.miniButtonLeft, GUILayout.Width(50)))
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
if (GUILayout.Button("Clear", EditorStyles.miniButtonRight, GUILayout.Width(50)))
luaFileProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Create Lua Script", EditorStyles.miniButton, GUILayout.Width(130)))
CreateNewLuaScript();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
}
private void DrawCollisionSection()
{
showCollision = EditorGUILayout.Foldout(showCollision, "Collision", true, PSXEditorStyles.FoldoutHeader);
if (!showCollision) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Type"));
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
if (collType != PSXCollisionType.None)
{
EditorGUILayout.PropertyField(staticColliderProp, new GUIContent("Static"));
bool isStatic = staticColliderProp.boolValue;
if (isStatic)
{
EditorGUILayout.LabelField(
"<color=#88cc88>Baked into world collision mesh. No runtime cost.</color>",
PSXEditorStyles.RichLabel);
}
else
{
EditorGUILayout.LabelField(
"<color=#88aaff>Runtime AABB collider. Fires Lua collision events.</color>",
PSXEditorStyles.RichLabel);
}
EditorGUILayout.Space(2);
EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Mesh"));
EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Layer"));
}
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(generateNavigationProp, new GUIContent("Generate Navigation"));
EditorGUI.indentLevel--;
}
private void DrawActions()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select Scene Exporter", EditorStyles.miniButton))
{
var se = FindFirstObjectByType<PSXSceneExporter>();
if (se != null)
Selection.activeGameObject = se.gameObject;
else
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
EditorGUILayout.EndHorizontal();
}
private void CreateNewLuaScript()
{
var exporter = target as PSXObjectExporter;
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script",
defaultName + ".lua",
"lua",
"Create Lua Script", defaultName + ".lua", "lua",
"Create a new Lua script for this object");
if (!string.IsNullOrEmpty(path))
if (string.IsNullOrEmpty(path)) return;
string template =
$"function onCreate(self)\nend\n\nfunction onUpdate(self, dt)\nend\n";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
string template = $@"-- Lua script for {exporter.gameObject.name}
--
-- Available globals: Entity, Vec3, Input, Timer, Camera, Audio,
-- Debug, Math, Scene, Persist
--
-- Available events:
-- onCreate(self) — called once when the object is registered
-- onUpdate(self, dt) — called every frame (dt = delta frames, usually 1)
-- onEnable(self) — called when the object becomes active
-- onDisable(self) — called when the object becomes inactive
-- onCollision(self, other) — called on collision with another object
-- onTriggerEnter(self, other)
-- onTriggerStay(self, other)
-- onTriggerExit(self, other)
-- onInteract(self) — called when the player interacts
-- onButtonPress(self, btn) — called on button press (btn = Input.CROSS etc.)
-- onButtonRelease(self, btn)
-- onDestroy(self) — called before the object is destroyed
--
-- Properties: self.position (Vec3), self.rotationY (pi-units), self.active (bool)
function onCreate(self)
-- Called once when this object is registered in the scene
end
function onUpdate(self, dt)
-- Called every frame. dt = number of elapsed frames (usually 1).
end
function onInteract(self)
-- Called when the player interacts with this object
end
";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
}
[MenuItem("CONTEXT/PSXObjectExporter/Copy Settings to Selected")]
private static void CopySettingsToSelected(MenuCommand command)
{
var source = command.context as PSXObjectExporter;
if (source == null) return;
foreach (var go in Selection.gameObjects)
{
var target = go.GetComponent<PSXObjectExporter>();
if (target != null && target != source)
{
Undo.RecordObject(target, "Copy PSX Settings");
// Copy via serialized object
EditorUtility.CopySerialized(source, target);
}
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -1,38 +1,49 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXSceneExporter.
/// When the component is selected and fog is enabled, activates a Unity scene-view
/// fog preview that approximates the PS1 linear fog distances.
///
/// Fog distance mapping:
/// fogFarSZ = 8000 / FogDensity (GTE SZ units)
/// fogNearSZ = fogFarSZ / 3
/// SZ is 20.12 fixed-point: SZ = (unityCoord / GTEScaling) * 4096
/// => unityDist = SZ * GTEScaling / 4096
/// => Unity fog near = (8000 / (FogDensity * 3)) * GTEScaling / 4096
/// => Unity fog far = (8000 / FogDensity) * GTEScaling / 4096
/// </summary>
[CustomEditor(typeof(PSXSceneExporter))]
public class PSXSceneExporterEditor : UnityEditor.Editor
{
// Saved RenderSettings state so we can restore it on deselect.
private SerializedProperty gteScalingProp;
private SerializedProperty sceneLuaProp;
private SerializedProperty fogEnabledProp;
private SerializedProperty fogColorProp;
private SerializedProperty fogDensityProp;
private SerializedProperty sceneTypeProp;
private SerializedProperty cutscenesProp;
private SerializedProperty loadingScreenProp;
private SerializedProperty previewBVHProp;
private SerializedProperty bvhDepthProp;
private bool _savedFog;
private Color _savedFogColor;
private FogMode _savedFogMode;
private float _savedFogStart;
private float _savedFogEnd;
private bool _previewActive = false;
private bool showFog = true;
private bool showCutscenes = true;
private bool showDebug = false;
private void OnEnable()
{
gteScalingProp = serializedObject.FindProperty("GTEScaling");
sceneLuaProp = serializedObject.FindProperty("SceneLuaFile");
fogEnabledProp = serializedObject.FindProperty("FogEnabled");
fogColorProp = serializedObject.FindProperty("FogColor");
fogDensityProp = serializedObject.FindProperty("FogDensity");
sceneTypeProp = serializedObject.FindProperty("SceneType");
cutscenesProp = serializedObject.FindProperty("Cutscenes");
loadingScreenProp = serializedObject.FindProperty("LoadingScreenPrefab");
previewBVHProp = serializedObject.FindProperty("PreviewBVH");
bvhDepthProp = serializedObject.FindProperty("BVHPreviewDepth");
SaveAndApplyFogPreview();
// Re-apply whenever the scene is repainted (handles inspector value changes).
EditorApplication.update += OnEditorUpdate;
}
@@ -44,11 +55,155 @@ namespace SplashEdit.EditorCode
private void OnEditorUpdate()
{
// Keep the preview in sync when the user tweaks values in the inspector.
if (_previewActive)
ApplyFogPreview();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
var exporter = (PSXSceneExporter)target;
DrawHeader();
EditorGUILayout.Space(4);
DrawSceneSettings();
PSXEditorStyles.DrawSeparator(6, 6);
DrawFogSection(exporter);
PSXEditorStyles.DrawSeparator(6, 6);
DrawCutscenesSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawLoadingSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawDebugSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawSceneStats();
serializedObject.ApplyModifiedProperties();
}
private void DrawHeader()
{
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.LabelField("Scene Exporter", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.EndVertical();
}
private void DrawSceneSettings()
{
EditorGUILayout.PropertyField(sceneTypeProp, new GUIContent("Scene Type"));
bool isInterior = (PSXSceneType)sceneTypeProp.enumValueIndex == PSXSceneType.Interior;
EditorGUILayout.LabelField(
isInterior
? "<color=#88aaff>Room/portal occlusion culling.</color>"
: "<color=#88cc88>BVH frustum culling.</color>",
PSXEditorStyles.RichLabel);
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(gteScalingProp, new GUIContent("GTE Scaling"));
EditorGUILayout.PropertyField(sceneLuaProp, new GUIContent("Scene Lua"));
if (sceneLuaProp.objectReferenceValue != null)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", EditorStyles.miniButtonLeft, GUILayout.Width(50)))
AssetDatabase.OpenAsset(sceneLuaProp.objectReferenceValue);
if (GUILayout.Button("Clear", EditorStyles.miniButtonRight, GUILayout.Width(50)))
sceneLuaProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
}
private void DrawFogSection(PSXSceneExporter exporter)
{
showFog = EditorGUILayout.Foldout(showFog, "Fog", true, PSXEditorStyles.FoldoutHeader);
if (!showFog) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(fogEnabledProp, new GUIContent("Enabled"));
if (fogEnabledProp.boolValue)
{
EditorGUILayout.PropertyField(fogColorProp, new GUIContent("Color"));
EditorGUILayout.PropertyField(fogDensityProp, new GUIContent("Density"));
float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
float fogFarUnity = (8000f / density) * gteScale / 4096f;
float fogNearUnity = fogFarUnity / 3f;
EditorGUILayout.Space(2);
EditorGUILayout.LabelField(
$"<color=#aaaaaa>Preview: {fogNearUnity:F1} {fogFarUnity:F1} units | " +
$"GTE: {8000f / (density * 3f):F0} {8000f / density:F0} SZ</color>",
PSXEditorStyles.RichLabel);
ApplyFogPreview();
}
else
{
RenderSettings.fog = false;
}
EditorGUI.indentLevel--;
}
private void DrawCutscenesSection()
{
showCutscenes = EditorGUILayout.Foldout(showCutscenes, "Cutscenes", true, PSXEditorStyles.FoldoutHeader);
if (!showCutscenes) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(cutscenesProp, new GUIContent("Clips"), true);
EditorGUI.indentLevel--;
}
private void DrawLoadingSection()
{
EditorGUILayout.PropertyField(loadingScreenProp, new GUIContent("Loading Screen Prefab"));
if (loadingScreenProp.objectReferenceValue != null)
{
var go = loadingScreenProp.objectReferenceValue as GameObject;
if (go != null && go.GetComponentInChildren<PSXCanvas>() == null)
{
EditorGUILayout.LabelField(
"<color=#ffaa44>Prefab has no PSXCanvas component.</color>",
PSXEditorStyles.RichLabel);
}
}
}
private void DrawDebugSection()
{
showDebug = EditorGUILayout.Foldout(showDebug, "Debug", true, PSXEditorStyles.FoldoutHeader);
if (!showDebug) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(previewBVHProp, new GUIContent("Preview BVH"));
if (previewBVHProp.boolValue)
EditorGUILayout.PropertyField(bvhDepthProp, new GUIContent("BVH Depth"));
EditorGUI.indentLevel--;
}
private void DrawSceneStats()
{
var exporters = FindObjectsOfType<PSXObjectExporter>();
int total = exporters.Length;
int active = exporters.Count(e => e.IsActive);
int withCollision = exporters.Count(e => e.CollisionType != PSXCollisionType.None);
int staticCol = exporters.Count(e => e.StaticCollider && e.CollisionType != PSXCollisionType.None);
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.LabelField(
$"<b>{active}</b>/{total} objects | <b>{withCollision}</b> colliders ({staticCol} static)",
PSXEditorStyles.RichLabel);
EditorGUILayout.EndVertical();
}
private void SaveAndApplyFogPreview()
{
_savedFog = RenderSettings.fog;
@@ -56,7 +211,6 @@ namespace SplashEdit.EditorCode
_savedFogMode = RenderSettings.fogMode;
_savedFogStart = RenderSettings.fogStartDistance;
_savedFogEnd = RenderSettings.fogEndDistance;
_previewActive = true;
ApplyFogPreview();
}
@@ -68,76 +222,31 @@ namespace SplashEdit.EditorCode
if (!exporter.FogEnabled)
{
// Fog disabled on the component - turn off the preview.
RenderSettings.fog = false;
return;
}
float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
// fogFarSZ in GTE SZ units (20.12 fp); convert to Unity world-space.
// SZ = (unityDist / GTEScaling) * 4096, so unityDist = SZ * GTEScaling / 4096
float fogFarSZ = 8000f / density;
float fogNearSZ = fogFarSZ / 3f;
float fogFarUnity = fogFarSZ * gteScale / 4096f;
float fogNearUnity = fogNearSZ * gteScale / 4096f;
RenderSettings.fog = true;
RenderSettings.fogColor = exporter.FogColor;
RenderSettings.fogMode = FogMode.Linear;
RenderSettings.fogStartDistance = fogNearUnity;
RenderSettings.fogEndDistance = fogFarUnity;
RenderSettings.fogStartDistance = fogNearSZ * gteScale / 4096f;
RenderSettings.fogEndDistance = fogFarSZ * gteScale / 4096f;
}
private void RestoreFog()
{
if (!_previewActive) return;
_previewActive = false;
RenderSettings.fog = _savedFog;
RenderSettings.fogColor = _savedFogColor;
RenderSettings.fogMode = _savedFogMode;
RenderSettings.fogStartDistance = _savedFogStart;
RenderSettings.fogEndDistance = _savedFogEnd;
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
// Show computed fog distances when fog is enabled, so the user
// can see exactly what range the preview represents.
var exporter = (PSXSceneExporter)target;
if (exporter.FogEnabled)
{
EditorGUILayout.Space(4);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Fog Preview (active in Scene view)", EditorStyles.boldLabel);
float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10);
float fogFarUnity = (8000f / density) * gteScale / 4096f;
float fogNearUnity = fogFarUnity / 3f;
EditorGUILayout.LabelField("Near distance", $"{fogNearUnity:F1} Unity units");
EditorGUILayout.LabelField("Far distance", $"{fogFarUnity:F1} Unity units");
EditorGUILayout.LabelField("(PS1 SZ range)", $"{8000f / (density * 3f):F0} - {8000f / density:F0} GTE units");
EditorGUILayout.EndVertical();
// Keep preview applied as values may have changed.
ApplyFogPreview();
}
else
{
// Make sure preview is off when fog is disabled.
RenderSettings.fog = false;
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -1,496 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Scene Validator Window - Validates the current scene for PS1 compatibility.
/// Checks for common issues that would cause problems on real hardware.
/// </summary>
public class PSXSceneValidatorWindow : EditorWindow
{
private Vector2 scrollPosition;
private List<ValidationResult> validationResults = new List<ValidationResult>();
private bool hasValidated = false;
private int errorCount = 0;
private int warningCount = 0;
private int infoCount = 0;
// Filter toggles
private bool showErrors = true;
private bool showWarnings = true;
private bool showInfo = true;
// PS1 Limits
private const int MAX_RECOMMENDED_TRIS_PER_OBJECT = 100;
private const int MAX_RECOMMENDED_TOTAL_TRIS = 400;
private const int MAX_VERTEX_COORD = 32767; // signed 16-bit
private const int MIN_VERTEX_COORD = -32768;
private const int VRAM_WIDTH = 1024;
private const int VRAM_HEIGHT = 512;
private static readonly Vector2 MinSize = new Vector2(500, 400);
public static void ShowWindow()
{
var window = GetWindow<PSXSceneValidatorWindow>("Scene Validator");
window.minSize = MinSize;
}
private void OnEnable()
{
validationResults.Clear();
hasValidated = false;
}
private void OnGUI()
{
DrawHeader();
DrawFilters();
DrawResults();
DrawFooter();
}
private void DrawHeader()
{
EditorGUILayout.Space(5);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("PS1 Scene Validator", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
if (GUILayout.Button("Validate Scene", GUILayout.Width(120)))
{
ValidateScene();
}
}
EditorGUILayout.Space(5);
// Summary bar
if (hasValidated)
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
{
var errorStyle = new GUIStyle(EditorStyles.label);
errorStyle.normal.textColor = errorCount > 0 ? Color.red : Color.green;
GUILayout.Label($"✗ {errorCount} Errors", errorStyle);
var warnStyle = new GUIStyle(EditorStyles.label);
warnStyle.normal.textColor = warningCount > 0 ? new Color(1f, 0.7f, 0f) : Color.green;
GUILayout.Label($"⚠ {warningCount} Warnings", warnStyle);
var infoStyle = new GUIStyle(EditorStyles.label);
infoStyle.normal.textColor = Color.cyan;
GUILayout.Label($" {infoCount} Info", infoStyle);
GUILayout.FlexibleSpace();
}
}
EditorGUILayout.Space(5);
}
private void DrawFilters()
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("Show:", GUILayout.Width(40));
showErrors = GUILayout.Toggle(showErrors, "Errors", EditorStyles.miniButtonLeft);
showWarnings = GUILayout.Toggle(showWarnings, "Warnings", EditorStyles.miniButtonMid);
showInfo = GUILayout.Toggle(showInfo, "Info", EditorStyles.miniButtonRight);
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
}
private void DrawResults()
{
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
{
scrollPosition = scrollView.scrollPosition;
if (!hasValidated)
{
EditorGUILayout.HelpBox("Click 'Validate Scene' to check for PS1 compatibility issues.", MessageType.Info);
return;
}
if (validationResults.Count == 0)
{
EditorGUILayout.HelpBox("No issues found! Your scene looks ready for PS1 export.", MessageType.Info);
return;
}
foreach (var result in validationResults)
{
if (result.Type == ValidationType.Error && !showErrors) continue;
if (result.Type == ValidationType.Warning && !showWarnings) continue;
if (result.Type == ValidationType.Info && !showInfo) continue;
DrawValidationResult(result);
}
}
}
private void DrawValidationResult(ValidationResult result)
{
MessageType msgType = result.Type switch
{
ValidationType.Error => MessageType.Error,
ValidationType.Warning => MessageType.Warning,
_ => MessageType.Info
};
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.HelpBox(result.Message, msgType);
if (result.RelatedObject != null)
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label("Object:", GUILayout.Width(50));
if (GUILayout.Button(result.RelatedObject.name, EditorStyles.linkLabel))
{
Selection.activeObject = result.RelatedObject;
EditorGUIUtility.PingObject(result.RelatedObject);
}
GUILayout.FlexibleSpace();
if (!string.IsNullOrEmpty(result.FixAction))
{
if (GUILayout.Button("Fix", GUILayout.Width(50)))
{
ApplyFix(result);
}
}
}
}
}
EditorGUILayout.Space(2);
}
private void DrawFooter()
{
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Select All With Errors"))
{
var errorObjects = validationResults
.Where(r => r.Type == ValidationType.Error && r.RelatedObject != null)
.Select(r => r.RelatedObject)
.Distinct()
.ToArray();
Selection.objects = errorObjects;
}
}
}
private void ValidateScene()
{
validationResults.Clear();
errorCount = 0;
warningCount = 0;
infoCount = 0;
// Check for scene exporter
ValidateSceneExporter();
// Check all PSX objects
ValidatePSXObjects();
// Check textures and VRAM
ValidateTextures();
// Check Lua files
ValidateLuaFiles();
// Overall scene stats
ValidateSceneStats();
hasValidated = true;
Repaint();
}
private void ValidateSceneExporter()
{
var exporters = Object.FindObjectsOfType<PSXSceneExporter>();
if (exporters.Length == 0)
{
AddResult(ValidationType.Error,
"No PSXSceneExporter found in scene. Add one via GameObject > PlayStation 1 > Scene Exporter",
null, "AddExporter");
}
else if (exporters.Length > 1)
{
AddResult(ValidationType.Warning,
$"Multiple PSXSceneExporters found ({exporters.Length}). Only one is needed per scene.",
exporters[0].gameObject);
}
}
private void ValidatePSXObjects()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
if (exporters.Length == 0)
{
AddResult(ValidationType.Info,
"No objects marked for PSX export. Add PSXObjectExporter components to GameObjects you want to export.",
null);
return;
}
foreach (var exporter in exporters)
{
ValidateSingleObject(exporter);
}
}
private void ValidateSingleObject(PSXObjectExporter exporter)
{
var go = exporter.gameObject;
// Check for mesh
var meshFilter = go.GetComponent<MeshFilter>();
if (meshFilter == null || meshFilter.sharedMesh == null)
{
AddResult(ValidationType.Warning,
$"'{go.name}' has no mesh. It will be exported as an empty object.",
go);
return;
}
var mesh = meshFilter.sharedMesh;
int triCount = mesh.triangles.Length / 3;
// Check triangle count
if (triCount > MAX_RECOMMENDED_TRIS_PER_OBJECT)
{
AddResult(ValidationType.Warning,
$"'{go.name}' has {triCount} triangles (recommended max: {MAX_RECOMMENDED_TRIS_PER_OBJECT}). Consider simplifying.",
go);
}
// Check vertex coordinates for GTE limits
var vertices = mesh.vertices;
var transform = go.transform;
bool hasOutOfBounds = false;
foreach (var vert in vertices)
{
var worldPos = transform.TransformPoint(vert);
// Check if fixed-point conversion would overflow (assuming scale factor)
float scaledX = worldPos.x * 4096f; // FixedPoint<12> scale
float scaledY = worldPos.y * 4096f;
float scaledZ = worldPos.z * 4096f;
if (scaledX > MAX_VERTEX_COORD || scaledX < MIN_VERTEX_COORD ||
scaledY > MAX_VERTEX_COORD || scaledY < MIN_VERTEX_COORD ||
scaledZ > MAX_VERTEX_COORD || scaledZ < MIN_VERTEX_COORD)
{
hasOutOfBounds = true;
break;
}
}
if (hasOutOfBounds)
{
AddResult(ValidationType.Error,
$"'{go.name}' has vertices that exceed PS1 coordinate limits. Move closer to origin or scale down.",
go);
}
// Check for renderer and material
var renderer = go.GetComponent<MeshRenderer>();
if (renderer == null)
{
AddResult(ValidationType.Info,
$"'{go.name}' has no MeshRenderer. Will be exported without visual rendering.",
go);
}
else if (renderer.sharedMaterial == null)
{
AddResult(ValidationType.Warning,
$"'{go.name}' has no material assigned. Will use default colors.",
go);
}
// Check texture settings on exporter
if (exporter.texture != null)
{
ValidateTexture(exporter.texture, go);
}
}
private void ValidateTextures()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
var textures = exporters
.Where(e => e.texture != null)
.Select(e => e.texture)
.Distinct()
.ToList();
if (textures.Count == 0)
{
AddResult(ValidationType.Info,
"No textures assigned to any PSX objects. Scene will be vertex-colored only.",
null);
return;
}
// Rough VRAM estimation
int estimatedVramUsage = 0;
foreach (var tex in textures)
{
// Rough estimate: width * height * bits/8
// This is simplified - actual packing is more complex
int bitsPerPixel = 16; // Assume 16bpp worst case
estimatedVramUsage += (tex.width * tex.height * bitsPerPixel) / 8;
}
int vramTotal = VRAM_WIDTH * VRAM_HEIGHT * 2; // 16bpp
int vramAvailable = vramTotal / 2; // Assume half for framebuffers
if (estimatedVramUsage > vramAvailable)
{
AddResult(ValidationType.Warning,
$"Estimated texture VRAM usage ({estimatedVramUsage / 1024}KB) may exceed available space (~{vramAvailable / 1024}KB). " +
"Consider using lower bit depths or smaller textures.",
null);
}
}
private void ValidateTexture(Texture2D texture, GameObject relatedObject)
{
// Check power of 2
if (!Mathf.IsPowerOfTwo(texture.width) || !Mathf.IsPowerOfTwo(texture.height))
{
AddResult(ValidationType.Warning,
$"Texture '{texture.name}' dimensions ({texture.width}x{texture.height}) are not power of 2. May cause issues.",
relatedObject);
}
// Check max size
if (texture.width > 256 || texture.height > 256)
{
AddResult(ValidationType.Warning,
$"Texture '{texture.name}' is large ({texture.width}x{texture.height}). Consider using 256x256 or smaller.",
relatedObject);
}
}
private void ValidateLuaFiles()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
foreach (var exporter in exporters)
{
if (exporter.LuaFile != null)
{
// Check if Lua file exists and is valid
string path = AssetDatabase.GetAssetPath(exporter.LuaFile);
if (string.IsNullOrEmpty(path))
{
AddResult(ValidationType.Error,
$"'{exporter.name}' references an invalid Lua file.",
exporter.gameObject);
}
}
}
}
private void ValidateSceneStats()
{
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
int totalTris = 0;
foreach (var exporter in exporters)
{
var mf = exporter.GetComponent<MeshFilter>();
if (mf != null && mf.sharedMesh != null)
{
totalTris += mf.sharedMesh.triangles.Length / 3;
}
}
AddResult(ValidationType.Info,
$"Scene statistics: {exporters.Length} objects, {totalTris} total triangles.",
null);
if (totalTris > MAX_RECOMMENDED_TOTAL_TRIS)
{
AddResult(ValidationType.Warning,
$"Total triangle count ({totalTris}) exceeds recommended maximum ({MAX_RECOMMENDED_TOTAL_TRIS}). " +
"Performance may be poor on real hardware.",
null);
}
}
private void AddResult(ValidationType type, string message, GameObject relatedObject, string fixAction = null)
{
validationResults.Add(new ValidationResult
{
Type = type,
Message = message,
RelatedObject = relatedObject,
FixAction = fixAction
});
switch (type)
{
case ValidationType.Error: errorCount++; break;
case ValidationType.Warning: warningCount++; break;
case ValidationType.Info: infoCount++; break;
}
}
private void ApplyFix(ValidationResult result)
{
switch (result.FixAction)
{
case "AddExporter":
var go = new GameObject("PSXSceneExporter");
go.AddComponent<PSXSceneExporter>();
Undo.RegisterCreatedObjectUndo(go, "Create PSX Scene Exporter");
Selection.activeGameObject = go;
ValidateScene(); // Re-validate
break;
}
}
private enum ValidationType
{
Error,
Warning,
Info
}
private class ValidationResult
{
public ValidationType Type;
public string Message;
public GameObject RelatedObject;
public string FixAction;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 0a26bf89301a2554ca287b9e28e44906

View File

@@ -176,8 +176,7 @@ namespace SplashEdit.EditorCode
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Wait for exit with timeout
var timeout = TimeSpan.FromSeconds(30);
var timeout = TimeSpan.FromMinutes(10);
if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
{
process.WaitForExit(); // Ensure all output is processed