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

View File

@@ -130,6 +130,24 @@ namespace SplashEdit.EditorCode
public static string CUEOutputPath => public static string CUEOutputPath =>
Path.Combine(BuildOutputDir, "psxsplash.cue"); 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> /// <summary>
/// Ensures the build output and tools directories exist. /// Ensures the build output and tools directories exist.
/// Also appends entries to the project .gitignore if not present. /// 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;
using System.IO.Ports; using System.IO.Ports;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using UnityEditor; using UnityEditor;
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
using UnityEngine; using UnityEngine;
@@ -46,6 +47,7 @@ namespace SplashEdit.EditorCode
private bool _hasRedux; private bool _hasRedux;
private bool _hasNativeProject; private bool _hasNativeProject;
private bool _hasPsxavenc; private bool _hasPsxavenc;
private bool _hasMkpsxiso;
private string _reduxVersion = ""; private string _reduxVersion = "";
// ───── Native project installer ───── // ───── Native project installer ─────
@@ -486,6 +488,22 @@ namespace SplashEdit.EditorCode
} }
EditorGUILayout.EndHorizontal(); 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 // Refresh button
EditorGUILayout.Space(2); EditorGUILayout.Space(2);
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
@@ -653,14 +671,11 @@ namespace SplashEdit.EditorCode
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
// GTE Scaling
EditorGUILayout.LabelField("Export Settings", EditorStyles.boldLabel); EditorGUILayout.LabelField("Export Settings", EditorStyles.boldLabel);
SplashSettings.DefaultGTEScaling = EditorGUILayout.FloatField("Default GTE Scaling", SplashSettings.DefaultGTEScaling); SplashSettings.DefaultGTEScaling = EditorGUILayout.FloatField("Default GTE Scaling", SplashSettings.DefaultGTEScaling);
SplashSettings.AutoValidateOnExport = EditorGUILayout.Toggle("Auto-Validate on Export", SplashSettings.AutoValidateOnExport);
EditorGUILayout.Space(6); EditorGUILayout.Space(6);
// Open dedicated VRAM windows
EditorGUILayout.LabelField("Advanced Tools", EditorStyles.boldLabel); EditorGUILayout.LabelField("Advanced Tools", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Open VRAM Editor", GUILayout.Height(24))) if (GUILayout.Button("Open VRAM Editor", GUILayout.Height(24)))
@@ -673,11 +688,6 @@ namespace SplashEdit.EditorCode
} }
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Open Scene Validator", EditorStyles.miniButton))
{
PSXSceneValidatorWindow.ShowWindow();
}
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
} }
@@ -711,6 +721,34 @@ namespace SplashEdit.EditorCode
EditorGUILayout.EndHorizontal(); 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); EditorGUILayout.Space(8);
// Big Build & Run button // Big Build & Run button
@@ -759,7 +797,7 @@ namespace SplashEdit.EditorCode
} }
if (GUILayout.Button("Compile Only", EditorStyles.miniButton, GUILayout.Width(100))) if (GUILayout.Button("Compile Only", EditorStyles.miniButton, GUILayout.Width(100)))
{ {
CompileNative(); CompileOnly();
} }
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
@@ -774,12 +812,11 @@ namespace SplashEdit.EditorCode
/// <summary> /// <summary>
/// The main pipeline: Validate → Export all scenes → Compile → Launch. /// The main pipeline: Validate → Export all scenes → Compile → Launch.
/// </summary> /// </summary>
public void BuildAndRun() public async void BuildAndRun()
{ {
if (_isBuilding) return; if (_isBuilding) return;
_isBuilding = true; _isBuilding = true;
// Open the PSX Console so build output is visible immediately
var console = EditorWindow.GetWindow<PSXConsoleWindow>(); var console = EditorWindow.GetWindow<PSXConsoleWindow>();
console.titleContent = new GUIContent("PSX Console", EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image); console.titleContent = new GUIContent("PSX Console", EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
console.minSize = new Vector2(400, 200); console.minSize = new Vector2(400, 200);
@@ -787,7 +824,6 @@ namespace SplashEdit.EditorCode
try try
{ {
// Step 1: Validate
Log("Validating toolchain...", LogType.Log); Log("Validating toolchain...", LogType.Log);
if (!ValidateToolchain()) if (!ValidateToolchain())
{ {
@@ -796,7 +832,6 @@ namespace SplashEdit.EditorCode
} }
Log("Toolchain OK.", LogType.Log); Log("Toolchain OK.", LogType.Log);
// Step 2: Export all scenes
Log("Exporting scenes...", LogType.Log); Log("Exporting scenes...", LogType.Log);
if (!ExportAllScenes()) if (!ExportAllScenes())
{ {
@@ -805,16 +840,15 @@ namespace SplashEdit.EditorCode
} }
Log($"Exported {_sceneList.Count} scene(s).", LogType.Log); Log($"Exported {_sceneList.Count} scene(s).", LogType.Log);
// Step 3: Compile native
Log("Compiling native code...", LogType.Log); 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); Log("Compilation failed. Check build log.", LogType.Error);
return; return;
} }
Log("Compile succeeded.", LogType.Log); Log("Compile succeeded.", LogType.Log);
// Step 4: Launch
Log("Launching...", LogType.Log); Log("Launching...", LogType.Log);
Launch(); Launch();
} }
@@ -852,6 +886,11 @@ namespace SplashEdit.EditorCode
Log("PCSX-Redux not found. Click Download in the Toolchain section.", LogType.Error); Log("PCSX-Redux not found. Click Download in the Toolchain section.", LogType.Error);
return false; 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; string nativeDir = SplashBuildPaths.NativeSourceDir;
if (string.IsNullOrEmpty(nativeDir) || !Directory.Exists(nativeDir)) if (string.IsNullOrEmpty(nativeDir) || !Directory.Exists(nativeDir))
@@ -902,7 +941,7 @@ namespace SplashEdit.EditorCode
EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single); EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Single);
// Find the exporter // Find the exporter
var exporter = UnityEngine.Object.FindObjectOfType<PSXSceneExporter>(); var exporter = UnityEngine.Object.FindFirstObjectByType<PSXSceneExporter>();
if (exporter == null) if (exporter == null)
{ {
Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning); Log($"Scene '{scene.name}' has no PSXSceneExporter. Skipping.", LogType.Warning);
@@ -1051,10 +1090,29 @@ namespace SplashEdit.EditorCode
// ───── Step 3: Compile ───── // ───── Step 3: Compile ─────
/// <summary> private async void CompileOnly()
/// Runs make in the native project directory. {
/// </summary> if (_isBuilding) return;
public bool CompileNative() _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; string nativeDir = SplashBuildPaths.NativeSourceDir;
if (string.IsNullOrEmpty(nativeDir)) if (string.IsNullOrEmpty(nativeDir))
@@ -1064,9 +1122,11 @@ namespace SplashEdit.EditorCode
} }
string buildArg = SplashSettings.Mode == BuildMode.Debug ? "BUILD=Debug" : ""; 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); Log($"Running: {makeCmd}", LogType.Log);
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
@@ -1084,45 +1144,57 @@ namespace SplashEdit.EditorCode
try try
{ {
var process = Process.Start(psi); var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
string stdout = process.StandardOutput.ReadToEnd(); var stdoutBuf = new System.Text.StringBuilder();
string stderr = process.StandardError.ReadToEnd(); var stderrBuf = new System.Text.StringBuilder();
process.WaitForExit();
// Log output to panel only (no Unity console spam) process.OutputDataReceived += (s, e) =>
if (!string.IsNullOrEmpty(stdout))
{ {
foreach (string line in stdout.Split('\n')) if (e.Data != null) stdoutBuf.AppendLine(e.Data);
{ };
if (!string.IsNullOrWhiteSpace(line)) process.ErrorDataReceived += (s, e) =>
LogToPanel(line.Trim(), LogType.Log); {
} 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, File.WriteAllText(SplashBuildPaths.BuildLogPath,
$"=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}"); $"=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}");
return false; return false;
} }
// Copy the compiled exe to PSXBuild/
string exeSource = FindCompiledExe(nativeDir); string exeSource = FindCompiledExe(nativeDir);
if (!string.IsNullOrEmpty(exeSource)) if (!string.IsNullOrEmpty(exeSource))
{ {
File.Copy(exeSource, SplashBuildPaths.CompiledExePath, true); File.Copy(exeSource, SplashBuildPaths.CompiledExePath, true);
Log($"Copied .ps-exe to PSXBuild/", LogType.Log); Log("Copied .ps-exe to PSXBuild/", LogType.Log);
} }
else else
{ {
@@ -1172,7 +1244,7 @@ namespace SplashEdit.EditorCode
LaunchToHardware(); LaunchToHardware();
break; break;
case BuildTarget.ISO: case BuildTarget.ISO:
Log("ISO build not yet implemented.", LogType.Warning); BuildAndLaunchISO();
break; 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() private void StopPCdrvHost()
{ {
if (_pcdrvHost != null) if (_pcdrvHost != null)
@@ -1339,6 +1640,8 @@ namespace SplashEdit.EditorCode
_hasPsxavenc = PSXAudioConverter.IsInstalled(); _hasPsxavenc = PSXAudioConverter.IsInstalled();
_hasMkpsxiso = MkpsxisoDownloader.IsInstalled();
string nativeDir = SplashBuildPaths.NativeSourceDir; string nativeDir = SplashBuildPaths.NativeSourceDir;
_hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir); _hasNativeProject = !string.IsNullOrEmpty(nativeDir) && Directory.Exists(nativeDir);
} }
@@ -1418,6 +1721,22 @@ namespace SplashEdit.EditorCode
Repaint(); 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() private void ScanSerialPorts()
{ {
try try

View File

@@ -124,12 +124,6 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", value); 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 --- // --- Play Mode Intercept ---
public static bool InterceptPlayMode public static bool InterceptPlayMode
{ {
@@ -137,6 +131,27 @@ namespace SplashEdit.EditorCode
set => EditorPrefs.SetBool(Prefix + "InterceptPlayMode", value); 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> /// <summary>
/// Resets all settings to defaults by deleting all prefixed keys. /// Resets all settings to defaults by deleting all prefixed keys.
/// </summary> /// </summary>
@@ -147,7 +162,8 @@ namespace SplashEdit.EditorCode
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath", "Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate", "PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout", "ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
"GTEScaling", "AutoValidate", "InterceptPlayMode" "GTEScaling", "AutoValidate", "InterceptPlayMode",
"LicenseFilePath", "ISOVolumeLabel"
}; };
foreach (string key in keys) foreach (string key in keys)

View File

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

View File

@@ -2,147 +2,56 @@ using UnityEngine;
using UnityEditor; using UnityEditor;
using SplashEdit.RuntimeCode; using SplashEdit.RuntimeCode;
using System.Linq; using System.Linq;
using System.Collections.Generic;
namespace SplashEdit.EditorCode namespace SplashEdit.EditorCode
{ {
/// <summary>
/// Custom inspector for PSXObjectExporter with enhanced UX.
/// Shows mesh info, texture preview, collision visualization, and validation.
/// </summary>
[CustomEditor(typeof(PSXObjectExporter))] [CustomEditor(typeof(PSXObjectExporter))]
[CanEditMultipleObjects] [CanEditMultipleObjects]
public class PSXObjectExporterEditor : UnityEditor.Editor public class PSXObjectExporterEditor : UnityEditor.Editor
{ {
// Serialized properties
private SerializedProperty isActiveProp; private SerializedProperty isActiveProp;
private SerializedProperty bitDepthProp; private SerializedProperty bitDepthProp;
private SerializedProperty luaFileProp; private SerializedProperty luaFileProp;
private SerializedProperty objectFlagsProp;
private SerializedProperty collisionTypeProp; private SerializedProperty collisionTypeProp;
private SerializedProperty staticColliderProp;
private SerializedProperty exportCollisionMeshProp; private SerializedProperty exportCollisionMeshProp;
private SerializedProperty customCollisionMeshProp; private SerializedProperty customCollisionMeshProp;
private SerializedProperty collisionLayerProp; private SerializedProperty collisionLayerProp;
private SerializedProperty previewNormalsProp; private SerializedProperty generateNavigationProp;
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 MeshFilter meshFilter; private MeshFilter meshFilter;
private MeshRenderer meshRenderer; private MeshRenderer meshRenderer;
private int triangleCount; private int triangleCount;
private int vertexCount; private int vertexCount;
private Bounds meshBounds;
private List<string> validationErrors = new List<string>();
private List<string> validationWarnings = new List<string>();
// Styles private bool showExport = true;
private GUIStyle headerStyle; private bool showCollision = true;
private GUIStyle errorStyle;
private GUIStyle warningStyle;
// Validation
private bool _validationDirty = true;
private void OnEnable() private void OnEnable()
{ {
// Get serialized properties
isActiveProp = serializedObject.FindProperty("isActive"); isActiveProp = serializedObject.FindProperty("isActive");
bitDepthProp = serializedObject.FindProperty("bitDepth"); bitDepthProp = serializedObject.FindProperty("bitDepth");
luaFileProp = serializedObject.FindProperty("luaFile"); luaFileProp = serializedObject.FindProperty("luaFile");
objectFlagsProp = serializedObject.FindProperty("objectFlags");
collisionTypeProp = serializedObject.FindProperty("collisionType"); collisionTypeProp = serializedObject.FindProperty("collisionType");
staticColliderProp = serializedObject.FindProperty("staticCollider");
exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh"); exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh");
customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh"); customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh");
collisionLayerProp = serializedObject.FindProperty("collisionLayer"); collisionLayerProp = serializedObject.FindProperty("collisionLayer");
previewNormalsProp = serializedObject.FindProperty("previewNormals"); generateNavigationProp = serializedObject.FindProperty("generateNavigation");
normalPreviewLengthProp = serializedObject.FindProperty("normalPreviewLength");
showCollisionBoundsProp = serializedObject.FindProperty("showCollisionBounds");
textureProp = serializedObject.FindProperty("texture");
// Cache mesh info
CacheMeshInfo(); CacheMeshInfo();
// Defer validation to first inspector draw
_validationDirty = true;
} }
private void CacheMeshInfo() private void CacheMeshInfo()
{ {
var exporter = target as PSXObjectExporter; var exporter = target as PSXObjectExporter;
if (exporter == null) return; if (exporter == null) return;
meshFilter = exporter.GetComponent<MeshFilter>(); meshFilter = exporter.GetComponent<MeshFilter>();
meshRenderer = exporter.GetComponent<MeshRenderer>(); meshRenderer = exporter.GetComponent<MeshRenderer>();
if (meshFilter != null && meshFilter.sharedMesh != null) if (meshFilter != null && meshFilter.sharedMesh != null)
{ {
var mesh = meshFilter.sharedMesh; triangleCount = meshFilter.sharedMesh.triangles.Length / 3;
triangleCount = mesh.triangles.Length / 3; vertexCount = meshFilter.sharedMesh.vertexCount;
vertexCount = mesh.vertexCount;
meshBounds = mesh.bounds;
}
}
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");
} }
} }
@@ -150,290 +59,154 @@ namespace SplashEdit.EditorCode
{ {
serializedObject.Update(); serializedObject.Update();
// Run deferred validation DrawHeader();
if (_validationDirty) EditorGUILayout.Space(4);
{
RunValidation();
_validationDirty = false;
}
InitStyles();
// Active toggle at top
EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object"));
if (!isActiveProp.boolValue) 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(); serializedObject.ApplyModifiedProperties();
return; return;
} }
EditorGUILayout.Space(5); DrawMeshSummary();
PSXEditorStyles.DrawSeparator(6, 6);
DrawExportSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawCollisionSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawActions();
// Mesh Info Section serializedObject.ApplyModifiedProperties();
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;
}
} }
private void InitStyles() private new void DrawHeader()
{ {
if (headerStyle == null) EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
{
headerStyle = new GUIStyle(EditorStyles.foldoutHeader);
}
if (errorStyle == null) EditorGUILayout.BeginHorizontal();
{ EditorGUILayout.PropertyField(isActiveProp, GUIContent.none, GUILayout.Width(18));
errorStyle = new GUIStyle(EditorStyles.label); var exporter = target as PSXObjectExporter;
errorStyle.normal.textColor = Color.red; EditorGUILayout.LabelField(exporter.gameObject.name, PSXEditorStyles.CardHeaderStyle);
} EditorGUILayout.EndHorizontal();
if (warningStyle == null) EditorGUILayout.EndVertical();
{
warningStyle = new GUIStyle(EditorStyles.label);
warningStyle.normal.textColor = new Color(1f, 0.7f, 0f);
}
} }
private void DrawMeshInfoSection() private void DrawMeshSummary()
{ {
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information"); if (meshFilter == null || meshFilter.sharedMesh == null)
if (showMeshInfo)
{ {
EditorGUI.indentLevel++; EditorGUILayout.HelpBox("No mesh on this object.", MessageType.Warning);
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)
return; 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); showExport = EditorGUILayout.Foldout(showExport, "Export", true, PSXEditorStyles.FoldoutHeader);
if (!showExport) return;
using (new EditorGUILayout.HorizontalScope()) 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")) 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)
{ {
var exporter = FindObjectOfType<PSXSceneExporter>(); EditorGUILayout.LabelField(
if (exporter != null) "<color=#88cc88>Baked into world collision mesh. No runtime cost.</color>",
{ PSXEditorStyles.RichLabel);
Selection.activeGameObject = exporter.gameObject; }
} else
else {
{ EditorGUILayout.LabelField(
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK"); "<color=#88aaff>Runtime AABB collider. Fires Lua collision events.</color>",
} PSXEditorStyles.RichLabel);
} }
if (GUILayout.Button("Open Scene Validator")) EditorGUILayout.Space(2);
{ EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
PSXSceneValidatorWindow.ShowWindow(); 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() private void CreateNewLuaScript()
@@ -441,73 +214,21 @@ namespace SplashEdit.EditorCode
var exporter = target as PSXObjectExporter; var exporter = target as PSXObjectExporter;
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_"); string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject( string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script", "Create Lua Script", defaultName + ".lua", "lua",
defaultName + ".lua",
"lua",
"Create a new Lua script for this object"); "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} luaFileProp.objectReferenceValue = luaFile;
-- serializedObject.ApplyModifiedProperties();
-- 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);
}
} }
} }
} }

View File

@@ -1,38 +1,49 @@
using UnityEngine; using UnityEngine;
using UnityEditor; using UnityEditor;
using SplashEdit.RuntimeCode; using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode 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))] [CustomEditor(typeof(PSXSceneExporter))]
public class PSXSceneExporterEditor : UnityEditor.Editor 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 bool _savedFog;
private Color _savedFogColor; private Color _savedFogColor;
private FogMode _savedFogMode; private FogMode _savedFogMode;
private float _savedFogStart; private float _savedFogStart;
private float _savedFogEnd; private float _savedFogEnd;
private bool _previewActive = false; private bool _previewActive = false;
private bool showFog = true;
private bool showCutscenes = true;
private bool showDebug = false;
private void OnEnable() 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(); SaveAndApplyFogPreview();
// Re-apply whenever the scene is repainted (handles inspector value changes).
EditorApplication.update += OnEditorUpdate; EditorApplication.update += OnEditorUpdate;
} }
@@ -44,11 +55,155 @@ namespace SplashEdit.EditorCode
private void OnEditorUpdate() private void OnEditorUpdate()
{ {
// Keep the preview in sync when the user tweaks values in the inspector.
if (_previewActive) if (_previewActive)
ApplyFogPreview(); 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() private void SaveAndApplyFogPreview()
{ {
_savedFog = RenderSettings.fog; _savedFog = RenderSettings.fog;
@@ -56,7 +211,6 @@ namespace SplashEdit.EditorCode
_savedFogMode = RenderSettings.fogMode; _savedFogMode = RenderSettings.fogMode;
_savedFogStart = RenderSettings.fogStartDistance; _savedFogStart = RenderSettings.fogStartDistance;
_savedFogEnd = RenderSettings.fogEndDistance; _savedFogEnd = RenderSettings.fogEndDistance;
_previewActive = true; _previewActive = true;
ApplyFogPreview(); ApplyFogPreview();
} }
@@ -68,76 +222,31 @@ namespace SplashEdit.EditorCode
if (!exporter.FogEnabled) if (!exporter.FogEnabled)
{ {
// Fog disabled on the component - turn off the preview.
RenderSettings.fog = false; RenderSettings.fog = false;
return; return;
} }
float gteScale = exporter.GTEScaling; float gteScale = exporter.GTEScaling;
int density = Mathf.Clamp(exporter.FogDensity, 1, 10); 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 fogFarSZ = 8000f / density;
float fogNearSZ = fogFarSZ / 3f; float fogNearSZ = fogFarSZ / 3f;
float fogFarUnity = fogFarSZ * gteScale / 4096f;
float fogNearUnity = fogNearSZ * gteScale / 4096f;
RenderSettings.fog = true; RenderSettings.fog = true;
RenderSettings.fogColor = exporter.FogColor; RenderSettings.fogColor = exporter.FogColor;
RenderSettings.fogMode = FogMode.Linear; RenderSettings.fogMode = FogMode.Linear;
RenderSettings.fogStartDistance = fogNearUnity; RenderSettings.fogStartDistance = fogNearSZ * gteScale / 4096f;
RenderSettings.fogEndDistance = fogFarUnity; RenderSettings.fogEndDistance = fogFarSZ * gteScale / 4096f;
} }
private void RestoreFog() private void RestoreFog()
{ {
if (!_previewActive) return; if (!_previewActive) return;
_previewActive = false; _previewActive = false;
RenderSettings.fog = _savedFog; RenderSettings.fog = _savedFog;
RenderSettings.fogColor = _savedFogColor; RenderSettings.fogColor = _savedFogColor;
RenderSettings.fogMode = _savedFogMode; RenderSettings.fogMode = _savedFogMode;
RenderSettings.fogStartDistance = _savedFogStart; RenderSettings.fogStartDistance = _savedFogStart;
RenderSettings.fogEndDistance = _savedFogEnd; 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.BeginOutputReadLine();
process.BeginErrorReadLine(); process.BeginErrorReadLine();
// Wait for exit with timeout var timeout = TimeSpan.FromMinutes(10);
var timeout = TimeSpan.FromSeconds(30);
if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds))) if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds)))
{ {
process.WaitForExit(); // Ensure all output is processed process.WaitForExit(); // Ensure all output is processed

View File

@@ -84,13 +84,16 @@ namespace SplashEdit.RuntimeCode
foreach (var exporter in exporters) foreach (var exporter in exporters)
{ {
// Dynamic objects are handled by the runtime collision system, skip them
if (!exporter.StaticCollider && exporter.CollisionType != PSXCollisionType.None)
continue;
PSXCollisionType effectiveType = exporter.CollisionType; PSXCollisionType effectiveType = exporter.CollisionType;
if (effectiveType == PSXCollisionType.None) if (effectiveType == PSXCollisionType.None)
{ {
if (autoIncludeSolid) if (autoIncludeSolid)
{ {
// Auto-include as Solid so all geometry blocks the player
effectiveType = PSXCollisionType.Solid; effectiveType = PSXCollisionType.Solid;
autoIncluded++; autoIncluded++;
} }
@@ -146,8 +149,7 @@ namespace SplashEdit.RuntimeCode
flags = (byte)PSXSurfaceFlag.Solid; flags = (byte)PSXSurfaceFlag.Solid;
// Check if stairs (tagged on exporter or steep-ish) // Check if stairs (tagged on exporter or steep-ish)
if (exporter.ObjectFlags.HasFlag(PSXObjectFlags.Static) && if (dotUp < 0.95f && dotUp > cosWalkable)
dotUp < 0.95f && dotUp > cosWalkable)
{ {
flags |= (byte)PSXSurfaceFlag.Stairs; flags |= (byte)PSXSurfaceFlag.Stairs;
} }

View File

@@ -5,31 +5,12 @@ using UnityEngine.Serialization;
namespace SplashEdit.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
/// <summary>
/// Collision type for PS1 runtime
/// </summary>
public enum PSXCollisionType public enum PSXCollisionType
{
None = 0, // No collision
Solid = 1, // Solid collision - blocks movement
Trigger = 2, // Trigger - fires events but doesn't block
Platform = 3 // Platform - solid from above, passable from below
}
/// <summary>
/// Object behavior flags for PS1 runtime
/// </summary>
[System.Flags]
public enum PSXObjectFlags
{ {
None = 0, None = 0,
Static = 1 << 0, // Object never moves (can be optimized) Solid = 1,
Dynamic = 1 << 1, // Object can move Trigger = 2,
Visible = 1 << 2, // Object is rendered Platform = 3
CastsShadow = 1 << 3, // Object casts shadows (future)
ReceivesShadow = 1 << 4, // Object receives shadows (future)
Interactable = 1 << 5, // Player can interact with this
AlwaysRender = 1 << 6, // Skip frustum culling for this object
} }
[RequireComponent(typeof(Renderer))] [RequireComponent(typeof(Renderer))]
@@ -44,183 +25,69 @@ namespace SplashEdit.RuntimeCode
public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>(); public List<PSXTexture2D> Textures { get; set; } = new List<PSXTexture2D>();
public PSXMesh Mesh { get; protected set; } public PSXMesh Mesh { get; protected set; }
[Header("Export Settings")]
[FormerlySerializedAs("BitDepth")] [FormerlySerializedAs("BitDepth")]
[SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT; [SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT;
[SerializeField] private LuaFile luaFile; [SerializeField] private LuaFile luaFile;
[Header("Object Flags")]
[SerializeField] private PSXObjectFlags objectFlags = PSXObjectFlags.Static | PSXObjectFlags.Visible;
[Header("Collision Settings")]
[SerializeField] private PSXCollisionType collisionType = PSXCollisionType.None; [SerializeField] private PSXCollisionType collisionType = PSXCollisionType.None;
[SerializeField] private bool staticCollider = true;
[SerializeField] private bool exportCollisionMesh = false; [SerializeField] private bool exportCollisionMesh = false;
[SerializeField] private Mesh customCollisionMesh; // Optional simplified collision mesh [SerializeField] private Mesh customCollisionMesh;
[Tooltip("Layer mask for collision detection (1-8)")]
[Range(1, 8)] [Range(1, 8)]
[SerializeField] private int collisionLayer = 1; [SerializeField] private int collisionLayer = 1;
[Header("Navigation")]
[Tooltip("Include this object's walkable surfaces in nav region generation")]
[SerializeField] private bool generateNavigation = false; [SerializeField] private bool generateNavigation = false;
[Header("Gizmo Settings")]
[FormerlySerializedAs("PreviewNormals")]
[SerializeField] private bool previewNormals = false;
[SerializeField] private float normalPreviewLength = 0.5f;
[SerializeField] private bool showCollisionBounds = true;
// Public accessors for editor and export
public PSXBPP BitDepth => bitDepth; public PSXBPP BitDepth => bitDepth;
public PSXCollisionType CollisionType => collisionType; public PSXCollisionType CollisionType => collisionType;
public bool StaticCollider => staticCollider;
public bool ExportCollisionMesh => exportCollisionMesh; public bool ExportCollisionMesh => exportCollisionMesh;
public Mesh CustomCollisionMesh => customCollisionMesh; public Mesh CustomCollisionMesh => customCollisionMesh;
public int CollisionLayer => collisionLayer; public int CollisionLayer => collisionLayer;
public PSXObjectFlags ObjectFlags => objectFlags;
public bool GenerateNavigation => generateNavigation; public bool GenerateNavigation => generateNavigation;
// For assigning texture from editor
public Texture2D texture;
private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new(); private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new();
private void OnDrawGizmos()
{
if (previewNormals)
{
MeshFilter filter = GetComponent<MeshFilter>();
if (filter != null)
{
Mesh mesh = filter.sharedMesh;
Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Gizmos.color = Color.green;
for (int i = 0; i < vertices.Length; i++)
{
Vector3 worldVertex = transform.TransformPoint(vertices[i]);
Vector3 worldNormal = transform.TransformDirection(normals[i]);
Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength);
}
}
}
}
private void OnDrawGizmosSelected()
{
// Draw collision bounds when object is selected
if (showCollisionBounds && collisionType != PSXCollisionType.None)
{
MeshFilter filter = GetComponent<MeshFilter>();
Mesh collisionMesh = customCollisionMesh != null ? customCollisionMesh : (filter?.sharedMesh);
if (collisionMesh != null)
{
Bounds bounds = collisionMesh.bounds;
// Choose color based on collision type
switch (collisionType)
{
case PSXCollisionType.Solid:
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.5f); // Red
break;
case PSXCollisionType.Trigger:
Gizmos.color = new Color(0.3f, 1f, 0.3f, 0.5f); // Green
break;
case PSXCollisionType.Platform:
Gizmos.color = new Color(0.3f, 0.3f, 1f, 0.5f); // Blue
break;
}
// Draw AABB
Matrix4x4 oldMatrix = Gizmos.matrix;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireCube(bounds.center, bounds.size);
// Draw filled with lower alpha
Color fillColor = Gizmos.color;
fillColor.a = 0.1f;
Gizmos.color = fillColor;
Gizmos.DrawCube(bounds.center, bounds.size);
Gizmos.matrix = oldMatrix;
}
}
}
public void CreatePSXTextures2D() public void CreatePSXTextures2D()
{ {
Renderer renderer = GetComponent<Renderer>(); Renderer renderer = GetComponent<Renderer>();
Textures.Clear(); Textures.Clear();
if (renderer != null) if (renderer == null) return;
Material[] materials = renderer.sharedMaterials;
foreach (Material mat in materials)
{ {
// If an override texture is set, use it for all submeshes if (mat == null || mat.mainTexture == null) continue;
if (texture != null)
Texture mainTexture = mat.mainTexture;
Texture2D tex2D = mainTexture is Texture2D existing
? existing
: ConvertToTexture2D(mainTexture);
if (tex2D == null) continue;
if (cache.TryGetValue((tex2D.GetInstanceID(), bitDepth), out var cached))
{ {
PSXTexture2D tex; Textures.Add(cached);
if (cache.ContainsKey((texture.GetInstanceID(), bitDepth)))
{
tex = cache[(texture.GetInstanceID(), bitDepth)];
}
else
{
tex = PSXTexture2D.CreateFromTexture2D(texture, bitDepth);
tex.OriginalTexture = texture;
cache.Add((texture.GetInstanceID(), bitDepth), tex);
}
Textures.Add(tex);
return;
} }
else
Material[] materials = renderer.sharedMaterials;
foreach (Material mat in materials)
{ {
if (mat != null && mat.mainTexture != null) var tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
{ tex.OriginalTexture = tex2D;
Texture mainTexture = mat.mainTexture; cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
Texture2D tex2D = null; Textures.Add(tex);
if (mainTexture is Texture2D existingTex2D)
{
tex2D = existingTex2D;
}
else
{
tex2D = ConvertToTexture2D(mainTexture);
}
if (tex2D != null)
{
PSXTexture2D tex;
if (cache.ContainsKey((tex2D.GetInstanceID(), bitDepth)))
{
tex = cache[(tex2D.GetInstanceID(), bitDepth)];
}
else
{
tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth);
tex.OriginalTexture = tex2D;
cache.Add((tex2D.GetInstanceID(), bitDepth), tex);
}
Textures.Add(tex);
}
}
} }
} }
} }
private Texture2D ConvertToTexture2D(Texture texture) private static Texture2D ConvertToTexture2D(Texture src)
{ {
Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false); Texture2D texture2D = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false);
RenderTexture currentActiveRT = RenderTexture.active; RenderTexture currentActiveRT = RenderTexture.active;
RenderTexture.active = texture as RenderTexture; RenderTexture.active = src as RenderTexture;
texture2D.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); texture2D.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0);
texture2D.Apply(); texture2D.Apply();
RenderTexture.active = currentActiveRT; RenderTexture.active = currentActiveRT;

View File

@@ -9,6 +9,11 @@ using UnityEngine;
namespace SplashEdit.RuntimeCode namespace SplashEdit.RuntimeCode
{ {
public enum PSXSceneType
{
Exterior = 0,
Interior = 1
}
[ExecuteInEditMode] [ExecuteInEditMode]
public class PSXSceneExporter : MonoBehaviour public class PSXSceneExporter : MonoBehaviour
@@ -35,7 +40,7 @@ namespace SplashEdit.RuntimeCode
[Header("Scene Type")] [Header("Scene Type")]
[Tooltip("Exterior uses BVH frustum culling. Interior uses room/portal occlusion.")] [Tooltip("Exterior uses BVH frustum culling. Interior uses room/portal occlusion.")]
public int SceneType = 0; // 0=exterior, 1=interior public PSXSceneType SceneType = PSXSceneType.Exterior;
[Header("Cutscenes")] [Header("Cutscenes")]
[Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")] [Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")]
@@ -166,7 +171,7 @@ namespace SplashEdit.RuntimeCode
// Collect them early so both systems use the same room indices. // Collect them early so both systems use the same room indices.
PSXRoom[] rooms = null; PSXRoom[] rooms = null;
PSXPortalLink[] portalLinks = null; PSXPortalLink[] portalLinks = null;
if (SceneType == 1) if (SceneType == PSXSceneType.Interior)
{ {
rooms = FindObjectsByType<PSXRoom>(FindObjectsSortMode.None); rooms = FindObjectsByType<PSXRoom>(FindObjectsSortMode.None);
portalLinks = FindObjectsByType<PSXPortalLink>(FindObjectsSortMode.None); portalLinks = FindObjectsByType<PSXPortalLink>(FindObjectsSortMode.None);
@@ -194,7 +199,7 @@ namespace SplashEdit.RuntimeCode
// Phase 5: Build room/portal system (for interior scenes) // Phase 5: Build room/portal system (for interior scenes)
_roomBuilder = new PSXRoomBuilder(); _roomBuilder = new PSXRoomBuilder();
if (SceneType == 1) if (SceneType == PSXSceneType.Interior)
{ {
if (rooms != null && rooms.Length > 0) if (rooms != null && rooms.Length > 0)
{ {

View File

@@ -50,7 +50,7 @@ namespace SplashEdit.RuntimeCode
public float gravity; public float gravity;
// Scene configuration (v11) // Scene configuration (v11)
public int sceneType; // 0=exterior, 1=interior public PSXSceneType sceneType;
public bool fogEnabled; public bool fogEnabled;
public Color fogColor; public Color fogColor;
public int fogDensity; // 1-10 public int fogDensity; // 1-10
@@ -111,7 +111,12 @@ namespace SplashEdit.RuntimeCode
int colliderCount = 0; int colliderCount = 0;
foreach (var e in scene.exporters) foreach (var e in scene.exporters)
{ {
if (e.CollisionType != PSXCollisionType.None) if (e.CollisionType == PSXCollisionType.None || e.StaticCollider)
continue;
Mesh cm = e.CustomCollisionMesh != null
? e.CustomCollisionMesh
: e.GetComponent<MeshFilter>()?.sharedMesh;
if (cm != null)
colliderCount++; colliderCount++;
} }
@@ -121,17 +126,17 @@ namespace SplashEdit.RuntimeCode
exporterIndex[scene.exporters[i]] = i; exporterIndex[scene.exporters[i]] = i;
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
// Header (72 bytes total — splashpack v8) // Header (104 bytes — splashpack v15)
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
writer.Write('S'); writer.Write('S');
writer.Write('P'); writer.Write('P');
writer.Write((ushort)13); // version writer.Write((ushort)15);
writer.Write((ushort)luaFiles.Count); writer.Write((ushort)luaFiles.Count);
writer.Write((ushort)scene.exporters.Length); writer.Write((ushort)scene.exporters.Length);
writer.Write((ushort)0); // navmeshCount (legacy)
writer.Write((ushort)scene.atlases.Length); writer.Write((ushort)scene.atlases.Length);
writer.Write((ushort)clutCount); writer.Write((ushort)clutCount);
writer.Write((ushort)colliderCount); writer.Write((ushort)colliderCount);
writer.Write((ushort)scene.interactables.Length);
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.x, gte)); writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.x, gte));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(-scene.playerPos.y, gte)); writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(-scene.playerPos.y, gte));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.z, gte)); writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerPos.z, gte));
@@ -142,37 +147,23 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerHeight, gte)); writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerHeight, gte));
// Scene Lua index
if (scene.sceneLuaFile != null) if (scene.sceneLuaFile != null)
writer.Write((short)luaFiles.IndexOf(scene.sceneLuaFile)); writer.Write((short)luaFiles.IndexOf(scene.sceneLuaFile));
else else
writer.Write((short)-1); writer.Write((short)-1);
// BVH info
writer.Write((ushort)scene.bvh.NodeCount); writer.Write((ushort)scene.bvh.NodeCount);
writer.Write((ushort)scene.bvh.TriangleRefCount); writer.Write((ushort)scene.bvh.TriangleRefCount);
// Component counts (version 4) writer.Write((ushort)scene.sceneType);
writer.Write((ushort)scene.interactables.Length); writer.Write((ushort)0); // pad0
writer.Write((ushort)0); // healthCount (removed)
writer.Write((ushort)0); // timerCount (removed)
writer.Write((ushort)0); // spawnerCount (removed)
// NavGrid (version 5, legacy)
writer.Write((ushort)0);
writer.Write((ushort)0);
// Scene type (version 6)
writer.Write((ushort)scene.sceneType); // 0=exterior, 1=interior
writer.Write((ushort)0);
// World collision + nav regions (version 7)
writer.Write((ushort)scene.collisionExporter.MeshCount); writer.Write((ushort)scene.collisionExporter.MeshCount);
writer.Write((ushort)scene.collisionExporter.TriangleCount); writer.Write((ushort)scene.collisionExporter.TriangleCount);
writer.Write((ushort)scene.navRegionBuilder.RegionCount); writer.Write((ushort)scene.navRegionBuilder.RegionCount);
writer.Write((ushort)scene.navRegionBuilder.PortalCount); writer.Write((ushort)scene.navRegionBuilder.PortalCount);
// Movement parameters (version 8, 12 bytes) // Movement parameters (12 bytes)
{ {
const float fps = 30f; const float fps = 30f;
float movePerFrame = scene.moveSpeed / fps / gte; float movePerFrame = scene.moveSpeed / fps / gte;
@@ -187,52 +178,49 @@ namespace SplashEdit.RuntimeCode
writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(gravPsx * 4096f), 0, 65535)); writer.Write((ushort)Mathf.Clamp(Mathf.RoundToInt(gravPsx * 4096f), 0, 65535));
writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerRadius, gte)); writer.Write((ushort)PSXTrig.ConvertCoordinateToPSX(scene.playerRadius, gte));
writer.Write((ushort)0); // padding writer.Write((ushort)0); // pad1
} }
// Name table offset placeholder (version 9, 4 bytes)
long nameTableOffsetPos = writer.BaseStream.Position; long nameTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // placeholder for name table offset writer.Write((uint)0);
// Audio clip info (version 10, 8 bytes)
int audioClipCount = scene.audioClips?.Length ?? 0; int audioClipCount = scene.audioClips?.Length ?? 0;
writer.Write((ushort)audioClipCount); writer.Write((ushort)audioClipCount);
writer.Write((ushort)0); // padding writer.Write((ushort)0); // pad2
long audioTableOffsetPos = writer.BaseStream.Position; long audioTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // placeholder for audio table offset writer.Write((uint)0);
// Fog + room/portal header (version 11, 12 bytes)
{ {
writer.Write((byte)(scene.fogEnabled ? 1 : 0)); writer.Write((byte)(scene.fogEnabled ? 1 : 0));
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.r * 255f), 0, 255)); writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.r * 255f), 0, 255));
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.g * 255f), 0, 255)); writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.g * 255f), 0, 255));
writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.b * 255f), 0, 255)); writer.Write((byte)Mathf.Clamp(Mathf.RoundToInt(scene.fogColor.b * 255f), 0, 255));
writer.Write((byte)Mathf.Clamp(scene.fogDensity, 1, 10)); writer.Write((byte)Mathf.Clamp(scene.fogDensity, 1, 10));
writer.Write((byte)0); // reserved writer.Write((byte)0); // pad3
int roomCount = scene.roomBuilder?.RoomCount ?? 0; int roomCount = scene.roomBuilder?.RoomCount ?? 0;
int portalCount = scene.roomBuilder?.PortalCount ?? 0; int portalCount = scene.roomBuilder?.PortalCount ?? 0;
int roomTriRefCount = scene.roomBuilder?.TotalTriRefCount ?? 0; int roomTriRefCount = scene.roomBuilder?.TotalTriRefCount ?? 0;
// roomCount is the room count NOT including catch-all; the binary adds +1 for it
writer.Write((ushort)(roomCount > 0 ? roomCount + 1 : 0)); writer.Write((ushort)(roomCount > 0 ? roomCount + 1 : 0));
writer.Write((ushort)portalCount); writer.Write((ushort)portalCount);
writer.Write((ushort)roomTriRefCount); writer.Write((ushort)roomTriRefCount);
} }
// Cutscene header (version 12, 8 bytes)
int cutsceneCount = scene.cutscenes?.Length ?? 0; int cutsceneCount = scene.cutscenes?.Length ?? 0;
writer.Write((ushort)cutsceneCount); writer.Write((ushort)cutsceneCount);
writer.Write((ushort)0); // reserved_cs writer.Write((ushort)0); // pad4
long cutsceneTableOffsetPos = writer.BaseStream.Position; long cutsceneTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // cutsceneTableOffset placeholder writer.Write((uint)0); // cutsceneTableOffset placeholder
// UI canvas header (version 13, 8 bytes)
int uiCanvasCount = scene.canvases?.Length ?? 0; int uiCanvasCount = scene.canvases?.Length ?? 0;
int uiFontCount = scene.fonts?.Length ?? 0; int uiFontCount = scene.fonts?.Length ?? 0;
writer.Write((ushort)uiCanvasCount); writer.Write((ushort)uiCanvasCount);
writer.Write((byte)uiFontCount); // was uiReserved low byte writer.Write((byte)uiFontCount);
writer.Write((byte)0); // was uiReserved high byte writer.Write((byte)0); // uiPad5
long uiTableOffsetPos = writer.BaseStream.Position; long uiTableOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // uiTableOffset placeholder writer.Write((uint)0);
long pixelDataOffsetPos = writer.BaseStream.Position;
writer.Write((uint)0); // pixelDataOffset placeholder
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
// Lua file metadata // Lua file metadata
@@ -295,7 +283,7 @@ namespace SplashEdit.RuntimeCode
for (int exporterIdx = 0; exporterIdx < scene.exporters.Length; exporterIdx++) for (int exporterIdx = 0; exporterIdx < scene.exporters.Length; exporterIdx++)
{ {
PSXObjectExporter exporter = scene.exporters[exporterIdx]; PSXObjectExporter exporter = scene.exporters[exporterIdx];
if (exporter.CollisionType == PSXCollisionType.None) if (exporter.CollisionType == PSXCollisionType.None || exporter.StaticCollider)
continue; continue;
MeshFilter meshFilter = exporter.GetComponent<MeshFilter>(); MeshFilter meshFilter = exporter.GetComponent<MeshFilter>();
@@ -483,33 +471,6 @@ namespace SplashEdit.RuntimeCode
} }
} }
// Atlas pixel data
foreach (TextureAtlas atlas in scene.atlases)
{
AlignToFourBytes(writer);
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
writer.Write(atlas.vramPixels[x, y].Pack());
}
// CLUT data
foreach (TextureAtlas atlas in scene.atlases)
{
foreach (var texture in atlas.ContainedTextures)
{
if (texture.ColorPalette != null)
{
AlignToFourBytes(writer);
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
foreach (VRAMPixel color in texture.ColorPalette)
writer.Write((ushort)color.Pack());
}
}
}
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
// Object name table (version 9) // Object name table (version 9)
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
@@ -535,46 +496,50 @@ namespace SplashEdit.RuntimeCode
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
// Audio clip data (version 10) // Audio clip data (version 10)
// Metadata entries are 16 bytes each, written contiguously.
// Name strings follow the metadata block with backfilled offsets.
// ADPCM blobs deferred to dead zone.
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
List<long> audioDataOffsetPositions = new List<long>();
if (audioClipCount > 0 && scene.audioClips != null) if (audioClipCount > 0 && scene.audioClips != null)
{ {
// Write audio table: per clip metadata (12 bytes each)
AlignToFourBytes(writer); AlignToFourBytes(writer);
long audioTableStart = writer.BaseStream.Position; long audioTableStart = writer.BaseStream.Position;
// First pass: write metadata placeholders (16 bytes each)
List<long> audioDataOffsetPositions = new List<long>();
List<long> audioNameOffsetPositions = new List<long>(); List<long> audioNameOffsetPositions = new List<long>();
List<string> audioClipNames = new List<string>();
// Phase 1: Write all 16-byte metadata entries contiguously
for (int i = 0; i < audioClipCount; i++) for (int i = 0; i < audioClipCount; i++)
{ {
var clip = scene.audioClips[i]; var clip = scene.audioClips[i];
audioDataOffsetPositions.Add(writer.BaseStream.Position);
writer.Write((uint)0); // dataOffset placeholder
writer.Write((uint)(clip.adpcmData?.Length ?? 0)); // sizeBytes
writer.Write((ushort)clip.sampleRate);
string name = clip.clipName ?? ""; string name = clip.clipName ?? "";
if (name.Length > 255) name = name.Substring(0, 255);
audioDataOffsetPositions.Add(writer.BaseStream.Position);
writer.Write((uint)0); // dataOffset placeholder (backfilled in dead zone)
writer.Write((uint)(clip.adpcmData?.Length ?? 0));
writer.Write((ushort)clip.sampleRate);
writer.Write((byte)(clip.loop ? 1 : 0)); writer.Write((byte)(clip.loop ? 1 : 0));
writer.Write((byte)System.Math.Min(name.Length, 255)); writer.Write((byte)name.Length);
audioNameOffsetPositions.Add(writer.BaseStream.Position); audioNameOffsetPositions.Add(writer.BaseStream.Position);
writer.Write((uint)0); // nameOffset placeholder writer.Write((uint)0); // nameOffset placeholder
audioClipNames.Add(name);
} }
// Second pass: write ADPCM data and backfill offsets // Phase 2: Write name strings (after all metadata entries)
for (int i = 0; i < audioClipCount; i++) for (int i = 0; i < audioClipCount; i++)
{ {
byte[] data = scene.audioClips[i].adpcmData; string name = audioClipNames[i];
if (data != null && data.Length > 0) long namePos = writer.BaseStream.Position;
{ byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
AlignToFourBytes(writer); writer.Write(nameBytes);
long dataPos = writer.BaseStream.Position; writer.Write((byte)0);
writer.Write(data);
// Backfill data offset long curPos = writer.BaseStream.Position;
long curPos = writer.BaseStream.Position; writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin); writer.Write((uint)namePos);
writer.Write((uint)dataPos); writer.Seek((int)curPos, SeekOrigin.Begin);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
} }
// Backfill audio table offset in header // Backfill audio table offset in header
@@ -584,28 +549,6 @@ namespace SplashEdit.RuntimeCode
writer.Write((uint)audioTableStart); writer.Write((uint)audioTableStart);
writer.Seek((int)curPos, SeekOrigin.Begin); writer.Seek((int)curPos, SeekOrigin.Begin);
} }
int totalAudioBytes = 0;
foreach (var clip in scene.audioClips)
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
// Third pass: write audio clip names and backfill name offsets
for (int i = 0; i < audioClipCount; i++)
{
string name = scene.audioClips[i].clipName ?? "";
if (name.Length > 255) name = name.Substring(0, 255);
long namePos = writer.BaseStream.Position;
byte[] nameBytes = System.Text.Encoding.ASCII.GetBytes(name);
writer.Write(nameBytes);
writer.Write((byte)0); // null terminator
long curPos = writer.BaseStream.Position;
writer.Seek((int)audioNameOffsetPositions[i], SeekOrigin.Begin);
writer.Write((uint)namePos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
} }
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
@@ -635,11 +578,12 @@ namespace SplashEdit.RuntimeCode
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
// UI canvas + font data (version 13) // UI canvas + font data (version 13)
// Font descriptors: 112 bytes each (before canvas data) // Font descriptors: 112 bytes each (before canvas data)
// Font pixel data: raw 4bpp (after font descriptors)
// Canvas descriptor table: 12 bytes per canvas // Canvas descriptor table: 12 bytes per canvas
// Element records: 48 bytes each // Element records: 48 bytes each
// Name and text strings follow with offset backfill // Name and text strings follow with offset backfill
// Font pixel data is deferred to the dead zone.
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
List<long> fontDataOffsetPositions = new List<long>();
if ((uiCanvasCount > 0 && scene.canvases != null) || uiFontCount > 0) if ((uiCanvasCount > 0 && scene.canvases != null) || uiFontCount > 0)
{ {
AlignToFourBytes(writer); AlignToFourBytes(writer);
@@ -648,7 +592,6 @@ namespace SplashEdit.RuntimeCode
// ── Font descriptors (112 bytes each) ── // ── Font descriptors (112 bytes each) ──
// Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2) // Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2)
// dataOffset(4) dataSize(4) // dataOffset(4) dataSize(4)
List<long> fontDataOffsetPositions = new List<long>();
if (scene.fonts != null) if (scene.fonts != null)
{ {
foreach (var font in scene.fonts) foreach (var font in scene.fonts)
@@ -669,32 +612,9 @@ namespace SplashEdit.RuntimeCode
} }
} }
// ── Font pixel data (raw 4bpp) ── // Font pixel data is deferred to the dead zone (after pixelDataOffset).
if (scene.fonts != null) // The C++ loader reads font pixel data via the dataOffset, uploads to VRAM,
{ // then never accesses it again.
for (int fi = 0; fi < scene.fonts.Length; fi++)
{
var font = scene.fonts[fi];
if (font.PixelData == null || font.PixelData.Length == 0) continue;
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(font.PixelData);
// Backfill data offset
long curPos = writer.BaseStream.Position;
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
if (scene.fonts.Length > 0)
{
int totalFontBytes = 0;
foreach (var f in scene.fonts) totalFontBytes += f.PixelData?.Length ?? 0;
log?.Invoke($"{scene.fonts.Length} custom font(s) written ({totalFontBytes} bytes 4bpp data).", LogType.Log);
}
}
// ── Canvas descriptor table (12 bytes each) ── // ── Canvas descriptor table (12 bytes each) ──
// Layout per descriptor: // Layout per descriptor:
@@ -873,6 +793,96 @@ namespace SplashEdit.RuntimeCode
log?.Invoke($"{uiCanvasCount} UI canvases ({totalElements} elements) written.", LogType.Log); log?.Invoke($"{uiCanvasCount} UI canvases ({totalElements} elements) written.", LogType.Log);
} }
// ══════════════════════════════════════════════════════
// DEAD ZONE — pixel/audio bulk data (freed after VRAM/SPU upload)
// Everything written after this point is not needed at runtime.
// ══════════════════════════════════════════════════════
AlignToFourBytes(writer);
long pixelDataStart = writer.BaseStream.Position;
// Atlas pixel data
foreach (TextureAtlas atlas in scene.atlases)
{
AlignToFourBytes(writer);
atlasOffset.DataOffsets.Add(writer.BaseStream.Position);
for (int y = 0; y < atlas.vramPixels.GetLength(1); y++)
for (int x = 0; x < atlas.vramPixels.GetLength(0); x++)
writer.Write(atlas.vramPixels[x, y].Pack());
}
// CLUT data
foreach (TextureAtlas atlas in scene.atlases)
{
foreach (var texture in atlas.ContainedTextures)
{
if (texture.ColorPalette != null)
{
AlignToFourBytes(writer);
clutOffset.DataOffsets.Add(writer.BaseStream.Position);
foreach (VRAMPixel color in texture.ColorPalette)
writer.Write((ushort)color.Pack());
}
}
}
// Audio ADPCM data
if (audioClipCount > 0 && scene.audioClips != null)
{
for (int i = 0; i < audioClipCount; i++)
{
byte[] data = scene.audioClips[i].adpcmData;
if (data != null && data.Length > 0)
{
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(data);
long curPos = writer.BaseStream.Position;
writer.Seek((int)audioDataOffsetPositions[i], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
int totalAudioBytes = 0;
foreach (var clip in scene.audioClips)
if (clip.adpcmData != null) totalAudioBytes += clip.adpcmData.Length;
log?.Invoke($"{audioClipCount} audio clips ({totalAudioBytes / 1024}KB ADPCM) written.", LogType.Log);
}
// Font pixel data
if (scene.fonts != null)
{
for (int fi = 0; fi < scene.fonts.Length; fi++)
{
var font = scene.fonts[fi];
if (font.PixelData == null || font.PixelData.Length == 0) continue;
AlignToFourBytes(writer);
long dataPos = writer.BaseStream.Position;
writer.Write(font.PixelData);
long curPos = writer.BaseStream.Position;
writer.Seek((int)fontDataOffsetPositions[fi], SeekOrigin.Begin);
writer.Write((uint)dataPos);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
}
// Backfill pixelDataOffset in header
{
long curPos = writer.BaseStream.Position;
writer.Seek((int)pixelDataOffsetPos, SeekOrigin.Begin);
writer.Write((uint)pixelDataStart);
writer.Seek((int)curPos, SeekOrigin.Begin);
}
long totalSize = writer.BaseStream.Position;
long deadBytes = totalSize - pixelDataStart;
log?.Invoke($"Pixel/audio dead zone: {deadBytes / 1024}KB (freed after VRAM/SPU upload).", LogType.Log);
// Backfill offsets // Backfill offsets
BackfillOffsets(writer, luaOffset, "lua", log); BackfillOffsets(writer, luaOffset, "lua", log);
BackfillOffsets(writer, meshOffset, "mesh", log); BackfillOffsets(writer, meshOffset, "mesh", log);