Compare commits

32 Commits
main ... lua

Author SHA1 Message Date
Jan Racek
01b636f3e2 ITS BROKEN 2026-03-29 16:14:30 +02:00
Jan Racek
61fbca17a7 Lua bytecode builder, cdrom fixes 2026-03-28 19:41:53 +01:00
275b4e891d Fixed UI ordering 2026-03-28 14:35:35 +01:00
132ab479c2 Revamped psxsplash installer 2026-03-28 13:51:55 +01:00
eff03e0e1a Back color configurable, added fps counter checkbox 2026-03-28 13:31:41 +01:00
5e862f8c0b redux launch interpreted 2026-03-28 11:58:28 +01:00
62bf7d8b2d Fixed linux dependency donwloads, fixed slow download speeds 2026-03-28 10:22:10 +01:00
Jan Racek
a251eeaed5 more fixes 2026-03-28 01:32:36 +01:00
Jan Racek
13ed569eaf cleanup 2026-03-27 21:29:24 +01:00
Jan Racek
45a552be5a memory reports 2026-03-27 19:29:41 +01:00
Jan Racek
24d0c1fa07 bugfixes 2026-03-27 18:31:35 +01:00
Jan Racek
1c48b8b425 Revamped collision system 2026-03-27 16:39:42 +01:00
Jan Racek
d29ef569b3 Broken RUntime 2026-03-27 13:47:18 +01:00
Jan Racek
6bf74fa929 Fixed ui textures 2026-03-26 20:27:23 +01:00
Jan Racek
d5be174247 Broken UI and Loading screens 2026-03-26 19:14:15 +01:00
Jan Racek
a8aa674a9c Fixed up textures in UI 2026-03-26 17:27:10 +01:00
Jan Racek
5fffcea6cf Somewhat fixed ui 2026-03-25 17:14:22 +01:00
Jan Racek
8914ba35cc Broken UI system 2026-03-25 12:25:48 +01:00
Jan Racek
bb8e0804f5 Cutscene sytstem 2026-03-24 15:50:35 +01:00
Jan Racek
4aa4e49424 psst 2026-03-24 13:00:54 +01:00
53e993f58e feature: Added psxsplash installer, added basic BSP implementation (non-functional) 2025-09-04 18:01:23 +02:00
0d1e363dbb Fixed texture deduping 2025-08-22 22:19:21 +02:00
ac0e4d8420 Added IsActive flag and scene lua files 2025-04-17 15:36:34 +02:00
9af5d7dd1a I hate lfs 2025-04-15 21:19:40 +02:00
dc9bfcb155 Fixed toolchain install on windows 2025-04-15 14:18:58 +02:00
849e221b32 Fixing lfs, hopefully 2025-04-15 13:32:24 +02:00
2013e31b04 Added automatic toolchain, gcc and make installation 2025-04-15 13:17:30 +02:00
Bandwidth
4cebe93c34 Merge pull request #13 from aliakseikalosha/lua
Update lua asset hadling
2025-04-12 21:13:31 +02:00
aliaksei.kalosha
551eb4c0de remove code repetition
2
2025-04-12 19:21:52 +02:00
aliaksei.kalosha
ecb1422937 update handling of Lua assets 2025-04-12 18:16:57 +02:00
a07a715d19 merge main into lua 2025-04-12 14:55:42 +02:00
b3da188438 Added lua exporting 2025-04-09 22:20:52 +02:00
203 changed files with 19100 additions and 1091 deletions

7
.gitattributes vendored
View File

@@ -1,6 +1,7 @@
#
# Auto Generated
#
# Other
*.pdf filter=lfs diff=lfs merge=lfs -text
# Unity Binary Assets
@@ -25,7 +26,6 @@
*.hdr filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.apng filter=lfs diff=lfs merge=lfs -text
*.atsc filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
@@ -69,15 +69,18 @@
*.wmv filter=lfs diff=lfs merge=lfs -text
# Unity-Specific
* text=auto
# Some assets such as lighting data, nav meshes, etc will always be binary,
# as will anything with [PreferBinarySerialization]
# (Even if you force it to text mode serialization)
# Meaning autoCRLF on git will fuck up your project.
*.asset auto
TimeManager.asset -text
#
# The following should be text to allow git merges
#
*.anim text
*.controller text
*.mat text
@@ -88,3 +91,5 @@ TimeManager.asset -text
*.unity text
*.preset text
*.lfs_test filter=lfs diff=lfs merge=lfs -text

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 311ff9868024b5078bd12a6c2252a4ef
guid: d7e9b1c3e60e2ff48be3cd61902ba6f1
folderAsset: yes
DefaultImporter:
externalObjects: {}

BIN
Data/SPLASHLICENSE.DAT Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 871e1f08910e9f329ad3fce5d77c8785
guid: 244f6913a02805e4aa3cebdd1240cab7
DefaultImporter:
externalObjects: {}
userData:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d64fb2a2412d7958ca13d15956c4182b
guid: f7b9a2e33a4c4754997cf0dd0f20acc8
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: cf7149f85c29d3f4d9a7f968d8825ffa
guid: 8e74ebc4b575d27499f7abd4d82b8849
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,197 @@
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, "bin", "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 client = new System.Net.WebClient())
{
client.Headers.Add("User-Agent", "SplashEdit/1.0");
client.DownloadProgressChanged += (s, e) =>
{
float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading mkpsxiso", $"Downloading... {sizeMB}", progress);
};
await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
}
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)
SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
try { File.Delete(tempFile); } catch { }
EditorUtility.ClearProgressBar();
if (IsInstalled())
{
// Make executable on Linux
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;
}
}
/// <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

@@ -0,0 +1,241 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Downloads and installs PCSX-Redux from the official distrib.app CDN.
/// Mirrors the logic from pcsx-redux.js (the official download script).
///
/// Flow: fetch platform manifest → find latest build ID → fetch build manifest →
/// get download URL → download zip → extract to .tools/pcsx-redux/
/// </summary>
public static class PCSXReduxDownloader
{
private const string MANIFEST_BASE = "https://distrib.app/storage/manifests/pcsx-redux/";
private static readonly HttpClient _http;
static PCSXReduxDownloader()
{
var handler = new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip
| System.Net.DecompressionMethods.Deflate
};
_http = new HttpClient(handler);
_http.Timeout = TimeSpan.FromSeconds(60);
_http.DefaultRequestHeaders.UserAgent.ParseAdd("SplashEdit/1.0");
}
/// <summary>
/// Returns the platform variant string for the current platform.
/// </summary>
private static string GetPlatformVariant()
{
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
return "dev-win-cli-x64";
case RuntimePlatform.LinuxEditor:
return "dev-linux-x64";
default:
return "dev-win-cli-x64";
}
}
/// <summary>
/// Downloads and installs PCSX-Redux to .tools/pcsx-redux/.
/// Shows progress bar during download.
/// </summary>
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
{
string variant = GetPlatformVariant();
log?.Invoke($"Platform variant: {variant}");
try
{
// Step 1: Fetch the master manifest to get the latest build ID
string manifestUrl = $"{MANIFEST_BASE}{variant}/manifest.json";
log?.Invoke($"Fetching manifest: {manifestUrl}");
string manifestJson = await _http.GetStringAsync(manifestUrl);
// Parse the latest build ID from the manifest.
// The manifest is JSON with a "builds" array. We want the highest ID.
// Simple JSON parsing without dependencies:
int latestBuildId = ParseLatestBuildId(manifestJson);
if (latestBuildId < 0)
{
log?.Invoke("Failed to parse build ID from manifest.");
return false;
}
log?.Invoke($"Latest build ID: {latestBuildId}");
// Step 2: Fetch the specific build manifest
string buildManifestUrl = $"{MANIFEST_BASE}{variant}/manifest-{latestBuildId}.json";
log?.Invoke($"Fetching build manifest...");
string buildManifestJson = await _http.GetStringAsync(buildManifestUrl);
// Parse the download path
string downloadPath = ParseDownloadPath(buildManifestJson);
if (string.IsNullOrEmpty(downloadPath))
{
log?.Invoke("Failed to parse download path from build manifest.");
return false;
}
string downloadUrl = $"https://distrib.app{downloadPath}";
log?.Invoke($"Downloading: {downloadUrl}");
// Step 3: Download the file
string tempFile = Path.Combine(Path.GetTempPath(), $"pcsx-redux-{latestBuildId}.zip");
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", "Downloading...", 0.1f);
using (var client = new System.Net.WebClient())
{
client.Headers.Add("User-Agent", "SplashEdit/1.0");
client.DownloadProgressChanged += (s, e) =>
{
float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading PCSX-Redux", $"Downloading... {sizeMB}", progress);
};
await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
}
log?.Invoke($"Downloaded to {tempFile}");
EditorUtility.DisplayProgressBar("Installing PCSX-Redux", "Extracting...", 0.9f);
// Step 4: Extract
string installDir = SplashBuildPaths.PCSXReduxDir;
if (Directory.Exists(installDir))
Directory.Delete(installDir, true);
Directory.CreateDirectory(installDir);
if (Application.platform == RuntimePlatform.LinuxEditor && tempFile.EndsWith(".tar.gz"))
{
var psi = new ProcessStartInfo
{
FileName = "tar",
Arguments = $"xzf \"{tempFile}\" -C \"{installDir}\" --strip-components=1",
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(psi);
proc?.WaitForExit();
}
else
{
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
log?.Invoke($"Extracted to {installDir}");
}
// Make executable
if(Application.platform == RuntimePlatform.LinuxEditor) {
var psi = new ProcessStartInfo
{
FileName = "chmod",
Arguments = $"+x \"{SplashBuildPaths.PCSXReduxBinary}\"",
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(psi);
proc?.WaitForExit();
}
// Clean up temp file
try { File.Delete(tempFile); } catch { }
// Step 5: Verify
if (SplashBuildPaths.IsPCSXReduxInstalled())
{
log?.Invoke("PCSX-Redux installed successfully!");
EditorUtility.ClearProgressBar();
return true;
}
else
{
// The zip might have a nested directory — try to find the exe
SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
if (SplashBuildPaths.IsPCSXReduxInstalled())
{
log?.Invoke("PCSX-Redux installed successfully!");
EditorUtility.ClearProgressBar();
return true;
}
log?.Invoke("Installation completed but PCSX-Redux binary not found at expected path.");
log?.Invoke($"Expected: {SplashBuildPaths.PCSXReduxBinary}");
log?.Invoke($"Check: {installDir}");
EditorUtility.ClearProgressBar();
return false;
}
}
catch (Exception ex)
{
log?.Invoke($"Download failed: {ex.Message}");
EditorUtility.ClearProgressBar();
return false;
}
}
/// <summary>
/// Parse the latest build ID from the master manifest JSON.
/// Expected format: {"builds":[{"id":1234,...},...],...}
/// distrib.app returns builds sorted newest-first, so we take the first.
/// Falls back to scanning all IDs if the "builds" section isn't found.
/// </summary>
private static int ParseLatestBuildId(string json)
{
// Fast path: find the first "id" inside "builds" array
int buildsIdx = json.IndexOf("\"builds\"", StringComparison.Ordinal);
int startPos = buildsIdx >= 0 ? buildsIdx : 0;
string searchToken = "\"id\":";
int idx = json.IndexOf(searchToken, startPos, StringComparison.Ordinal);
if (idx < 0) return -1;
int pos = idx + searchToken.Length;
while (pos < json.Length && char.IsWhiteSpace(json[pos])) pos++;
int numStart = pos;
while (pos < json.Length && char.IsDigit(json[pos])) pos++;
if (pos > numStart && int.TryParse(json.Substring(numStart, pos - numStart), out int id))
return id;
return -1;
}
/// <summary>
/// Parse the download path from a build-specific manifest.
/// Expected format: {...,"path":"/storage/builds/..."}
/// </summary>
private static string ParseDownloadPath(string json)
{
string searchToken = "\"path\":";
int idx = json.IndexOf(searchToken, StringComparison.Ordinal);
if (idx < 0) return null;
int pos = idx + searchToken.Length;
while (pos < json.Length && char.IsWhiteSpace(json[pos])) pos++;
if (pos >= json.Length || json[pos] != '"') return null;
pos++; // skip opening quote
int pathStart = pos;
while (pos < json.Length && json[pos] != '"') pos++;
return json.Substring(pathStart, pos - pathStart);
}
}
}

View File

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

View File

@@ -0,0 +1,780 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// PCdrv Host — serves files to a PS1 over serial (Unirom/NOTPSXSerial protocol).
///
/// PCdrv uses MIPS `break` instructions to request file I/O from the host.
/// For real hardware running Unirom, we must:
/// 1. Enter debug mode (DEBG/OKAY) — installs Unirom's kernel-resident SIO handler
/// 2. Continue execution (CONT/OKAY)
/// 3. Monitor serial for escape sequences: 0x00 followed by 'p' = PCDrv command
/// 4. Handle file operations (init, open, read, close, seek, etc.)
///
/// Without entering debug mode first, `break` instructions cause an unhandled
/// "BP break (0x9)" crash because no handler is registered.
///
/// Protocol based on NOTPSXSerial: https://github.com/JonathanDotCel/NOTPSXSerial
/// </summary>
public class PCdrvSerialHost : IDisposable
{
private SerialPort _port;
private CancellationTokenSource _cts;
private Task _listenTask;
private readonly string _portName;
private readonly int _baudRate;
private readonly string _baseDir;
private readonly Action<string> _log;
private readonly Action<string> _psxLog;
// File handle table (1-indexed, handles are not recycled)
private readonly List<PCFile> _files = new List<PCFile>();
private class PCFile
{
public string Name;
public FileStream Stream;
public int Handle;
public bool Closed;
public FileAccess Mode;
}
// Protocol escape char — PCDrv commands are prefixed with 0x00 + 'p'
private const byte ESCAPE_CHAR = 0x00;
// PCDrv function codes (from Unirom kernel)
private const int FUNC_INIT = 0x101;
private const int FUNC_CREAT = 0x102;
private const int FUNC_OPEN = 0x103;
private const int FUNC_CLOSE = 0x104;
private const int FUNC_READ = 0x105;
private const int FUNC_WRITE = 0x106;
private const int FUNC_SEEK = 0x107;
public bool IsRunning => _listenTask != null && !_listenTask.IsCompleted;
public bool HasError { get; private set; }
public PCdrvSerialHost(string portName, int baudRate, string baseDir, Action<string> log, Action<string> psxLog = null)
{
_portName = portName;
_baudRate = baudRate;
_baseDir = baseDir;
_log = log;
_psxLog = psxLog;
}
/// <summary>
/// Opens a new serial port and begins the monitor/PCdrv loop.
/// Note: DEBG must have been sent BEFORE the exe was uploaded (via UniromUploader.UploadExeForPCdrv).
/// Use the Start(SerialPort) overload to pass an already-open port from the uploader.
/// </summary>
public void Start()
{
if (IsRunning) return;
_port = new SerialPort(_portName, _baudRate)
{
ReadTimeout = 5000,
WriteTimeout = 5000,
StopBits = StopBits.Two,
Parity = Parity.None,
DataBits = 8,
Handshake = Handshake.None,
DtrEnable = true,
RtsEnable = true
};
_port.Open();
_log?.Invoke($"PCdrv host: opened {_portName} @ {_baudRate}");
_log?.Invoke($"PCdrv host: serving files from {_baseDir}");
StartMonitorLoop();
}
/// <summary>
/// Starts the PCDrv monitor loop on an already-open serial port.
/// Use this after UniromUploader.UploadExeForPCdrv() which sends DEBG → SEXE
/// and returns the open port. The debug hooks are already installed and the
/// exe is already running — we just need to listen for escape sequences.
/// </summary>
public void Start(SerialPort openPort)
{
if (IsRunning) return;
_port = openPort;
_log?.Invoke($"PCdrv host: serving files from {_baseDir}");
_log?.Invoke("PCdrv host: monitoring for PCDrv requests...");
StartMonitorLoop();
}
private void StartMonitorLoop()
{
_cts = new CancellationTokenSource();
_listenTask = Task.Run(() => MonitorLoop(_cts.Token));
}
public void Stop()
{
_cts?.Cancel();
try { _listenTask?.Wait(2000); } catch { }
foreach (var f in _files)
{
if (!f.Closed && f.Stream != null)
{
try { f.Stream.Close(); f.Stream.Dispose(); } catch { }
}
}
_files.Clear();
if (_port != null && _port.IsOpen)
{
try { _port.Close(); } catch { }
}
_port?.Dispose();
_port = null;
_log?.Invoke("PCdrv host stopped.");
}
public void Dispose() => Stop();
// ═══════════════════════════════════════════════════════════════
// Monitor loop — reads serial byte-by-byte looking for escape sequences
// Matches NOTPSXSerial's Bridge.MonitorSerial()
// ═══════════════════════════════════════════════════════════════
private void MonitorLoop(CancellationToken ct)
{
bool lastByteWasEscape = false;
var textBuffer = new StringBuilder();
int totalBytesReceived = 0;
int consecutiveErrors = 0;
DateTime lastLogTime = DateTime.Now;
_log?.Invoke("PCdrv monitor: waiting for data from PS1...");
while (!ct.IsCancellationRequested)
{
try
{
if (_port.BytesToRead == 0)
{
// Flush any accumulated text output periodically
if (textBuffer.Length > 0 && (DateTime.Now - lastLogTime).TotalMilliseconds > 100)
{
EmitPsxLine(textBuffer.ToString());
textBuffer.Clear();
lastLogTime = DateTime.Now;
}
Thread.Sleep(1);
continue;
}
int b = _port.ReadByte();
consecutiveErrors = 0;
totalBytesReceived++;
// Log first bytes received to help diagnose protocol issues
if (totalBytesReceived <= 32)
{
_log?.Invoke($"PCdrv monitor: byte #{totalBytesReceived} = 0x{b:X2} ('{(b >= 0x20 && b < 0x7F ? (char)b : '.')}')");
}
else if (totalBytesReceived == 33)
{
_log?.Invoke("PCdrv monitor: (suppressing per-byte logging, check PS1> lines for output)");
}
if (lastByteWasEscape)
{
lastByteWasEscape = false;
// Flush any text before handling escape
if (textBuffer.Length > 0)
{
EmitPsxLine(textBuffer.ToString());
textBuffer.Clear();
}
if (b == ESCAPE_CHAR)
{
// Double escape = literal 0x00 in output, ignore
continue;
}
if (b == 'p')
{
// PCDrv command incoming
_log?.Invoke("PCdrv monitor: got escape+p → PCDrv command!");
HandlePCDrvCommand(ct);
}
else
{
// Unknown escape sequence — log it
_log?.Invoke($"PCdrv monitor: unknown escape seq: 0x00 + 0x{b:X2} ('{(b >= 0x20 && b < 0x7F ? (char)b : '.')}')");
}
continue;
}
if (b == ESCAPE_CHAR)
{
lastByteWasEscape = true;
continue;
}
// Regular byte — this is printf output from the PS1
if (b == '\n' || b == '\r')
{
if (textBuffer.Length > 0)
{
EmitPsxLine(textBuffer.ToString());
textBuffer.Clear();
lastLogTime = DateTime.Now;
}
}
else if (b >= 0x20 && b < 0x7F)
{
textBuffer.Append((char)b);
// Flush long lines immediately
if (textBuffer.Length >= 200)
{
EmitPsxLine(textBuffer.ToString());
textBuffer.Clear();
lastLogTime = DateTime.Now;
}
}
// else: non-printable byte that's not escape, skip
}
catch (TimeoutException) { }
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
if (ct.IsCancellationRequested) break;
consecutiveErrors++;
_log?.Invoke($"PCdrv monitor error: {ex.Message}");
if (consecutiveErrors >= 3)
{
_log?.Invoke("PCdrv host: too many errors, connection lost. Stopping.");
HasError = true;
break;
}
Thread.Sleep(100); // Back off before retry
}
}
}
// ═══════════════════════════════════════════════════════════════
// PCDrv command dispatcher
// Matches NOTPSXSerial's PCDrv.ReadCommand()
// ═══════════════════════════════════════════════════════════════
private void HandlePCDrvCommand(CancellationToken ct)
{
int funcCode = ReadInt32(ct);
switch (funcCode)
{
case FUNC_INIT: HandleInit(); break;
case FUNC_CREAT: HandleCreate(ct); break;
case FUNC_OPEN: HandleOpen(ct); break;
case FUNC_CLOSE: HandleClose(ct); break;
case FUNC_READ: HandleRead(ct); break;
case FUNC_WRITE: HandleWrite(ct); break;
case FUNC_SEEK: HandleSeek(ct); break;
default:
_log?.Invoke($"PCdrv: unknown function 0x{funcCode:X}");
break;
}
}
// ═══════════════════════════════════════════════════════════════
// Individual PCDrv handlers — match NOTPSXSerial's PCDrv.cs
// ═══════════════════════════════════════════════════════════════
private void HandleInit()
{
_log?.Invoke("PCdrv: INIT");
SendString("OKAY");
_port.Write(new byte[] { 0 }, 0, 1); // null terminator expected by Unirom
}
private void HandleOpen(CancellationToken ct)
{
// Unirom sends: we respond OKAY first, then read filename + mode
SendString("OKAY");
string filename = ReadNullTermString(ct);
int modeParam = ReadInt32(ct);
// Log raw bytes for debugging garbled filenames
_log?.Invoke($"PCdrv: OPEN \"{filename}\" mode={modeParam} (len={filename.Length}, hex={BitConverter.ToString(System.Text.Encoding.ASCII.GetBytes(filename))})");
// Check if already open
var existing = FindOpenFile(filename);
if (existing != null)
{
_log?.Invoke($"PCdrv: already open, handle={existing.Handle}");
SendString("OKAY");
WriteInt32(existing.Handle);
return;
}
string fullPath;
try
{
fullPath = ResolvePath(filename);
}
catch (Exception ex)
{
_log?.Invoke($"PCdrv: invalid filename \"{filename}\": {ex.Message}");
SendString("NOPE");
return;
}
if (!File.Exists(fullPath))
{
_log?.Invoke($"PCdrv: file not found: {fullPath}");
SendString("NOPE");
return;
}
try
{
var fs = new FileStream(fullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
int handle = NextHandle();
_files.Add(new PCFile { Name = filename, Stream = fs, Handle = handle, Closed = false, Mode = FileAccess.ReadWrite });
SendString("OKAY");
WriteInt32(handle);
_log?.Invoke($"PCdrv: opened handle={handle}");
}
catch (Exception ex)
{
_log?.Invoke($"PCdrv: open failed: {ex.Message}");
SendString("NOPE");
}
}
private void HandleCreate(CancellationToken ct)
{
SendString("OKAY");
string filename = ReadNullTermString(ct);
int parameters = ReadInt32(ct);
_log?.Invoke($"PCdrv: CREAT \"{filename}\" params={parameters}");
var existing = FindOpenFile(filename);
if (existing != null)
{
SendString("OKAY");
WriteInt32(existing.Handle);
return;
}
string fullPath;
try { fullPath = ResolvePath(filename); }
catch (Exception ex)
{
_log?.Invoke($"PCdrv: invalid filename \"{filename}\": {ex.Message}");
SendString("NOPE");
return;
}
try
{
// Create or truncate the file
if (!File.Exists(fullPath))
{
var temp = File.Create(fullPath);
temp.Flush(); temp.Close(); temp.Dispose();
}
var fs = new FileStream(fullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
int handle = NextHandle();
_files.Add(new PCFile { Name = filename, Stream = fs, Handle = handle, Closed = false, Mode = FileAccess.ReadWrite });
SendString("OKAY");
WriteInt32(handle);
_log?.Invoke($"PCdrv: created handle={handle}");
}
catch (Exception ex)
{
_log?.Invoke($"PCdrv: create failed: {ex.Message}");
SendString("NOPE");
}
}
private void HandleClose(CancellationToken ct)
{
// Unirom sends: we respond OKAY first, then read handle + 2 unused params
SendString("OKAY");
int handle = ReadInt32(ct);
int _unused1 = ReadInt32(ct);
int _unused2 = ReadInt32(ct);
_log?.Invoke($"PCdrv: CLOSE handle={handle}");
var f = FindOpenFile(handle);
if (f == null)
{
// No such file — "great success" per NOTPSXSerial
SendString("OKAY");
WriteInt32(0);
return;
}
try
{
f.Stream.Close();
f.Stream.Dispose();
f.Closed = true;
SendString("OKAY");
WriteInt32(handle);
_log?.Invoke($"PCdrv: closed handle={handle}");
}
catch (Exception ex)
{
_log?.Invoke($"PCdrv: close error: {ex.Message}");
SendString("NOPE");
}
}
private void HandleRead(CancellationToken ct)
{
// Unirom sends: we respond OKAY first, then read handle + len + memaddr
SendString("OKAY");
int handle = ReadInt32(ct);
int length = ReadInt32(ct);
int memAddr = ReadInt32(ct); // for debugging only
_log?.Invoke($"PCdrv: READ handle={handle} len=0x{length:X} memAddr=0x{memAddr:X}");
var f = FindOpenFile(handle);
if (f == null)
{
_log?.Invoke($"PCdrv: no file with handle {handle}");
SendString("NOPE");
return;
}
try
{
byte[] data = new byte[length];
int bytesRead = f.Stream.Read(data, 0, length);
SendString("OKAY");
WriteInt32(data.Length);
// Checksum (simple byte sum, forced V3 = true per NOTPSXSerial)
uint checksum = CalculateChecksum(data);
WriteUInt32(checksum);
// Send data using chunked writer (with per-chunk ack for V2+)
WriteDataChunked(data);
_log?.Invoke($"PCdrv: sent {bytesRead} bytes for handle={handle}");
}
catch (Exception ex)
{
_log?.Invoke($"PCdrv: read error: {ex.Message}");
SendString("NOPE");
}
}
private void HandleWrite(CancellationToken ct)
{
SendString("OKAY");
int handle = ReadInt32(ct);
int length = ReadInt32(ct);
int memAddr = ReadInt32(ct);
_log?.Invoke($"PCdrv: WRITE handle={handle} len={length}");
var f = FindOpenFile(handle);
if (f == null)
{
SendString("NOPE");
return;
}
SendString("OKAY");
// Read data from PSX
byte[] data = ReadBytes(length, ct);
f.Stream.Write(data, 0, length);
f.Stream.Flush();
SendString("OKAY");
WriteInt32(length);
_log?.Invoke($"PCdrv: wrote {length} bytes to handle={handle}");
}
private void HandleSeek(CancellationToken ct)
{
SendString("OKAY");
int handle = ReadInt32(ct);
int offset = ReadInt32(ct);
int whence = ReadInt32(ct);
_log?.Invoke($"PCdrv: SEEK handle={handle} offset={offset} whence={whence}");
var f = FindOpenFile(handle);
if (f == null)
{
SendString("NOPE");
return;
}
SeekOrigin origin = whence switch
{
0 => SeekOrigin.Begin,
1 => SeekOrigin.Current,
2 => SeekOrigin.End,
_ => SeekOrigin.Begin
};
try
{
long newPos = f.Stream.Seek(offset, origin);
SendString("OKAY");
WriteInt32((int)newPos);
}
catch (Exception ex)
{
_log?.Invoke($"PCdrv: seek error: {ex.Message}");
SendString("NOPE");
}
}
// ═══════════════════════════════════════════════════════════════
// PS1 output routing
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Routes PS1 printf output to PSXConsoleWindow (via _psxLog) if available,
/// otherwise falls back to the control panel log.
/// </summary>
private void EmitPsxLine(string text)
{
if (_psxLog != null)
_psxLog.Invoke(text);
else
_log?.Invoke($"PS1> {text}");
}
// ═══════════════════════════════════════════════════════════════
// Chunked data write — matches NOTPSXSerial's WriteBytes()
// Sends data in 2048-byte chunks; for protocol V2+ Unirom
// responds with CHEK/MORE/ERR! per chunk.
// ═══════════════════════════════════════════════════════════════
private void WriteDataChunked(byte[] data)
{
int chunkSize = 2048;
for (int i = 0; i < data.Length; i += chunkSize)
{
int thisChunk = Math.Min(chunkSize, data.Length - i);
_port.Write(data, i, thisChunk);
// Wait for bytes to drain
while (_port.BytesToWrite > 0)
Thread.Sleep(0);
// V2 protocol: wait for CHEK, send chunk checksum, wait for MORE
// For now, handle this if present
if (_port.BytesToRead >= 4)
{
string resp = ReadFixedString(4);
if (resp == "CHEK")
{
ulong chunkSum = 0;
for (int j = 0; j < thisChunk; j++)
chunkSum += data[i + j];
_port.Write(BitConverter.GetBytes((uint)chunkSum), 0, 4);
Thread.Sleep(1);
// Wait for MORE or ERR!
string ack = WaitFor4CharResponse(5000);
if (ack == "ERR!")
{
_log?.Invoke("PCdrv: chunk checksum error, retrying...");
i -= chunkSize; // retry
}
}
}
}
}
// ═══════════════════════════════════════════════════════════════
// File handle helpers
// ═══════════════════════════════════════════════════════════════
private int NextHandle() => _files.Count + 1;
private PCFile FindOpenFile(string name)
{
for (int i = 0; i < _files.Count; i++)
{
if (!_files[i].Closed && _files[i].Name.Equals(name, StringComparison.OrdinalIgnoreCase))
return _files[i];
}
return null;
}
private PCFile FindOpenFile(int handle)
{
for (int i = 0; i < _files.Count; i++)
{
if (!_files[i].Closed && _files[i].Handle == handle)
return _files[i];
}
return null;
}
private string ResolvePath(string filename)
{
// Strip leading slashes and backslashes
filename = filename.TrimStart('/', '\\');
return Path.Combine(_baseDir, filename);
}
// ═══════════════════════════════════════════════════════════════
// Low-level serial I/O
// ═══════════════════════════════════════════════════════════════
private int ReadInt32(CancellationToken ct)
{
byte[] buf = new byte[4];
for (int i = 0; i < 4; i++)
buf[i] = (byte)ReadByteBlocking(ct);
return BitConverter.ToInt32(buf, 0);
}
private uint ReadUInt32(CancellationToken ct)
{
byte[] buf = new byte[4];
for (int i = 0; i < 4; i++)
buf[i] = (byte)ReadByteBlocking(ct);
return BitConverter.ToUInt32(buf, 0);
}
private byte[] ReadBytes(int count, CancellationToken ct)
{
byte[] data = new byte[count];
int pos = 0;
while (pos < count)
{
ct.ThrowIfCancellationRequested();
if (_port.BytesToRead > 0)
{
int read = _port.Read(data, pos, Math.Min(count - pos, _port.BytesToRead));
pos += read;
}
else
{
Thread.Sleep(1);
}
}
return data;
}
private int ReadByteBlocking(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (_port.BytesToRead > 0)
return _port.ReadByte();
Thread.Sleep(1);
}
throw new OperationCanceledException();
}
private string ReadNullTermString(CancellationToken ct)
{
var sb = new StringBuilder();
while (true)
{
int b = ReadByteBlocking(ct);
if (b == 0) break;
sb.Append((char)b);
if (sb.Length > 255) break;
}
return sb.ToString();
}
private void SendString(string s)
{
byte[] data = Encoding.ASCII.GetBytes(s);
_port.Write(data, 0, data.Length);
}
private void WriteInt32(int value)
{
_port.Write(BitConverter.GetBytes(value), 0, 4);
}
private void WriteUInt32(uint value)
{
_port.Write(BitConverter.GetBytes(value), 0, 4);
}
private string ReadFixedString(int count)
{
var sb = new StringBuilder();
for (int i = 0; i < count; i++)
{
if (_port.BytesToRead > 0)
sb.Append((char)_port.ReadByte());
}
return sb.ToString();
}
private string WaitFor4CharResponse(int timeoutMs)
{
string buffer = "";
DateTime deadline = DateTime.Now.AddMilliseconds(timeoutMs);
while (DateTime.Now < deadline)
{
if (_port.BytesToRead > 0)
{
buffer += (char)_port.ReadByte();
if (buffer.Length > 4)
buffer = buffer.Substring(buffer.Length - 4);
if (buffer == "MORE" || buffer == "ERR!")
return buffer;
}
else
{
Thread.Sleep(1);
}
}
return buffer;
}
private static uint CalculateChecksum(byte[] data)
{
// Force V3-style for PCDrv reads (per NOTPSXSerial: forceProtocolV3=true)
uint sum = 0;
for (int i = 0; i < data.Length; i++)
sum += data[i];
return sum;
}
}
}

View File

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

View File

@@ -0,0 +1,283 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using SplashEdit.RuntimeCode;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Downloads psxavenc and converts WAV audio to PS1 SPU ADPCM format.
/// psxavenc is the standard tool for PS1 audio encoding from the
/// WonderfulToolchain project.
/// </summary>
[InitializeOnLoad]
public static class PSXAudioConverter
{
static PSXAudioConverter()
{
// Register the converter delegate so Runtime code can call it
// without directly referencing this Editor assembly.
PSXSceneExporter.AudioConvertDelegate = ConvertToADPCM;
}
private const string PSXAVENC_VERSION = "v0.3.1";
private const string PSXAVENC_RELEASE_BASE =
"https://github.com/WonderfulToolchain/psxavenc/releases/download/";
private static readonly HttpClient _http = new HttpClient();
/// <summary>
/// Path to the psxavenc binary inside .tools/
/// </summary>
public static string PsxavencBinary
{
get
{
string dir = Path.Combine(SplashBuildPaths.ToolsDir, "psxavenc");
if (Application.platform == RuntimePlatform.WindowsEditor)
return Path.Combine(dir, "psxavenc.exe");
return Path.Combine(dir, "psxavenc");
}
}
public static bool IsInstalled() => File.Exists(PsxavencBinary);
/// <summary>
/// Downloads and installs psxavenc 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 = $"psxavenc-windows.zip";
break;
case RuntimePlatform.LinuxEditor:
archiveName = $"psxavenc-linux.zip";
break;
default:
log?.Invoke("Only Windows and Linux are supported.");
return false;
}
string downloadUrl = $"{PSXAVENC_RELEASE_BASE}{PSXAVENC_VERSION}/{archiveName}";
log?.Invoke($"Downloading psxavenc: {downloadUrl}");
try
{
string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
EditorUtility.DisplayProgressBar("Downloading psxavenc", "Downloading...", 0.1f);
using (var client = new System.Net.WebClient())
{
client.Headers.Add("User-Agent", "SplashEdit/1.0");
client.DownloadProgressChanged += (s, e) =>
{
float progress = 0.1f + 0.8f * (e.ProgressPercentage / 100f);
string sizeMB = $"{e.BytesReceived / (1024 * 1024)}/{e.TotalBytesToReceive / (1024 * 1024)} MB";
EditorUtility.DisplayProgressBar("Downloading psxavenc", $"Downloading... {sizeMB}", progress);
};
await client.DownloadFileTaskAsync(new Uri(downloadUrl), tempFile);
}
log?.Invoke("Extracting...");
EditorUtility.DisplayProgressBar("Installing psxavenc", "Extracting...", 0.9f);
string installDir = Path.Combine(SplashBuildPaths.ToolsDir, "psxavenc");
if (Directory.Exists(installDir))
Directory.Delete(installDir, true);
Directory.CreateDirectory(installDir);
if (tempFile.EndsWith(".zip"))
{
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
}
else
{
// tar.gz extraction — use system tar
var psi = new ProcessStartInfo
{
FileName = "tar",
Arguments = $"xzf \"{tempFile}\" -C \"{installDir}\" --strip-components=1",
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(psi);
proc.WaitForExit();
}
// Fix nested directory (sometimes archives have one extra level)
SplashEdit.RuntimeCode.Utils.FixNestedDirectory(installDir);
try { File.Delete(tempFile); } catch { }
EditorUtility.ClearProgressBar();
if (IsInstalled())
{
// Make executable on Linux
if (Application.platform == RuntimePlatform.LinuxEditor)
{
var chmod = Process.Start("chmod", $"+x \"{PsxavencBinary}\"");
chmod?.WaitForExit();
}
log?.Invoke("psxavenc installed successfully!");
return true;
}
log?.Invoke($"psxavenc binary not found at: {PsxavencBinary}");
return false;
}
catch (Exception ex)
{
log?.Invoke($"psxavenc download failed: {ex.Message}");
EditorUtility.ClearProgressBar();
return false;
}
}
/// <summary>
/// Converts a Unity AudioClip to PS1 SPU ADPCM format using psxavenc.
/// Returns the ADPCM byte array, or null on failure.
/// </summary>
public static byte[] ConvertToADPCM(AudioClip clip, int targetSampleRate, bool loop)
{
if (!IsInstalled())
{
Debug.LogError("[SplashEdit] psxavenc not installed. Install it from the Setup tab.");
return null;
}
if (clip == null)
{
Debug.LogError("[SplashEdit] AudioClip is null.");
return null;
}
// Export Unity AudioClip to a temporary WAV file
string tempWav = Path.Combine(Path.GetTempPath(), $"psx_audio_{clip.name}.wav");
string tempVag = Path.Combine(Path.GetTempPath(), $"psx_audio_{clip.name}.vag");
try
{
ExportWav(clip, tempWav);
// Run psxavenc: convert WAV to SPU ADPCM
// -t spu: raw SPU ADPCM output (no header, ready for DMA upload)
// -f <rate>: target sample rate
// -L: enable looping flag in the last ADPCM block
string loopFlag = loop ? "-L" : "";
string args = $"-t spu -f {targetSampleRate} {loopFlag} \"{tempWav}\" \"{tempVag}\"";
var psi = new ProcessStartInfo
{
FileName = PsxavencBinary,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
var process = Process.Start(psi);
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
Debug.LogError($"[SplashEdit] psxavenc failed: {stderr}");
return null;
}
if (!File.Exists(tempVag))
{
Debug.LogError("[SplashEdit] psxavenc produced no output file.");
return null;
}
// -t spu outputs raw SPU ADPCM blocks (no header) — use directly.
byte[] adpcm = File.ReadAllBytes(tempVag);
if (adpcm.Length == 0)
{
Debug.LogError("[SplashEdit] psxavenc produced empty output.");
return null;
}
return adpcm;
}
finally
{
try { if (File.Exists(tempWav)) File.Delete(tempWav); } catch { }
try { if (File.Exists(tempVag)) File.Delete(tempVag); } catch { }
}
}
/// <summary>
/// Exports a Unity AudioClip to a 16-bit mono WAV file.
/// </summary>
private static void ExportWav(AudioClip clip, string path)
{
float[] samples = new float[clip.samples * clip.channels];
clip.GetData(samples, 0);
// Downmix to mono if stereo
float[] mono;
if (clip.channels > 1)
{
mono = new float[clip.samples];
for (int i = 0; i < clip.samples; i++)
{
float sum = 0;
for (int ch = 0; ch < clip.channels; ch++)
sum += samples[i * clip.channels + ch];
mono[i] = sum / clip.channels;
}
}
else
{
mono = samples;
}
// Write WAV
using (var fs = new FileStream(path, FileMode.Create))
using (var writer = new BinaryWriter(fs))
{
int sampleCount = mono.Length;
int dataSize = sampleCount * 2; // 16-bit
int fileSize = 44 + dataSize;
// RIFF header
writer.Write(new char[] { 'R', 'I', 'F', 'F' });
writer.Write(fileSize - 8);
writer.Write(new char[] { 'W', 'A', 'V', 'E' });
// fmt chunk
writer.Write(new char[] { 'f', 'm', 't', ' ' });
writer.Write(16); // chunk size
writer.Write((short)1); // PCM
writer.Write((short)1); // mono
writer.Write(clip.frequency);
writer.Write(clip.frequency * 2); // byte rate
writer.Write((short)2); // block align
writer.Write((short)16); // bits per sample
// data chunk
writer.Write(new char[] { 'd', 'a', 't', 'a' });
writer.Write(dataSize);
for (int i = 0; i < sampleCount; i++)
{
short sample = (short)(Mathf.Clamp(mono[i], -1f, 1f) * 32767f);
writer.Write(sample);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 372b2ef07e125584ba43312b0662d7ac

View File

@@ -0,0 +1,435 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using UnityEditor;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// A live console window that displays stdout/stderr from PCSX-Redux and PSX build output.
/// Opens automatically when a build starts or the emulator launches.
/// </summary>
public class PSXConsoleWindow : EditorWindow
{
private const string WINDOW_TITLE = "PSX Console";
private const string MENU_PATH = "PlayStation 1/PSX Console";
private const int MAX_LINES = 2000;
private const int TRIM_AMOUNT = 500;
// ── Shared state (set by SplashControlPanel) ──
private static Process _process;
private static readonly List<LogLine> _lines = new List<LogLine>();
private static readonly object _lock = new object();
private static volatile bool _autoScroll = true;
private static volatile bool _reading;
// ── Instance state ──
private Vector2 _scrollPos;
private string _filterText = "";
private bool _showStdout = true;
private bool _showStderr = true;
private bool _wrapLines = true;
private GUIStyle _monoStyle;
private GUIStyle _monoStyleErr;
private GUIStyle _monoStyleSelected;
private int _lastLineCount;
// ── Selection state (for shift-click range and right-click copy) ──
private int _selectionAnchor = -1; // first clicked line index (into _lines)
private int _selectionEnd = -1; // last shift-clicked line index (into _lines)
private struct LogLine
{
public string text;
public bool isError;
public string timestamp;
}
// ═══════════════════════════════════════════════════════════════
// Menu
// ═══════════════════════════════════════════════════════════════
[MenuItem(MENU_PATH, false, 10)]
public static void ShowWindow()
{
var window = GetWindow<PSXConsoleWindow>();
window.titleContent = new GUIContent(WINDOW_TITLE, EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
window.minSize = new Vector2(400, 200);
window.Show();
}
// ═══════════════════════════════════════════════════════════════
// Public API — called by SplashControlPanel
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Adds a line to the console from any source (serial host, emulator fallback, etc.).
/// Thread-safe. Works whether the window is open or not.
/// </summary>
public static void AddLine(string text, bool isError = false)
{
if (string.IsNullOrEmpty(text)) return;
lock (_lock)
{
_lines.Add(new LogLine
{
text = text,
isError = isError,
timestamp = DateTime.Now.ToString("HH:mm:ss.fff")
});
if (_lines.Count > MAX_LINES)
{
_lines.RemoveRange(0, TRIM_AMOUNT);
}
}
// Repaint is handled by OnEditorUpdate polling _lines.Count changes.
// Do NOT call EditorApplication.delayCall here - AddLine is called
// from background threads (serial host, process readers) and
// delayCall is not thread-safe. It kills the calling thread.
}
/// <summary>
/// Opens the console window and begins capturing output from the given process.
/// The process must have RedirectStandardOutput and RedirectStandardError enabled.
/// </summary>
public static PSXConsoleWindow Attach(Process process)
{
// Stop reading from any previous process (but keep existing lines)
_reading = false;
_process = process;
var window = GetWindow<PSXConsoleWindow>();
window.titleContent = new GUIContent(WINDOW_TITLE, EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow").image);
window.minSize = new Vector2(400, 200);
window.Show();
// Start async readers
_reading = true;
StartReader(process.StandardOutput, false);
StartReader(process.StandardError, true);
return window;
}
/// <summary>
/// Stops reading and detaches from the current process.
/// </summary>
public static void Detach()
{
_reading = false;
_process = null;
}
// ═══════════════════════════════════════════════════════════════
// Async readers
// ═══════════════════════════════════════════════════════════════
private static void StartReader(System.IO.StreamReader reader, bool isError)
{
var thread = new Thread(() =>
{
try
{
while (_reading && !reader.EndOfStream)
{
string line = reader.ReadLine();
if (line == null) break;
lock (_lock)
{
_lines.Add(new LogLine
{
text = line,
isError = isError,
timestamp = DateTime.Now.ToString("HH:mm:ss.fff")
});
// Trim if too many lines
if (_lines.Count > MAX_LINES)
{
_lines.RemoveRange(0, TRIM_AMOUNT);
}
}
}
}
catch (Exception)
{
// Stream closed — normal when process exits
}
})
{
IsBackground = true,
Name = isError ? "PSXConsole-stderr" : "PSXConsole-stdout"
};
thread.Start();
}
// ═══════════════════════════════════════════════════════════════
// Window lifecycle
// ═══════════════════════════════════════════════════════════════
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
}
private void OnEditorUpdate()
{
// Repaint when new lines arrive
int count;
lock (_lock) { count = _lines.Count; }
if (count != _lastLineCount)
{
_lastLineCount = count;
Repaint();
}
}
// ═══════════════════════════════════════════════════════════════
// GUI
// ═══════════════════════════════════════════════════════════════
private void EnsureStyles()
{
if (_monoStyle == null)
{
_monoStyle = new GUIStyle(EditorStyles.label)
{
font = Font.CreateDynamicFontFromOSFont("Consolas", 12),
fontSize = 11,
richText = false,
wordWrap = _wrapLines,
normal = { textColor = new Color(0.85f, 0.85f, 0.85f) },
padding = new RectOffset(4, 4, 1, 1),
margin = new RectOffset(0, 0, 0, 0)
};
}
if (_monoStyleErr == null)
{
_monoStyleErr = new GUIStyle(_monoStyle)
{
normal = { textColor = new Color(1f, 0.45f, 0.4f) }
};
}
if (_monoStyleSelected == null)
{
_monoStyleSelected = new GUIStyle(_monoStyle)
{
normal =
{
textColor = new Color(0.95f, 0.95f, 0.95f),
background = MakeSolidTexture(new Color(0.25f, 0.40f, 0.65f, 0.6f))
}
};
}
_monoStyle.wordWrap = _wrapLines;
_monoStyleErr.wordWrap = _wrapLines;
_monoStyleSelected.wordWrap = _wrapLines;
}
private static Texture2D MakeSolidTexture(Color color)
{
var tex = new Texture2D(1, 1);
tex.SetPixel(0, 0, color);
tex.Apply();
return tex;
}
// Snapshot taken at the start of each OnGUI so Layout and Repaint
// events always see the same line count (prevents "Getting control
// position in a group with only N controls" errors).
private LogLine[] _snapshot = Array.Empty<LogLine>();
private void OnGUI()
{
EnsureStyles();
// Take a snapshot once per OnGUI so Layout and Repaint see
// identical control counts even if background threads add lines.
if (Event.current.type == EventType.Layout)
{
lock (_lock)
{
_snapshot = _lines.ToArray();
}
}
DrawToolbar();
DrawConsoleOutput();
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
// Process status
bool alive = _process != null && !_process.HasExited;
var statusColor = GUI.contentColor;
GUI.contentColor = alive ? Color.green : Color.gray;
GUILayout.Label(alive ? "● Live" : "● Stopped", EditorStyles.toolbarButton, GUILayout.Width(60));
GUI.contentColor = statusColor;
// Filter
GUILayout.Label("Filter:", GUILayout.Width(40));
_filterText = EditorGUILayout.TextField(_filterText, EditorStyles.toolbarSearchField, GUILayout.Width(150));
GUILayout.FlexibleSpace();
// Toggles
_showStdout = GUILayout.Toggle(_showStdout, "stdout", EditorStyles.toolbarButton, GUILayout.Width(50));
_showStderr = GUILayout.Toggle(_showStderr, "stderr", EditorStyles.toolbarButton, GUILayout.Width(50));
_wrapLines = GUILayout.Toggle(_wrapLines, "Wrap", EditorStyles.toolbarButton, GUILayout.Width(40));
// Auto-scroll
_autoScroll = GUILayout.Toggle(_autoScroll, "Auto↓", EditorStyles.toolbarButton, GUILayout.Width(50));
// Clear
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(45)))
{
lock (_lock) { _lines.Clear(); }
}
// Copy all
if (GUILayout.Button("Copy", EditorStyles.toolbarButton, GUILayout.Width(40)))
{
CopyToClipboard();
}
EditorGUILayout.EndHorizontal();
}
private void DrawConsoleOutput()
{
// Simple scroll view - no BeginArea/EndArea mixing that causes layout errors.
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
// Dark background behind the scroll content
Rect scrollBg = EditorGUILayout.BeginVertical();
EditorGUI.DrawRect(scrollBg, new Color(0.13f, 0.13f, 0.15f));
bool hasFilter = !string.IsNullOrEmpty(_filterText);
string filterLower = hasFilter ? _filterText.ToLowerInvariant() : null;
int selMin = Mathf.Min(_selectionAnchor, _selectionEnd);
int selMax = Mathf.Max(_selectionAnchor, _selectionEnd);
bool hasSelection = _selectionAnchor >= 0 && _selectionEnd >= 0;
// Iterate the snapshot taken during Layout so the control count
// is stable across Layout and Repaint events.
var snapshot = _snapshot;
if (snapshot.Length == 0)
{
GUILayout.Label("Waiting for output...", EditorStyles.centeredGreyMiniLabel);
}
for (int i = 0; i < snapshot.Length; i++)
{
var line = snapshot[i];
if (line.isError && !_showStderr) continue;
if (!line.isError && !_showStdout) continue;
if (hasFilter && line.text.ToLowerInvariant().IndexOf(filterLower, StringComparison.Ordinal) < 0)
continue;
bool selected = hasSelection && i >= selMin && i <= selMax;
GUIStyle style = selected ? _monoStyleSelected : (line.isError ? _monoStyleErr : _monoStyle);
string label = $"[{line.timestamp}] {line.text}";
GUILayout.Label(label, style);
// Handle click/right-click on last drawn rect
Rect lineRect = GUILayoutUtility.GetLastRect();
Event evt = Event.current;
if (evt.type == EventType.MouseDown && lineRect.Contains(evt.mousePosition))
{
if (evt.button == 0)
{
if (evt.shift && _selectionAnchor >= 0)
_selectionEnd = i;
else
{
_selectionAnchor = i;
_selectionEnd = i;
}
evt.Use();
Repaint();
}
else if (evt.button == 1)
{
int clickedLine = i;
bool lineInSelection = hasSelection && clickedLine >= selMin && clickedLine <= selMax;
var menu = new GenericMenu();
if (lineInSelection && selMin != selMax)
{
menu.AddItem(new GUIContent("Copy selected lines"), false, () => CopyRange(selMin, selMax));
menu.AddSeparator("");
}
menu.AddItem(new GUIContent("Copy this line"), false, () =>
{
string text;
lock (_lock)
{
text = clickedLine < _lines.Count
? $"[{_lines[clickedLine].timestamp}] {_lines[clickedLine].text}"
: "";
}
EditorGUIUtility.systemCopyBuffer = text;
});
menu.ShowAsContext();
evt.Use();
}
}
}
EditorGUILayout.EndVertical();
if (_autoScroll)
_scrollPos.y = float.MaxValue;
EditorGUILayout.EndScrollView();
}
private void CopyRange(int fromIndex, int toIndex)
{
var sb = new StringBuilder();
lock (_lock)
{
int lo = Mathf.Min(fromIndex, toIndex);
int hi = Mathf.Max(fromIndex, toIndex);
for (int i = lo; i <= hi && i < _lines.Count; i++)
{
string prefix = _lines[i].isError ? "[ERR]" : "[OUT]";
sb.AppendLine($"[{_lines[i].timestamp}] {prefix} {_lines[i].text}");
}
}
EditorGUIUtility.systemCopyBuffer = sb.ToString();
}
private void CopyToClipboard()
{
var sb = new StringBuilder();
lock (_lock)
{
foreach (var line in _lines)
{
string prefix = line.isError ? "[ERR]" : "[OUT]";
sb.AppendLine($"[{line.timestamp}] {prefix} {line.text}");
}
}
EditorGUIUtility.systemCopyBuffer = sb.ToString();
}
}
}

View File

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

View File

@@ -0,0 +1,777 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Unified styling system for PSX Splash editor windows.
/// Provides consistent colors, fonts, icons, and GUIStyles across the entire plugin.
/// </summary>
[InitializeOnLoad]
public static class PSXEditorStyles
{
static PSXEditorStyles()
{
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
}
private static void OnBeforeAssemblyReload()
{
foreach (var tex in _textureCache.Values)
{
if (tex != null)
Object.DestroyImmediate(tex);
}
_textureCache.Clear();
_styleCache.Clear();
}
#region Colors - PS1 Inspired Palette
// Primary colors
public static readonly Color PrimaryBlue = new Color(0.15f, 0.35f, 0.65f);
public static readonly Color PrimaryDark = new Color(0.12f, 0.12f, 0.14f);
public static readonly Color PrimaryLight = new Color(0.22f, 0.22f, 0.25f);
// Accent colors
public static readonly Color AccentGold = new Color(0.95f, 0.75f, 0.2f);
public static readonly Color AccentCyan = new Color(0.3f, 0.85f, 0.95f);
public static readonly Color AccentMagenta = new Color(0.85f, 0.3f, 0.65f);
public static readonly Color AccentGreen = new Color(0.35f, 0.85f, 0.45f);
// Semantic colors
public static readonly Color Success = new Color(0.35f, 0.8f, 0.4f);
public static readonly Color Warning = new Color(0.95f, 0.75f, 0.2f);
public static readonly Color Error = new Color(0.9f, 0.3f, 0.35f);
public static readonly Color Info = new Color(0.4f, 0.7f, 0.95f);
// Background colors
public static readonly Color BackgroundDark = new Color(0.15f, 0.15f, 0.17f);
public static readonly Color BackgroundMedium = new Color(0.2f, 0.2f, 0.22f);
public static readonly Color BackgroundLight = new Color(0.28f, 0.28f, 0.3f);
public static readonly Color BackgroundHighlight = new Color(0.25f, 0.35f, 0.5f);
// Text colors
public static readonly Color TextPrimary = new Color(0.9f, 0.9f, 0.92f);
public static readonly Color TextSecondary = new Color(0.65f, 0.65f, 0.7f);
public static readonly Color TextMuted = new Color(0.45f, 0.45f, 0.5f);
// VRAM specific colors
public static readonly Color VRAMFrameBuffer1 = new Color(1f, 0.3f, 0.3f, 0.4f);
public static readonly Color VRAMFrameBuffer2 = new Color(0.3f, 1f, 0.3f, 0.4f);
public static readonly Color VRAMProhibited = new Color(1f, 0f, 0f, 0.25f);
public static readonly Color VRAMTexture = new Color(0.3f, 0.6f, 1f, 0.5f);
public static readonly Color VRAMCLUT = new Color(1f, 0.6f, 0.3f, 0.5f);
#endregion
#region Cached Styles
private static Dictionary<string, GUIStyle> _styleCache = new Dictionary<string, GUIStyle>();
private static Dictionary<string, Texture2D> _textureCache = new Dictionary<string, Texture2D>();
#endregion
#region Textures
public static Texture2D GetSolidTexture(Color color)
{
string key = $"solid_{color.r}_{color.g}_{color.b}_{color.a}";
if (!_textureCache.TryGetValue(key, out var tex) || tex == null)
{
tex = new Texture2D(1, 1);
tex.SetPixel(0, 0, color);
tex.Apply();
tex.hideFlags = HideFlags.HideAndDontSave;
_textureCache[key] = tex;
}
return tex;
}
public static Texture2D CreateGradientTexture(int width, int height, Color top, Color bottom)
{
Texture2D tex = new Texture2D(width, height);
for (int y = 0; y < height; y++)
{
Color c = Color.Lerp(bottom, top, (float)y / height);
for (int x = 0; x < width; x++)
{
tex.SetPixel(x, y, c);
}
}
tex.Apply();
tex.hideFlags = HideFlags.HideAndDontSave;
return tex;
}
public static Texture2D CreateRoundedRect(int width, int height, int radius, Color fillColor, Color borderColor, int borderWidth = 1)
{
Texture2D tex = new Texture2D(width, height);
Color transparent = new Color(0, 0, 0, 0);
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// Check if pixel is within rounded corners
bool inCorner = false;
float dist = 0;
// Top-left
if (x < radius && y > height - radius - 1)
{
dist = Vector2.Distance(new Vector2(x, y), new Vector2(radius, height - radius - 1));
inCorner = true;
}
// Top-right
else if (x > width - radius - 1 && y > height - radius - 1)
{
dist = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, height - radius - 1));
inCorner = true;
}
// Bottom-left
else if (x < radius && y < radius)
{
dist = Vector2.Distance(new Vector2(x, y), new Vector2(radius, radius));
inCorner = true;
}
// Bottom-right
else if (x > width - radius - 1 && y < radius)
{
dist = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, radius));
inCorner = true;
}
if (inCorner)
{
if (dist > radius)
tex.SetPixel(x, y, transparent);
else if (dist > radius - borderWidth)
tex.SetPixel(x, y, borderColor);
else
tex.SetPixel(x, y, fillColor);
}
else
{
// Check border
if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth)
tex.SetPixel(x, y, borderColor);
else
tex.SetPixel(x, y, fillColor);
}
}
}
tex.Apply();
tex.hideFlags = HideFlags.HideAndDontSave;
return tex;
}
#endregion
#region GUIStyles
private static GUIStyle _windowHeader;
public static GUIStyle WindowHeader
{
get
{
if (_windowHeader == null)
{
_windowHeader = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 18,
alignment = TextAnchor.MiddleLeft,
padding = new RectOffset(10, 10, 8, 8),
margin = new RectOffset(0, 0, 0, 5)
};
_windowHeader.normal.textColor = TextPrimary;
}
return _windowHeader;
}
}
private static GUIStyle _sectionHeader;
public static GUIStyle SectionHeader
{
get
{
if (_sectionHeader == null)
{
_sectionHeader = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14,
alignment = TextAnchor.MiddleLeft,
padding = new RectOffset(5, 5, 8, 8),
margin = new RectOffset(0, 0, 10, 5)
};
_sectionHeader.normal.textColor = TextPrimary;
}
return _sectionHeader;
}
}
private static GUIStyle _cardStyle;
public static GUIStyle CardStyle
{
get
{
if (_cardStyle == null)
{
_cardStyle = new GUIStyle()
{
padding = new RectOffset(12, 12, 10, 10),
margin = new RectOffset(5, 5, 5, 5),
normal = { background = GetSolidTexture(BackgroundMedium) }
};
}
return _cardStyle;
}
}
private static GUIStyle _cardHeaderStyle;
public static GUIStyle CardHeaderStyle
{
get
{
if (_cardHeaderStyle == null)
{
_cardHeaderStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 13,
padding = new RectOffset(0, 0, 0, 5),
margin = new RectOffset(0, 0, 0, 5)
};
_cardHeaderStyle.normal.textColor = TextPrimary;
}
return _cardHeaderStyle;
}
}
private static GUIStyle _primaryButton;
public static GUIStyle PrimaryButton
{
get
{
if (_primaryButton == null)
{
_primaryButton = new GUIStyle(GUI.skin.button)
{
fontSize = 12,
fontStyle = FontStyle.Bold,
padding = new RectOffset(15, 15, 8, 8),
margin = new RectOffset(5, 5, 5, 5),
alignment = TextAnchor.MiddleCenter
};
_primaryButton.normal.textColor = Color.white;
_primaryButton.normal.background = GetSolidTexture(PrimaryBlue);
_primaryButton.hover.background = GetSolidTexture(PrimaryBlue * 1.2f);
_primaryButton.active.background = GetSolidTexture(PrimaryBlue * 0.8f);
}
return _primaryButton;
}
}
private static GUIStyle _secondaryButton;
public static GUIStyle SecondaryButton
{
get
{
if (_secondaryButton == null)
{
_secondaryButton = new GUIStyle(GUI.skin.button)
{
fontSize = 11,
padding = new RectOffset(12, 12, 6, 6),
margin = new RectOffset(3, 3, 3, 3),
alignment = TextAnchor.MiddleCenter
};
_secondaryButton.normal.textColor = TextPrimary;
_secondaryButton.normal.background = GetSolidTexture(BackgroundLight);
_secondaryButton.hover.background = GetSolidTexture(BackgroundLight * 1.3f);
_secondaryButton.active.background = GetSolidTexture(BackgroundLight * 0.7f);
}
return _secondaryButton;
}
}
private static GUIStyle _successButton;
public static GUIStyle SuccessButton
{
get
{
if (_successButton == null)
{
_successButton = new GUIStyle(PrimaryButton);
_successButton.normal.background = GetSolidTexture(Success * 0.8f);
_successButton.hover.background = GetSolidTexture(Success);
_successButton.active.background = GetSolidTexture(Success * 0.6f);
}
return _successButton;
}
}
private static GUIStyle _dangerButton;
public static GUIStyle DangerButton
{
get
{
if (_dangerButton == null)
{
_dangerButton = new GUIStyle(PrimaryButton);
_dangerButton.normal.background = GetSolidTexture(Error * 0.8f);
_dangerButton.hover.background = GetSolidTexture(Error);
_dangerButton.active.background = GetSolidTexture(Error * 0.6f);
}
return _dangerButton;
}
}
private static GUIStyle _statusBadge;
public static GUIStyle StatusBadge
{
get
{
if (_statusBadge == null)
{
_statusBadge = new GUIStyle(EditorStyles.label)
{
fontSize = 10,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter,
padding = new RectOffset(8, 8, 3, 3),
margin = new RectOffset(3, 3, 3, 3)
};
}
return _statusBadge;
}
}
private static GUIStyle _toolbarStyle;
public static GUIStyle ToolbarStyle
{
get
{
if (_toolbarStyle == null)
{
_toolbarStyle = new GUIStyle()
{
padding = new RectOffset(8, 8, 6, 6),
margin = new RectOffset(0, 0, 0, 0),
normal = { background = GetSolidTexture(BackgroundDark) }
};
}
return _toolbarStyle;
}
}
private static GUIStyle _infoBox;
public static GUIStyle InfoBox
{
get
{
if (_infoBox == null)
{
_infoBox = new GUIStyle(EditorStyles.helpBox)
{
fontSize = 11,
padding = new RectOffset(10, 10, 8, 8),
margin = new RectOffset(5, 5, 5, 5),
richText = true
};
}
return _infoBox;
}
}
private static GUIStyle _centeredLabel;
public static GUIStyle CenteredLabel
{
get
{
if (_centeredLabel == null)
{
_centeredLabel = new GUIStyle(EditorStyles.label)
{
alignment = TextAnchor.MiddleCenter,
wordWrap = true
};
}
return _centeredLabel;
}
}
private static GUIStyle _richLabel;
public static GUIStyle RichLabel
{
get
{
if (_richLabel == null)
{
_richLabel = new GUIStyle(EditorStyles.label)
{
richText = true,
wordWrap = true
};
}
return _richLabel;
}
}
private static GUIStyle _foldoutHeader;
public static GUIStyle FoldoutHeader
{
get
{
if (_foldoutHeader == null)
{
_foldoutHeader = new GUIStyle(EditorStyles.foldout)
{
fontSize = 12,
fontStyle = FontStyle.Bold,
padding = new RectOffset(15, 0, 3, 3)
};
_foldoutHeader.normal.textColor = TextPrimary;
}
return _foldoutHeader;
}
}
#endregion
#region Drawing Helpers
/// <summary>
/// Draw a horizontal separator line
/// </summary>
public static void DrawSeparator(float topMargin = 5, float bottomMargin = 5)
{
GUILayout.Space(topMargin);
var rect = GUILayoutUtility.GetRect(1, 1, GUILayout.ExpandWidth(true));
EditorGUI.DrawRect(rect, TextMuted * 0.5f);
GUILayout.Space(bottomMargin);
}
/// <summary>
/// Draw a status badge with color
/// </summary>
public static void DrawStatusBadge(string text, Color color, float width = 80)
{
var style = new GUIStyle(StatusBadge);
style.normal.background = GetSolidTexture(color);
style.normal.textColor = GetContrastColor(color);
GUILayout.Label(text, style, GUILayout.Width(width));
}
/// <summary>
/// Draw a progress bar
/// </summary>
public static void DrawProgressBar(float progress, string label, Color fillColor, float height = 20)
{
var rect = GUILayoutUtility.GetRect(100, height, GUILayout.ExpandWidth(true));
// Background
EditorGUI.DrawRect(rect, BackgroundDark);
// Fill
var fillRect = new Rect(rect.x, rect.y, rect.width * Mathf.Clamp01(progress), rect.height);
EditorGUI.DrawRect(fillRect, fillColor);
// Border
DrawBorder(rect, TextMuted * 0.5f, 1);
// Label
var labelStyle = new GUIStyle(EditorStyles.label)
{
alignment = TextAnchor.MiddleCenter,
normal = { textColor = TextPrimary }
};
GUI.Label(rect, $"{label} ({progress * 100:F0}%)", labelStyle);
}
/// <summary>
/// Draw a border around a rect
/// </summary>
public static void DrawBorder(Rect rect, Color color, int thickness = 1)
{
// Top
EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, thickness), color);
// Bottom
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - thickness, rect.width, thickness), color);
// Left
EditorGUI.DrawRect(new Rect(rect.x, rect.y, thickness, rect.height), color);
// Right
EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.y, thickness, rect.height), color);
}
/// <summary>
/// Get a contrasting text color for a background
/// </summary>
public static Color GetContrastColor(Color background)
{
float luminance = 0.299f * background.r + 0.587f * background.g + 0.114f * background.b;
return luminance > 0.5f ? Color.black : Color.white;
}
/// <summary>
/// Begin a styled card section
/// </summary>
public static void BeginCard()
{
EditorGUILayout.BeginVertical(CardStyle);
}
/// <summary>
/// End a styled card section
/// </summary>
public static void EndCard()
{
EditorGUILayout.EndVertical();
}
/// <summary>
/// Draw a card with header and content
/// </summary>
public static bool DrawFoldoutCard(string title, bool isExpanded, System.Action drawContent)
{
EditorGUILayout.BeginVertical(CardStyle);
EditorGUILayout.BeginHorizontal();
isExpanded = EditorGUILayout.Foldout(isExpanded, title, true, FoldoutHeader);
EditorGUILayout.EndHorizontal();
if (isExpanded)
{
EditorGUILayout.Space(5);
drawContent?.Invoke();
}
EditorGUILayout.EndVertical();
return isExpanded;
}
/// <summary>
/// Draw a large icon button (for dashboard)
/// </summary>
public static bool DrawIconButton(string label, string icon, string description, float width = 150, float height = 100)
{
var rect = GUILayoutUtility.GetRect(width, height);
bool isHover = rect.Contains(Event.current.mousePosition);
var bgColor = isHover ? BackgroundHighlight : BackgroundMedium;
EditorGUI.DrawRect(rect, bgColor);
DrawBorder(rect, isHover ? AccentCyan : TextMuted * 0.3f, 1);
// Icon (using Unity's built-in icons or a placeholder)
var iconRect = new Rect(rect.x + rect.width / 2 - 16, rect.y + 15, 32, 32);
var iconContent = EditorGUIUtility.IconContent(icon);
if (iconContent != null && iconContent.image != null)
{
GUI.DrawTexture(iconRect, iconContent.image);
}
// Label
var labelRect = new Rect(rect.x, rect.y + 52, rect.width, 20);
var labelStyle = new GUIStyle(EditorStyles.boldLabel)
{
alignment = TextAnchor.MiddleCenter,
normal = { textColor = TextPrimary }
};
GUI.Label(labelRect, label, labelStyle);
// Description
var descRect = new Rect(rect.x + 5, rect.y + 70, rect.width - 10, 25);
var descStyle = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.UpperCenter,
wordWrap = true,
normal = { textColor = TextSecondary }
};
GUI.Label(descRect, description, descStyle);
return GUI.Button(rect, GUIContent.none, GUIStyle.none);
}
/// <summary>
/// Draw a horizontal button group
/// </summary>
public static int DrawButtonGroup(string[] labels, int selected, float height = 25)
{
EditorGUILayout.BeginHorizontal();
for (int i = 0; i < labels.Length; i++)
{
bool isSelected = i == selected;
var style = new GUIStyle(GUI.skin.button)
{
fontStyle = isSelected ? FontStyle.Bold : FontStyle.Normal
};
if (isSelected)
{
style.normal.background = GetSolidTexture(PrimaryBlue);
style.normal.textColor = Color.white;
}
else
{
style.normal.background = GetSolidTexture(BackgroundLight);
style.normal.textColor = TextSecondary;
}
if (GUILayout.Button(labels[i], style, GUILayout.Height(height)))
{
selected = i;
}
}
EditorGUILayout.EndHorizontal();
return selected;
}
#endregion
#region Layout Helpers
/// <summary>
/// Begin a toolbar row
/// </summary>
public static void BeginToolbar()
{
EditorGUILayout.BeginHorizontal(ToolbarStyle);
}
/// <summary>
/// End a toolbar row
/// </summary>
public static void EndToolbar()
{
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// Add flexible space
/// </summary>
public static void FlexibleSpace()
{
GUILayout.FlexibleSpace();
}
/// <summary>
/// Begin a centered layout
/// </summary>
public static void BeginCentered()
{
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.BeginVertical();
}
/// <summary>
/// End a centered layout
/// </summary>
public static void EndCentered()
{
EditorGUILayout.EndVertical();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
#endregion
#region Cleanup
/// <summary>
/// Clear cached styles and textures. Call when recompiling.
/// </summary>
public static void ClearCache()
{
foreach (var tex in _textureCache.Values)
{
if (tex != null)
Object.DestroyImmediate(tex);
}
_textureCache.Clear();
_windowHeader = null;
_sectionHeader = null;
_cardStyle = null;
_cardHeaderStyle = null;
_primaryButton = null;
_secondaryButton = null;
_successButton = null;
_dangerButton = null;
_statusBadge = null;
_toolbarStyle = null;
_infoBox = null;
_centeredLabel = null;
_richLabel = null;
_foldoutHeader = null;
}
#endregion
}
/// <summary>
/// Icons used throughout the PSX Splash editor
/// </summary>
public static class PSXIcons
{
// Unity built-in icons that work well for our purposes
public const string Scene = "d_SceneAsset Icon";
public const string Build = "d_BuildSettings.SelectedIcon";
public const string Settings = "d_Settings";
public const string Play = "d_PlayButton";
public const string Refresh = "d_Refresh";
public const string Warning = "d_console.warnicon";
public const string Error = "d_console.erroricon";
public const string Info = "d_console.infoicon";
public const string Success = "d_Progress";
public const string Texture = "d_Texture Icon";
public const string Mesh = "d_Mesh Icon";
public const string Script = "d_cs Script Icon";
public const string Folder = "d_Folder Icon";
public const string Download = "d_Download-Available";
public const string Upload = "d_UpArrow";
public const string Link = "d_Linked";
public const string Unlink = "d_Unlinked";
public const string Eye = "d_scenevis_visible_hover";
public const string EyeOff = "d_scenevis_hidden_hover";
public const string Add = "d_Toolbar Plus";
public const string Remove = "d_Toolbar Minus";
public const string Edit = "d_editicon.sml";
public const string Search = "d_Search Icon";
public const string Console = "d_UnityEditor.ConsoleWindow";
public const string Help = "d__Help";
public const string GameObject = "d_GameObject Icon";
public const string Camera = "d_Camera Icon";
public const string Light = "d_Light Icon";
public const string Prefab = "d_Prefab Icon";
/// <summary>
/// Get a GUIContent with icon and tooltip
/// </summary>
public static GUIContent GetContent(string icon, string tooltip = "")
{
var content = EditorGUIUtility.IconContent(icon);
if (content == null) content = new GUIContent();
content.tooltip = tooltip;
return content;
}
/// <summary>
/// Get a GUIContent with icon, text and tooltip
/// </summary>
public static GUIContent GetContent(string icon, string text, string tooltip)
{
var content = EditorGUIUtility.IconContent(icon);
if (content == null) content = new GUIContent();
content.text = text;
content.tooltip = tooltip;
return content;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8aefa79a412d32c4f8bc8249bb4cd118

View File

@@ -0,0 +1,188 @@
using System;
using System.IO;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Memory analysis report for a single exported scene.
/// All values are in bytes unless noted otherwise.
/// </summary>
[Serializable]
public class SceneMemoryReport
{
public string sceneName;
// ─── Main RAM ───
public long splashpackFileSize; // Total file on disc
public long splashpackLiveSize; // Bytes kept in RAM at runtime (before bulk data freed)
public int triangleCount;
public int gameObjectCount;
// ─── VRAM (1024 x 512 x 2 = 1,048,576 bytes) ───
public long framebufferSize; // 2 x W x H x 2
public long textureAtlasSize; // Sum of atlas pixel data
public long clutSize; // Sum of CLUT entries x 2
public long fontVramSize; // Custom font textures
public int atlasCount;
public int clutCount;
// ─── SPU RAM (512KB, 0x1010 reserved) ───
public long audioDataSize;
public int audioClipCount;
// ─── CD Storage ───
public long loaderPackSize;
// ─── Constants ───
public const long TOTAL_RAM = 2 * 1024 * 1024;
public const long KERNEL_RESERVED = 0x10000; // 64KB kernel area
public const long USABLE_RAM = TOTAL_RAM - KERNEL_RESERVED;
public const long TOTAL_VRAM = 1024 * 512 * 2; // 1MB
public const long TOTAL_SPU = 512 * 1024;
public const long SPU_RESERVED = 0x1010;
public const long USABLE_SPU = TOTAL_SPU - SPU_RESERVED;
// Fixed runtime overhead from C++ (renderer.hh constants, now configurable)
public static long BUMP_ALLOC_TOTAL => 2L * SplashSettings.BumpSize;
public static long OT_TOTAL => 2L * SplashSettings.OtSize * 4;
public const long VIS_REFS = 4096 * 4; // 16KB
public const long STACK_ESTIMATE = 32 * 1024; // 32KB
public const long LUA_OVERHEAD = 16 * 1024; // 16KB approximate
public const long SYSTEM_FONT_VRAM = 4 * 1024; // ~4KB
public long FixedOverhead => BUMP_ALLOC_TOTAL + OT_TOTAL + VIS_REFS + STACK_ESTIMATE + LUA_OVERHEAD;
// Heap estimate and warnings
public long EstimatedHeapFree => USABLE_RAM - TotalRamUsage;
public bool IsHeapWarning => EstimatedHeapFree < 128 * 1024; // < 128KB free
public bool IsHeapCritical => EstimatedHeapFree < 64 * 1024; // < 64KB free
/// <summary>RAM used by scene data (live portion of splashpack).</summary>
public long SceneRamUsage => splashpackLiveSize > 0 ? splashpackLiveSize : splashpackFileSize;
/// <summary>Total estimated RAM: fixed overhead + scene data. Does NOT include code/BSS.</summary>
public long TotalRamUsage => FixedOverhead + SceneRamUsage;
public long TotalVramUsed => framebufferSize + textureAtlasSize + clutSize + fontVramSize + SYSTEM_FONT_VRAM;
public long TotalSpuUsed => audioDataSize;
public long TotalDiscSize => splashpackFileSize + loaderPackSize;
public float RamPercent => Mathf.Clamp01((float)TotalRamUsage / USABLE_RAM) * 100f;
public float VramPercent => Mathf.Clamp01((float)TotalVramUsed / TOTAL_VRAM) * 100f;
public float SpuPercent => USABLE_SPU > 0 ? Mathf.Clamp01((float)TotalSpuUsed / USABLE_SPU) * 100f : 0f;
public long RamFree => USABLE_RAM - TotalRamUsage;
public long VramFree => TOTAL_VRAM - TotalVramUsed;
public long SpuFree => USABLE_SPU - TotalSpuUsed;
}
/// <summary>
/// Builds a SceneMemoryReport by reading the exported splashpack binary header
/// and the scene's VRAM/audio data.
/// </summary>
public static class SceneMemoryAnalyzer
{
/// <summary>
/// Analyze an exported scene. Call after ExportToPath().
/// </summary>
/// <param name="sceneName">Display name for the scene.</param>
/// <param name="splashpackPath">Path to the exported .splashpack file.</param>
/// <param name="loaderPackPath">Path to the loading screen file (may be null).</param>
/// <param name="atlases">Texture atlases from the export pipeline.</param>
/// <param name="audioExportSizes">Array of ADPCM byte sizes per audio clip.</param>
/// <param name="fonts">Custom font descriptors.</param>
public static SceneMemoryReport Analyze(
string sceneName,
string splashpackPath,
string loaderPackPath,
SplashEdit.RuntimeCode.TextureAtlas[] atlases,
long[] audioExportSizes,
SplashEdit.RuntimeCode.PSXFontData[] fonts,
int triangleCount = 0)
{
var r = new SceneMemoryReport { sceneName = sceneName };
// ── File sizes ──
if (File.Exists(splashpackPath))
r.splashpackFileSize = new FileInfo(splashpackPath).Length;
if (!string.IsNullOrEmpty(loaderPackPath) && File.Exists(loaderPackPath))
r.loaderPackSize = new FileInfo(loaderPackPath).Length;
r.triangleCount = triangleCount;
// ── Parse splashpack header for counts and pixelDataOffset ──
if (File.Exists(splashpackPath))
{
try { ReadHeader(splashpackPath, r); }
catch (Exception e) { Debug.LogWarning($"Memory report: failed to read header: {e.Message}"); }
}
// ── Framebuffers ──
int fbW = SplashSettings.ResolutionWidth;
int fbH = SplashSettings.ResolutionHeight;
int fbCount = SplashSettings.DualBuffering ? 2 : 1;
r.framebufferSize = fbW * fbH * 2L * fbCount;
// ── VRAM: Texture atlases + CLUTs ──
if (atlases != null)
{
r.atlasCount = atlases.Length;
foreach (var atlas in atlases)
{
r.textureAtlasSize += atlas.Width * SplashEdit.RuntimeCode.TextureAtlas.Height * 2L;
foreach (var tex in atlas.ContainedTextures)
{
if (tex.ColorPalette != null)
{
r.clutCount++;
r.clutSize += tex.ColorPalette.Count * 2L;
}
}
}
}
// ── VRAM: Custom fonts ──
if (fonts != null)
{
foreach (var font in fonts)
{
if (font.TextureHeight > 0)
r.fontVramSize += 64L * font.TextureHeight * 2; // 4bpp = 64 hwords wide
}
}
// ── SPU: Audio ──
if (audioExportSizes != null)
{
r.audioClipCount = audioExportSizes.Length;
foreach (long sz in audioExportSizes)
r.audioDataSize += sz;
}
return r;
}
private static void ReadHeader(string path, SceneMemoryReport r)
{
using (var reader = new BinaryReader(File.OpenRead(path)))
{
if (reader.BaseStream.Length < 104) return;
// Magic + version (4 bytes)
reader.ReadBytes(4);
// luaFileCount(2) + gameObjectCount(2) + textureAtlasCount(2) + clutCount(2)
reader.ReadUInt16(); // luaFileCount
r.gameObjectCount = reader.ReadUInt16();
reader.ReadUInt16(); // textureAtlasCount
reader.ReadUInt16(); // clutCount
// Skip to pixelDataOffset at byte 100
reader.BaseStream.Seek(100, SeekOrigin.Begin);
uint pixelDataOffset = reader.ReadUInt32();
r.splashpackLiveSize = pixelDataOffset > 0 ? pixelDataOffset : r.splashpackFileSize;
}
}
}
}

View File

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

View File

@@ -0,0 +1,251 @@
using System.IO;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Manages all build-related paths for the SplashEdit pipeline.
/// All output goes outside Assets/ to avoid Unity import overhead.
/// </summary>
public static class SplashBuildPaths
{
/// <summary>
/// The build output directory at the Unity project root.
/// Contains exported splashpacks, manifest, compiled .ps-exe, ISO, build log.
/// </summary>
public static string BuildOutputDir =>
Path.Combine(ProjectRoot, "PSXBuild");
/// <summary>
/// The tools directory at the Unity project root.
/// Contains auto-downloaded tools like PCSX-Redux.
/// </summary>
public static string ToolsDir =>
Path.Combine(ProjectRoot, ".tools");
/// <summary>
/// PCSX-Redux install directory inside .tools/.
/// </summary>
public static string PCSXReduxDir =>
Path.Combine(ToolsDir, "pcsx-redux");
/// <summary>
/// Platform-specific PCSX-Redux binary path.
/// </summary>
public static string PCSXReduxBinary
{
get
{
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
return Path.Combine(PCSXReduxDir, "pcsx-redux.exe");
case RuntimePlatform.LinuxEditor:
return Path.Combine(PCSXReduxDir, "PCSX-Redux-HEAD-x86_64.AppImage");
default:
return Path.Combine(PCSXReduxDir, "pcsx-redux");
}
}
}
/// <summary>
/// The Unity project root (parent of Assets/).
/// </summary>
public static string ProjectRoot =>
Directory.GetParent(Application.dataPath).FullName;
/// <summary>
/// Path to the native psxsplash source.
/// First checks SplashSettings override, then looks for common locations.
/// </summary>
public static string NativeSourceDir
{
get
{
// 1. Check the user-configured path from SplashSettings
string custom = SplashSettings.NativeProjectPath;
if (!string.IsNullOrEmpty(custom) && Directory.Exists(custom))
return custom;
// 2. Look inside the Unity project's Assets/ folder (git clone location)
string assetsClone = Path.Combine(UnityEngine.Application.dataPath, "psxsplash");
if (Directory.Exists(assetsClone) && File.Exists(Path.Combine(assetsClone, "Makefile")))
return assetsClone;
// 3. Look for Native/ inside the package
string packageNative = Path.GetFullPath(
Path.Combine("Packages", "net.psxsplash.splashedit", "Native"));
if (Directory.Exists(packageNative))
return packageNative;
return "";
}
}
/// <summary>
/// The compiled .ps-exe output from the native build.
/// </summary>
public static string CompiledExePath =>
Path.Combine(BuildOutputDir, "psxsplash.ps-exe");
/// <summary>
/// The scene manifest file path.
/// </summary>
public static string ManifestPath =>
Path.Combine(BuildOutputDir, "manifest.bin");
/// <summary>
/// Build log file path.
/// </summary>
public static string BuildLogPath =>
Path.Combine(BuildOutputDir, "build.log");
/// <summary>
/// Gets the splashpack output path for a scene by index.
/// Uses a deterministic naming scheme: scene_0.splashpack, scene_1.splashpack, etc.
/// </summary>
public static string GetSceneSplashpackPath(int sceneIndex, string sceneName)
{
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.splashpack");
}
/// <summary>
/// Default license file path (SPLASHLICENSE.DAT) shipped in the package Data folder.
/// Resolved relative to the Unity project so it works on any machine.
/// </summary>
public static string DefaultLicenseFilePath =>
Path.GetFullPath(Path.Combine("Packages", "net.psxsplash.splashedit", "Data", "SPLASHLICENSE.DAT"));
/// <summary>
/// Gets the loader pack (loading screen) output path for a scene by index.
/// Uses a deterministic naming scheme: scene_0.loading, scene_1.loading, etc.
/// </summary>
public static string GetSceneLoaderPackPath(int sceneIndex, string sceneName)
{
return Path.Combine(BuildOutputDir, $"scene_{sceneIndex}.loading");
}
/// <summary>
/// ISO output path for release builds.
/// </summary>
public static string ISOOutputPath =>
Path.Combine(BuildOutputDir, "psxsplash.bin");
/// <summary>
/// CUE sheet path for release builds.
/// </summary>
public static string CUEOutputPath =>
Path.Combine(BuildOutputDir, "psxsplash.cue");
/// <summary>
/// XML catalog path used by mkpsxiso to build the ISO image.
/// </summary>
public static string ISOCatalogPath =>
Path.Combine(BuildOutputDir, "psxsplash.xml");
/// <summary>
/// SYSTEM.CNF file path generated for the ISO image.
/// The PS1 BIOS reads this to find and launch the executable.
/// </summary>
public static string SystemCnfPath =>
Path.Combine(BuildOutputDir, "SYSTEM.CNF");
/// <summary>
/// Checks if mkpsxiso is installed in the tools directory.
/// </summary>
public static bool IsMkpsxisoInstalled() => MkpsxisoDownloader.IsInstalled();
/// <summary>
/// Ensures the build output and tools directories exist.
/// Also appends entries to the project .gitignore if not present.
/// </summary>
public static void EnsureDirectories()
{
Directory.CreateDirectory(BuildOutputDir);
Directory.CreateDirectory(ToolsDir);
EnsureGitIgnore();
}
// ───── Lua bytecode compilation paths ─────
/// <summary>
/// Directory for Lua source files extracted during export.
/// </summary>
public static string LuaSrcDir =>
Path.Combine(BuildOutputDir, "lua_src");
/// <summary>
/// Directory for compiled Lua bytecode files.
/// </summary>
public static string LuaCompiledDir =>
Path.Combine(BuildOutputDir, "lua_compiled");
/// <summary>
/// Manifest file listing input/output pairs for the PS1 Lua compiler.
/// </summary>
public static string LuaManifestPath =>
Path.Combine(LuaSrcDir, "manifest.txt");
/// <summary>
/// Sentinel file written by luac_psx when compilation is complete.
/// Contains "OK" on success or "ERROR" on failure.
/// </summary>
public static string LuaDoneSentinel =>
Path.Combine(LuaSrcDir, "__done__");
/// <summary>
/// Path to the luac_psx PS1 compiler executable (built from tools/luac_psx/).
/// </summary>
public static string LuacPsxExePath =>
Path.Combine(NativeSourceDir, "tools", "luac_psx", "luac_psx.ps-exe");
/// <summary>
/// Path to the luac_psx tools directory (for building the compiler).
/// </summary>
public static string LuacPsxDir =>
Path.Combine(NativeSourceDir, "tools", "luac_psx");
/// <summary>
/// Checks if PCSX-Redux is installed in the tools directory.
/// </summary>
public static bool IsPCSXReduxInstalled()
{
return File.Exists(PCSXReduxBinary);
}
private static void EnsureGitIgnore()
{
string gitignorePath = Path.Combine(ProjectRoot, ".gitignore");
string[] entriesToAdd = new[] { "/PSXBuild/", "/.tools/" };
string existingContent = "";
if (File.Exists(gitignorePath))
{
existingContent = File.ReadAllText(gitignorePath);
}
bool modified = false;
string toAppend = "";
foreach (string entry in entriesToAdd)
{
// Check if entry already exists (exact line match)
if (!existingContent.Contains(entry))
{
if (!modified)
{
toAppend += "\n# SplashEdit build output\n";
modified = true;
}
toAppend += entry + "\n";
}
}
if (modified)
{
File.AppendAllText(gitignorePath, toAppend);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3988772ca929eb14ea3bee6b643de4d0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5540e6cbefeb70d48a0c1e3843719784

View File

@@ -0,0 +1,214 @@
using UnityEditor;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Enumerates the pipeline target for builds.
/// </summary>
public enum BuildTarget
{
Emulator, // PCSX-Redux with PCdrv
RealHardware, // Send .ps-exe over serial via Unirom
ISO // Build a CD image
}
/// <summary>
/// Enumerates the build configuration.
/// </summary>
public enum BuildMode
{
Debug,
Release
}
/// <summary>
/// Centralized EditorPrefs-backed settings for the SplashEdit pipeline.
/// All settings are project-scoped using a prefix derived from the project path.
/// </summary>
public static class SplashSettings
{
// Prefix all keys with project path hash to support multiple projects
internal static string Prefix => "SplashEdit_" + Application.dataPath.GetHashCode().ToString("X8") + "_";
// --- Build settings ---
public static BuildTarget Target
{
get => (BuildTarget)EditorPrefs.GetInt(Prefix + "Target", (int)BuildTarget.Emulator);
set => EditorPrefs.SetInt(Prefix + "Target", (int)value);
}
public static BuildMode Mode
{
get => (BuildMode)EditorPrefs.GetInt(Prefix + "Mode", (int)BuildMode.Release);
set => EditorPrefs.SetInt(Prefix + "Mode", (int)value);
}
// --- Toolchain paths ---
public static string NativeProjectPath
{
get => EditorPrefs.GetString(Prefix + "NativeProjectPath", "");
set => EditorPrefs.SetString(Prefix + "NativeProjectPath", value);
}
public static string MIPSToolchainPath
{
get => EditorPrefs.GetString(Prefix + "MIPSToolchainPath", "");
set => EditorPrefs.SetString(Prefix + "MIPSToolchainPath", value);
}
// --- PCSX-Redux ---
public static string PCSXReduxPath
{
get
{
string custom = EditorPrefs.GetString(Prefix + "PCSXReduxPath", "");
if (!string.IsNullOrEmpty(custom))
return custom;
// Fall back to auto-downloaded location
if (SplashBuildPaths.IsPCSXReduxInstalled())
return SplashBuildPaths.PCSXReduxBinary;
return "";
}
set => EditorPrefs.SetString(Prefix + "PCSXReduxPath", value);
}
public static string PCSXReduxPCdrvBase
{
get => EditorPrefs.GetString(Prefix + "PCSXReduxPCdrvBase", SplashBuildPaths.BuildOutputDir);
set => EditorPrefs.SetString(Prefix + "PCSXReduxPCdrvBase", value);
}
// --- Serial / Real Hardware ---
public static string SerialPort
{
get => EditorPrefs.GetString(Prefix + "SerialPort", "COM3");
set => EditorPrefs.SetString(Prefix + "SerialPort", value);
}
public static int SerialBaudRate
{
get => EditorPrefs.GetInt(Prefix + "SerialBaudRate", 115200);
set => EditorPrefs.SetInt(Prefix + "SerialBaudRate", value);
}
// --- VRAM Layout (hardcoded 320x240, dual-buffered, vertical) ---
public static int ResolutionWidth
{
get => 320;
set { } // no-op, hardcoded
}
public static int ResolutionHeight
{
get => 240;
set { } // no-op, hardcoded
}
public static bool DualBuffering
{
get => true;
set { } // no-op, hardcoded
}
public static bool VerticalLayout
{
get => true;
set { } // no-op, hardcoded
}
// --- Clean Build ---
public static bool CleanBuild
{
get => EditorPrefs.GetBool(Prefix + "CleanBuild", true);
set => EditorPrefs.SetBool(Prefix + "CleanBuild", value);
}
// --- Memory Overlay ---
/// <summary>
/// When enabled, compiles the runtime with a heap/RAM usage progress bar
/// and text overlay at the top-right corner of the screen.
/// Passes MEMOVERLAY=1 to the native Makefile.
/// </summary>
public static bool MemoryOverlay
{
get => EditorPrefs.GetBool(Prefix + "MemoryOverlay", false);
set => EditorPrefs.SetBool(Prefix + "MemoryOverlay", value);
}
// --- FPS Overlay ---
/// <summary>
/// When enabled, compiles the runtime with an FPS counter
/// and text overlay at the top-left corner of the screen.
/// Passes FPSOVERLAY=1 to the native Makefile.
/// </summary>
public static bool FpsOverlay
{
get => EditorPrefs.GetBool(Prefix + "FpsOverlay", false);
set => EditorPrefs.SetBool(Prefix + "FpsOverlay", value);
}
// --- Renderer sizes ---
public static int OtSize
{
get => EditorPrefs.GetInt(Prefix + "OtSize", 2048 * 4);
set => EditorPrefs.SetInt(Prefix + "OtSize", value);
}
public static int BumpSize
{
get => EditorPrefs.GetInt(Prefix + "BumpSize", 8096 * 16);
set => EditorPrefs.SetInt(Prefix + "BumpSize", value);
}
// --- Export settings ---
public static float DefaultGTEScaling
{
get => EditorPrefs.GetFloat(Prefix + "GTEScaling", 100f);
set => EditorPrefs.SetFloat(Prefix + "GTEScaling", 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", SplashBuildPaths.DefaultLicenseFilePath);
set => EditorPrefs.SetString(Prefix + "LicenseFilePath", value);
}
/// <summary>
/// Volume label for the ISO image (up to 31 characters, uppercase).
/// </summary>
public static string ISOVolumeLabel
{
get => EditorPrefs.GetString(Prefix + "ISOVolumeLabel", "PSXSPLASH");
set => EditorPrefs.SetString(Prefix + "ISOVolumeLabel", value);
}
/// <summary>
/// Resets all settings to defaults by deleting all prefixed keys.
/// </summary>
public static void ResetAll()
{
string[] keys = new[]
{
"Target", "Mode", "NativeProjectPath", "MIPSToolchainPath",
"PCSXReduxPath", "PCSXReduxPCdrvBase", "SerialPort", "SerialBaudRate",
"ResWidth", "ResHeight", "DualBuffering", "VerticalLayout",
"GTEScaling", "AutoValidate",
"LicenseFilePath", "ISOVolumeLabel",
"OtSize", "BumpSize"
};
foreach (string key in keys)
{
EditorPrefs.DeleteKey(Prefix + key);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4765dbe728569d84699a22347e7c14ff

View File

@@ -0,0 +1,549 @@
using System;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Uploads a .ps-exe to a PS1 running Unirom 8 via serial.
/// Implements the NOTPSXSerial / Unirom protocol:
/// Challenge/Response handshake → header → metadata → chunked data with checksums.
/// Reference: https://github.com/JonathanDotCel/NOTPSXSerial
/// </summary>
public static class UniromUploader
{
// Protocol constants
private const string CHALLENGE_SEND_EXE = "SEXE";
private const string RESPONSE_OK = "OKAY";
private const int CHUNK_SIZE = 2048;
private const int HEADER_SIZE = 0x800; // 2048
private const int SERIAL_TIMEOUT_MS = 5000;
// Protocol version — negotiated during handshake
private static int _protocolVersion = 1;
/// <summary>
/// Uploads a .ps-exe file to the PS1 via serial.
/// The PS1 must be at the Unirom shell prompt.
/// </summary>
public static bool UploadExe(string portName, int baudRate, string exePath, Action<string> log)
{
var port = DoUpload(portName, baudRate, exePath, log, installDebugHooks: false);
if (port == null) return false;
try { port.Close(); } catch { }
port.Dispose();
return true;
}
/// <summary>
/// Uploads a .ps-exe with Unirom debug hooks installed, using SBIN+JUMP
/// instead of SEXE to avoid BIOS Exec() clobbering the debug handler.
///
/// Flow: DEBG (install kernel-resident debug hooks) → SBIN (raw binary to address)
/// → JUMP (start execution at entry point). This bypasses BIOS Exec() entirely,
/// so the exception vector table patched by DEBG survives into the running program.
///
/// Returns the open SerialPort for the caller to use for PCDrv monitoring.
/// The caller takes ownership of the returned port.
/// </summary>
public static SerialPort UploadExeForPCdrv(string portName, int baudRate, string exePath, Action<string> log)
{
return DoUploadSBIN(portName, baudRate, exePath, log);
}
/// <summary>
/// Core SEXE upload implementation. Opens port, optionally sends DEBG, does SEXE upload.
/// Used by UploadExe() for simple uploads without PCDrv.
/// Returns the open SerialPort (caller must close/dispose when done).
/// Returns null on failure.
/// </summary>
private static SerialPort DoUpload(string portName, int baudRate, string exePath, Action<string> log, bool installDebugHooks)
{
if (!File.Exists(exePath))
{
log?.Invoke($"File not found: {exePath}");
return null;
}
byte[] exeData = File.ReadAllBytes(exePath);
log?.Invoke($"Uploading {Path.GetFileName(exePath)} ({exeData.Length} bytes)");
// Pad to 2048-byte sector boundary (required by Unirom)
int mod = exeData.Length % CHUNK_SIZE;
if (mod != 0)
{
int paddingRequired = CHUNK_SIZE - mod;
byte[] padded = new byte[exeData.Length + paddingRequired];
Buffer.BlockCopy(exeData, 0, padded, 0, exeData.Length);
exeData = padded;
log?.Invoke($"Padded to {exeData.Length} bytes (2048-byte boundary)");
}
_protocolVersion = 1;
SerialPort port = null;
try
{
port = new SerialPort(portName, baudRate)
{
ReadTimeout = SERIAL_TIMEOUT_MS,
WriteTimeout = SERIAL_TIMEOUT_MS,
StopBits = StopBits.Two,
Parity = Parity.None,
DataBits = 8,
Handshake = Handshake.None,
DtrEnable = true,
RtsEnable = true
};
port.Open();
// Drain any leftover bytes in the buffer
while (port.BytesToRead > 0)
port.ReadByte();
// ── Step 0 (PCDrv only): Install debug hooks while Unirom is still in command mode ──
if (installDebugHooks)
{
log?.Invoke("Installing debug hooks (DEBG)...");
if (!ChallengeResponse(port, "DEBG", "OKAY", log))
{
log?.Invoke("WARNING: DEBG failed. Is Unirom at the shell? PCDrv may not work.");
}
else
{
log?.Invoke("Debug hooks installed.");
}
Thread.Sleep(100);
while (port.BytesToRead > 0)
port.ReadByte();
}
// ── Step 1: Challenge/Response handshake ──
log?.Invoke("Sending SEXE challenge...");
if (!ChallengeResponse(port, CHALLENGE_SEND_EXE, RESPONSE_OK, log))
{
log?.Invoke("No response from Unirom. Is the PS1 at the Unirom shell?");
port.Close(); port.Dispose();
return null;
}
log?.Invoke($"Unirom responded (protocol V{_protocolVersion}). Starting transfer...");
// ── Step 2: Calculate checksum (skip first 0x800 header sector) ──
uint checksum = CalculateChecksum(exeData, skipFirstSector: true);
// ── Step 3: Send the 2048-byte header sector ──
port.Write(exeData, 0, HEADER_SIZE);
// ── Step 4: Send metadata ──
port.Write(exeData, 0x10, 4); // Jump/PC address
port.Write(exeData, 0x18, 4); // Base/write address
port.Write(BitConverter.GetBytes(exeData.Length - HEADER_SIZE), 0, 4); // Data length
port.Write(BitConverter.GetBytes(checksum), 0, 4); // Checksum
// ── Step 5: Send data chunks (skip first sector) ──
if (!WriteChunked(port, exeData, skipFirstSector: true, log))
{
log?.Invoke("Data transfer failed.");
port.Close(); port.Dispose();
return null;
}
log?.Invoke("Upload complete. Exe executing on PS1.");
return port;
}
catch (Exception ex)
{
log?.Invoke($"Upload failed: {ex.Message}");
if (port != null && port.IsOpen)
{
try { port.Close(); } catch { }
}
port?.Dispose();
return null;
}
}
/// <summary>
/// Uploads a .ps-exe using DEBG + SBIN + JUMP to preserve debug hooks.
///
/// Unlike SEXE which calls BIOS Exec() (reinitializing the exception vector table
/// and destroying DEBG's kernel-resident debug handler), SBIN writes raw bytes
/// directly to the target address and JUMP starts execution without touching
/// the BIOS. This preserves the break-instruction handler that PCDrv depends on.
///
/// Protocol:
/// 1. DEBG → OKAY: Install kernel-resident SIO debug stub
/// 2. SBIN → OKAY: addr(4 LE) + len(4 LE) + checksum(4 LE) + raw program data
/// 3. JUMP → OKAY: addr(4 LE) — jump to entry point
/// </summary>
private static SerialPort DoUploadSBIN(string portName, int baudRate, string exePath, Action<string> log)
{
if (!File.Exists(exePath))
{
log?.Invoke($"File not found: {exePath}");
return null;
}
byte[] exeData = File.ReadAllBytes(exePath);
log?.Invoke($"Uploading {Path.GetFileName(exePath)} ({exeData.Length} bytes) via SBIN+JUMP");
// Validate this is a PS-X EXE
if (exeData.Length < HEADER_SIZE + 4)
{
log?.Invoke("File too small to be a valid PS-X EXE.");
return null;
}
string magic = Encoding.ASCII.GetString(exeData, 0, 8);
if (!magic.StartsWith("PS-X EXE"))
{
log?.Invoke($"Not a PS-X EXE (magic: '{magic}')");
return null;
}
// Parse header
uint entryPoint = BitConverter.ToUInt32(exeData, 0x10); // PC / jump address
uint destAddr = BitConverter.ToUInt32(exeData, 0x18); // Copy destination
uint textSize = BitConverter.ToUInt32(exeData, 0x1C); // Text section size
log?.Invoke($"PS-X EXE: entry=0x{entryPoint:X8}, dest=0x{destAddr:X8}, textSz=0x{textSize:X}");
// Extract program data (everything after the 2048-byte header)
int progDataLen = exeData.Length - HEADER_SIZE;
byte[] progData = new byte[progDataLen];
Buffer.BlockCopy(exeData, HEADER_SIZE, progData, 0, progDataLen);
// Pad program data to 2048-byte boundary (required by Unirom chunked transfer)
int mod = progData.Length % CHUNK_SIZE;
if (mod != 0)
{
int paddingRequired = CHUNK_SIZE - mod;
byte[] padded = new byte[progData.Length + paddingRequired];
Buffer.BlockCopy(progData, 0, padded, 0, progData.Length);
progData = padded;
log?.Invoke($"Program data padded to {progData.Length} bytes");
}
_protocolVersion = 1;
SerialPort port = null;
try
{
port = new SerialPort(portName, baudRate)
{
ReadTimeout = SERIAL_TIMEOUT_MS,
WriteTimeout = SERIAL_TIMEOUT_MS,
StopBits = StopBits.Two,
Parity = Parity.None,
DataBits = 8,
Handshake = Handshake.None,
DtrEnable = true,
RtsEnable = true
};
port.Open();
// Drain any leftover bytes
while (port.BytesToRead > 0)
port.ReadByte();
// ── Step 1: DEBG — Install kernel-resident debug hooks ──
log?.Invoke("Installing debug hooks (DEBG)...");
if (!ChallengeResponse(port, "DEBG", "OKAY", log))
{
log?.Invoke("DEBG failed. Is Unirom at the shell?");
port.Close(); port.Dispose();
return null;
}
log?.Invoke("Debug hooks installed.");
// Drain + settle — Unirom may send extra bytes after DEBG
Thread.Sleep(100);
while (port.BytesToRead > 0)
port.ReadByte();
// ── Step 2: SBIN — Upload raw program data to target address ──
log?.Invoke($"Sending SBIN to 0x{destAddr:X8} ({progData.Length} bytes)...");
if (!ChallengeResponse(port, "SBIN", "OKAY", log))
{
log?.Invoke("SBIN failed. Unirom may not support this command.");
port.Close(); port.Dispose();
return null;
}
// SBIN metadata: address(4) + length(4) + checksum(4)
uint checksum = CalculateChecksum(progData, skipFirstSector: false);
port.Write(BitConverter.GetBytes(destAddr), 0, 4);
port.Write(BitConverter.GetBytes(progData.Length), 0, 4);
port.Write(BitConverter.GetBytes(checksum), 0, 4);
log?.Invoke($"SBIN metadata sent (checksum=0x{checksum:X8}). Sending data...");
// Send program data chunks
if (!WriteChunked(port, progData, skipFirstSector: false, log))
{
log?.Invoke("SBIN data transfer failed.");
port.Close(); port.Dispose();
return null;
}
log?.Invoke("SBIN upload complete.");
// Drain any residual
Thread.Sleep(100);
while (port.BytesToRead > 0)
port.ReadByte();
// ── Step 3: JUMP — Start execution at entry point ──
log?.Invoke($"Sending JUMP to 0x{entryPoint:X8}...");
if (!ChallengeResponse(port, "JUMP", "OKAY", log))
{
log?.Invoke("JUMP failed.");
port.Close(); port.Dispose();
return null;
}
// JUMP payload: just the address (4 bytes LE)
port.Write(BitConverter.GetBytes(entryPoint), 0, 4);
log?.Invoke("JUMP sent. Exe now running (debug hooks preserved).");
return port;
}
catch (Exception ex)
{
log?.Invoke($"Upload failed: {ex.Message}");
if (port != null && port.IsOpen)
{
try { port.Close(); } catch { }
}
port?.Dispose();
return null;
}
}
// ═══════════════════════════════════════════════════════════════
// Challenge / Response with protocol negotiation
// ═══════════════════════════════════════════════════════════════
private static bool ChallengeResponse(SerialPort port, string challenge, string expectedResponse, Action<string> log)
{
// Send the challenge
byte[] challengeBytes = Encoding.ASCII.GetBytes(challenge);
port.Write(challengeBytes, 0, challengeBytes.Length);
Thread.Sleep(50);
// Wait for the response with protocol negotiation
return WaitResponse(port, expectedResponse, log);
}
private static bool WaitResponse(SerialPort port, string expected, Action<string> log, int timeoutMs = 10000)
{
string buffer = "";
DateTime deadline = DateTime.Now.AddMilliseconds(timeoutMs);
while (DateTime.Now < deadline)
{
if (port.BytesToRead > 0)
{
buffer += (char)port.ReadByte();
// Keep buffer at 4 chars max (rolling window)
if (buffer.Length > 4)
buffer = buffer.Substring(buffer.Length - 4);
// Protocol V3 upgrade (DJB2 checksums)
// Always respond — Unirom re-offers V2/V3 for each command,
// and our protocolVersion may already be >1 from a prior DEBG exchange.
if (buffer == "OKV3")
{
log?.Invoke("Upgraded to protocol V3");
byte[] upv3 = Encoding.ASCII.GetBytes("UPV3");
port.Write(upv3, 0, upv3.Length);
_protocolVersion = 3;
buffer = "";
continue;
}
// Protocol V2 upgrade (per-chunk checksums)
if (buffer == "OKV2")
{
log?.Invoke("Upgraded to protocol V2");
byte[] upv2 = Encoding.ASCII.GetBytes("UPV2");
port.Write(upv2, 0, upv2.Length);
if (_protocolVersion < 2) _protocolVersion = 2;
buffer = "";
continue;
}
// Unsupported in debug mode
if (buffer == "UNSP")
{
log?.Invoke("Command not supported while Unirom is in debug mode.");
return false;
}
// Got the expected response
if (buffer == expected)
return true;
}
else
{
Thread.Sleep(1);
}
}
return false;
}
// ═══════════════════════════════════════════════════════════════
// Chunked data transfer with per-chunk checksum verification
// ═══════════════════════════════════════════════════════════════
private static bool WriteChunked(SerialPort port, byte[] data, bool skipFirstSector, Action<string> log)
{
int start = skipFirstSector ? CHUNK_SIZE : 0;
int totalDataBytes = data.Length - start;
int numChunks = (totalDataBytes + CHUNK_SIZE - 1) / CHUNK_SIZE;
int chunkIndex = 0;
for (int offset = start; offset < data.Length; )
{
// Determine chunk size (last chunk may be smaller)
int thisChunk = Math.Min(CHUNK_SIZE, data.Length - offset);
// Calculate per-chunk checksum (simple byte sum for V2, also works for V1)
ulong chunkChecksum = 0;
for (int j = 0; j < thisChunk; j++)
chunkChecksum += data[offset + j];
// Send the chunk
port.Write(data, offset, thisChunk);
// Wait for bytes to drain
while (port.BytesToWrite > 0)
Thread.Sleep(0);
chunkIndex++;
// Progress report every 10 chunks or on last chunk
if (chunkIndex % 10 == 0 || offset + thisChunk >= data.Length)
{
int sent = offset + thisChunk - start;
int pct = totalDataBytes > 0 ? sent * 100 / totalDataBytes : 100;
log?.Invoke($"Upload: {pct}% ({sent}/{totalDataBytes})");
}
// Protocol V2/V3: per-chunk checksum verification
if (_protocolVersion >= 2)
{
if (!HandleChunkAck(port, chunkChecksum, data, offset, thisChunk, log, out bool retry))
{
return false;
}
if (retry)
continue; // Don't advance offset — resend this chunk
}
offset += thisChunk;
}
return true;
}
/// <summary>
/// Handles the per-chunk CHEK/MORE/ERR! exchange for protocol V2+.
/// </summary>
private static bool HandleChunkAck(SerialPort port, ulong chunkChecksum, byte[] data, int offset, int chunkSize, Action<string> log, out bool retry)
{
retry = false;
// Wait for "CHEK" request from Unirom
string cmdBuffer = "";
DateTime deadline = DateTime.Now.AddMilliseconds(SERIAL_TIMEOUT_MS);
while (DateTime.Now < deadline)
{
if (port.BytesToRead > 0)
{
cmdBuffer += (char)port.ReadByte();
if (cmdBuffer.Length > 4)
cmdBuffer = cmdBuffer.Substring(cmdBuffer.Length - 4);
if (cmdBuffer == "CHEK")
break;
}
else
{
Thread.Sleep(1);
}
}
if (cmdBuffer != "CHEK")
{
log?.Invoke("Timeout waiting for CHEK from Unirom");
return false;
}
// Send the chunk checksum (4 bytes, little-endian)
port.Write(BitConverter.GetBytes((uint)chunkChecksum), 0, 4);
Thread.Sleep(1);
// Wait for MORE (ok) or ERR! (resend)
cmdBuffer = "";
deadline = DateTime.Now.AddMilliseconds(SERIAL_TIMEOUT_MS);
while (DateTime.Now < deadline)
{
if (port.BytesToRead > 0)
{
cmdBuffer += (char)port.ReadByte();
if (cmdBuffer.Length > 4)
cmdBuffer = cmdBuffer.Substring(cmdBuffer.Length - 4);
if (cmdBuffer == "MORE")
return true;
if (cmdBuffer == "ERR!")
{
log?.Invoke("Checksum error — retrying chunk...");
retry = true;
return true;
}
}
else
{
Thread.Sleep(1);
}
}
log?.Invoke("Timeout waiting for MORE/ERR! from Unirom");
return false;
}
// ═══════════════════════════════════════════════════════════════
// Checksum calculation
// ═══════════════════════════════════════════════════════════════
private static uint CalculateChecksum(byte[] data, bool skipFirstSector)
{
int start = skipFirstSector ? HEADER_SIZE : 0;
if (_protocolVersion == 3)
{
// DJB2 hash
uint hash = 5381;
for (int i = start; i < data.Length; i++)
hash = ((hash << 5) + hash) ^ data[i];
return hash;
}
else
{
// Simple byte sum
uint sum = 0;
for (int i = start; i < data.Length; i++)
sum += data[i];
return sum;
}
}
}
}

View File

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

View File

@@ -0,0 +1,39 @@
using UnityEditor;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Automatically opens the SplashEdit Control Panel on the first editor
/// session if the MIPS toolchain has not been installed yet.
/// </summary>
[InitializeOnLoad]
public static class DependencyCheckInitializer
{
private const string SessionKey = "SplashEditOpenedThisSession";
static DependencyCheckInitializer()
{
EditorApplication.update += OpenControlPanelOnStart;
}
private static void OpenControlPanelOnStart()
{
EditorApplication.update -= OpenControlPanelOnStart;
if (SessionState.GetBool(SessionKey, false))
return;
SessionState.SetBool(SessionKey, true);
// Only auto-open the Control Panel when the toolchain is missing
bool toolchainReady = ToolchainChecker.IsToolAvailable("mips") ||
ToolchainChecker.IsToolAvailable("mipsel-none-elf-gcc") ||
ToolchainChecker.IsToolAvailable("mipsel-linux-gnu-gcc");
if (!toolchainReady)
{
SplashControlPanel.ShowWindow();
}
}
}
}

View File

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

View File

@@ -1,6 +1,7 @@
fileFormatVersion: 2
guid: 1944ac962a00b23c2a880b5134cdc7ab
TextScriptImporter:
guid: 0279126b700b37d4485c1f4f1ae44e54
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:

View File

@@ -0,0 +1,122 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXInteractable component.
/// </summary>
[CustomEditor(typeof(PSXInteractable))]
public class PSXInteractableEditor : UnityEditor.Editor
{
private bool _interactionFoldout = true;
private bool _advancedFoldout = false;
private SerializedProperty _interactionRadius;
private SerializedProperty _interactButton;
private SerializedProperty _isRepeatable;
private SerializedProperty _cooldownFrames;
private SerializedProperty _showPrompt;
private SerializedProperty _promptCanvasName;
private SerializedProperty _requireLineOfSight;
private static readonly string[] ButtonNames =
{
"Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left",
"L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"
};
private void OnEnable()
{
_interactionRadius = serializedObject.FindProperty("interactionRadius");
_interactButton = serializedObject.FindProperty("interactButton");
_isRepeatable = serializedObject.FindProperty("isRepeatable");
_cooldownFrames = serializedObject.FindProperty("cooldownFrames");
_showPrompt = serializedObject.FindProperty("showPrompt");
_promptCanvasName = serializedObject.FindProperty("promptCanvasName");
_requireLineOfSight = serializedObject.FindProperty("requireLineOfSight");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.BeginHorizontal();
GUILayout.Label(EditorGUIUtility.IconContent("d_Selectable Icon"), GUILayout.Width(30), GUILayout.Height(30));
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("PSX Interactable", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.LabelField("Player interaction trigger for PS1", PSXEditorStyles.RichLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
_interactionFoldout = PSXEditorStyles.DrawFoldoutCard("Interaction Settings", _interactionFoldout, () =>
{
EditorGUILayout.PropertyField(_interactionRadius);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Interact Button");
_interactButton.intValue = EditorGUILayout.Popup(_interactButton.intValue, ButtonNames);
EditorGUILayout.EndHorizontal();
EditorGUILayout.PropertyField(_isRepeatable);
if (_isRepeatable.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_cooldownFrames, new GUIContent("Cooldown (frames)"));
float seconds = _cooldownFrames.intValue / 60f;
EditorGUILayout.LabelField($"~ {seconds:F2} seconds at 60fps", EditorStyles.miniLabel);
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(_showPrompt, new GUIContent("Show Prompt Canvas"));
if (_showPrompt.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_promptCanvasName, new GUIContent("Canvas Name"));
if (string.IsNullOrEmpty(_promptCanvasName.stringValue))
{
EditorGUILayout.HelpBox(
"Enter the name of a PSXCanvas that will be shown when the player is in range and hidden when they leave.",
MessageType.Info);
}
if (_promptCanvasName.stringValue != null && _promptCanvasName.stringValue.Length > 15)
{
EditorGUILayout.HelpBox("Canvas name is limited to 15 characters.", MessageType.Warning);
}
EditorGUI.indentLevel--;
}
});
EditorGUILayout.Space(2);
_advancedFoldout = PSXEditorStyles.DrawFoldoutCard("Advanced", _advancedFoldout, () =>
{
EditorGUILayout.PropertyField(_requireLineOfSight,
new GUIContent("Require Facing",
"Player must be facing the object to interact. Uses a forward-direction check."));
});
EditorGUILayout.Space(4);
// Lua events card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Lua Events", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.LabelField("onInteract", PSXEditorStyles.RichLabel);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7bd9caaf5a0cb90409cf0acdf17d8d89

View File

@@ -0,0 +1,271 @@
// I raged that my scrollwheel was broken while writing this and that's why it's 2 files.
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for PSXAudioClip component.
/// </summary>
[CustomEditor(typeof(PSXAudioClip))]
public class PSXAudioClipEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Audio Clip", PSXEditorStyles.CardHeaderStyle);
PSXAudioClip audioClip = (PSXAudioClip)target;
EditorGUILayout.BeginHorizontal();
if (audioClip.Clip != null)
PSXEditorStyles.DrawStatusBadge("Clip Set", PSXEditorStyles.Success, 70);
else
PSXEditorStyles.DrawStatusBadge("No Clip", PSXEditorStyles.Warning, 70);
if (audioClip.Loop)
PSXEditorStyles.DrawStatusBadge("Loop", PSXEditorStyles.AccentCyan, 50);
EditorGUILayout.EndHorizontal();
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Clip Settings", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("ClipName"), new GUIContent("Clip Name",
"Name used to identify this clip in Lua (Audio.Play(\"name\"))."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Clip"), new GUIContent("Audio Clip",
"Unity AudioClip to convert to PS1 SPU ADPCM format."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("SampleRate"), new GUIContent("Sample Rate",
"Target sample rate for the PS1 (lower = smaller, max 44100)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Loop"), new GUIContent("Loop",
"Whether this clip should loop when played."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("DefaultVolume"), new GUIContent("Volume",
"Default playback volume (0-127)."));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Info card
if (audioClip.Clip != null)
{
PSXEditorStyles.BeginCard();
float duration = audioClip.Clip.length;
int srcRate = audioClip.Clip.frequency;
EditorGUILayout.LabelField(
$"Source: {srcRate} Hz, {duration:F2}s, {audioClip.Clip.channels}ch\n" +
$"Target: {audioClip.SampleRate} Hz SPU ADPCM",
PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXPlayer component.
/// </summary>
[CustomEditor(typeof(PSXPlayer))]
public class PSXPlayerEditor : Editor
{
private bool _dimensionsFoldout = true;
private bool _movementFoldout = true;
private bool _navigationFoldout = true;
private bool _physicsFoldout = true;
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Player", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.LabelField("First-person player controller for PS1", PSXEditorStyles.RichLabel);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Dimensions
_dimensionsFoldout = PSXEditorStyles.DrawFoldoutCard("Player Dimensions", _dimensionsFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("playerHeight"), new GUIContent("Height",
"Camera eye height above the player's feet."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("playerRadius"), new GUIContent("Radius",
"Collision radius for wall sliding."));
});
EditorGUILayout.Space(2);
// Movement
_movementFoldout = PSXEditorStyles.DrawFoldoutCard("Movement", _movementFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("moveSpeed"), new GUIContent("Walk Speed",
"Walk speed in world units per second."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("sprintSpeed"), new GUIContent("Sprint Speed",
"Sprint speed in world units per second."));
});
EditorGUILayout.Space(2);
// Navigation
_navigationFoldout = PSXEditorStyles.DrawFoldoutCard("Navigation", _navigationFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("maxStepHeight"), new GUIContent("Max Step Height",
"Maximum height the agent can step up."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("walkableSlopeAngle"), new GUIContent("Walkable Slope",
"Maximum walkable slope angle in degrees."));
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("navCellSize"), new GUIContent("Cell Size (XZ)",
"Voxel size in XZ plane (smaller = more accurate but slower)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("navCellHeight"), new GUIContent("Cell Height",
"Voxel height (smaller = more accurate vertical resolution)."));
});
EditorGUILayout.Space(2);
// Jump & Gravity
_physicsFoldout = PSXEditorStyles.DrawFoldoutCard("Jump & Gravity", _physicsFoldout, () =>
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("jumpHeight"), new GUIContent("Jump Height",
"Peak jump height in world units."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("gravity"), new GUIContent("Gravity",
"Downward acceleration in world units per second squared."));
});
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXPortalLink component.
/// </summary>
[CustomEditor(typeof(PSXPortalLink))]
public class PSXPortalLinkEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
PSXPortalLink portal = (PSXPortalLink)target;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Portal Link", PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.BeginHorizontal();
bool valid = portal.RoomA != null && portal.RoomB != null && portal.RoomA != portal.RoomB;
if (valid)
PSXEditorStyles.DrawStatusBadge("Valid", PSXEditorStyles.Success, 55);
else
PSXEditorStyles.DrawStatusBadge("Invalid", PSXEditorStyles.Error, 60);
EditorGUILayout.EndHorizontal();
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Room references card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Connected Rooms", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("RoomA"), new GUIContent("Room A",
"First room connected by this portal."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("RoomB"), new GUIContent("Room B",
"Second room connected by this portal."));
// Validation warnings
if (portal.RoomA == null || portal.RoomB == null)
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Both Room A and Room B must be assigned for export.", PSXEditorStyles.InfoBox);
}
else if (portal.RoomA == portal.RoomB)
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Room A and Room B must be different rooms.", PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Portal size card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Portal Dimensions", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("PortalSize"), new GUIContent("Size (W, H)",
"Size of the portal opening (width, height) in world units."));
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXRoom component.
/// </summary>
[CustomEditor(typeof(PSXRoom))]
public class PSXRoomEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
PSXRoom room = (PSXRoom)target;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Room", PSXEditorStyles.CardHeaderStyle);
if (!string.IsNullOrEmpty(room.RoomName))
EditorGUILayout.LabelField(room.RoomName, PSXEditorStyles.RichLabel);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Room Settings", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("RoomName"), new GUIContent("Room Name",
"Optional display name for this room (used in editor gizmos)."));
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("VolumeSize"), new GUIContent("Volume Size",
"Size of the room volume in local space."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("VolumeOffset"), new GUIContent("Volume Offset",
"Offset of the volume center relative to the transform position."));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Info card
PSXEditorStyles.BeginCard();
Bounds wb = room.GetWorldBounds();
Vector3 size = wb.size;
EditorGUILayout.LabelField(
$"World bounds: {size.x:F1} x {size.y:F1} x {size.z:F1}",
PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3fd7a7bcc7d0ff841b158f2744d48010

View File

@@ -0,0 +1,617 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
// --- Scene Preview Gizmos ---
// A single canvas-level gizmo draws all children in hierarchy order
// so depth stacking is correct (last child in hierarchy renders on top).
public static class PSXUIGizmos
{
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected)]
static void DrawCanvasGizmo(PSXCanvas canvas, GizmoType gizmoType)
{
RectTransform canvasRt = canvas.GetComponent<RectTransform>();
if (canvasRt == null) return;
bool canvasSelected = (gizmoType & GizmoType.Selected) != 0;
// Canvas border
Vector3[] canvasCorners = new Vector3[4];
canvasRt.GetWorldCorners(canvasCorners);
Color border = canvasSelected ? Color.yellow : new Color(1, 1, 0, 0.3f);
Handles.DrawSolidRectangleWithOutline(canvasCorners, Color.clear, border);
// Draw all children in hierarchy order (first child = back, last child = front)
var children = canvas.GetComponentsInChildren<Transform>(true).Reverse();
foreach (var child in children)
{
if (child == canvas.transform) continue;
bool childSelected = Selection.Contains(child.gameObject);
var box = child.GetComponent<PSXUIBox>();
if (box != null) { DrawBox(box, childSelected); continue; }
var image = child.GetComponent<PSXUIImage>();
if (image != null) { DrawImage(image, childSelected); continue; }
var text = child.GetComponent<PSXUIText>();
if (text != null) { DrawText(text, childSelected); continue; }
var bar = child.GetComponent<PSXUIProgressBar>();
if (bar != null) { DrawProgressBar(bar, childSelected); continue; }
}
// Canvas label when selected
if (canvasSelected)
{
Vector2 res = PSXCanvas.PSXResolution;
Vector3 topMid = (canvasCorners[1] + canvasCorners[2]) * 0.5f;
string label = $"PSX Canvas: {canvas.CanvasName} ({res.x}x{res.y})";
GUIStyle style = new GUIStyle(EditorStyles.boldLabel);
style.normal.textColor = Color.yellow;
Handles.Label(topMid, label, style);
}
}
static void DrawBox(PSXUIBox box, bool selected)
{
RectTransform rt = box.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Color fill = box.BoxColor;
fill.a = selected ? 1f : 0.9f;
Color borderColor = selected ? Color.white : new Color(1, 1, 1, 0.5f);
Handles.DrawSolidRectangleWithOutline(corners, fill, borderColor);
}
static void DrawImage(PSXUIImage image, bool selected)
{
RectTransform rt = image.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
if (image.SourceTexture != null)
{
Color tint = image.TintColor;
tint.a = selected ? 1f : 0.9f;
Handles.DrawSolidRectangleWithOutline(corners, tint * 0.3f, tint);
Handles.BeginGUI();
Vector2 min = HandleUtility.WorldToGUIPoint(corners[0]);
Vector2 max = HandleUtility.WorldToGUIPoint(corners[2]);
Rect screenRect = new Rect(
Mathf.Min(min.x, max.x), Mathf.Min(min.y, max.y),
Mathf.Abs(max.x - min.x), Mathf.Abs(max.y - min.y));
if (screenRect.width > 2 && screenRect.height > 2)
{
GUI.color = new Color(tint.r, tint.g, tint.b, selected ? 1f : 0.9f);
GUI.DrawTexture(screenRect, image.SourceTexture, ScaleMode.StretchToFill);
GUI.color = Color.white;
}
Handles.EndGUI();
}
else
{
Color fill = new Color(0.4f, 0.4f, 0.8f, selected ? 0.8f : 0.6f);
Handles.DrawSolidRectangleWithOutline(corners, fill, Color.cyan);
}
}
static void DrawText(PSXUIText text, bool selected)
{
RectTransform rt = text.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Color borderColor = text.TextColor;
borderColor.a = selected ? 1f : 0.7f;
Color fill = new Color(0, 0, 0, selected ? 0.6f : 0.4f);
Handles.DrawSolidRectangleWithOutline(corners, fill, borderColor);
string label = string.IsNullOrEmpty(text.DefaultText) ? "[empty]" : text.DefaultText;
PSXFontAsset font = text.GetEffectiveFont();
int glyphW = font != null ? font.GlyphWidth : 8;
int glyphH = font != null ? font.GlyphHeight : 16;
Handles.BeginGUI();
Vector2 topLeft = HandleUtility.WorldToGUIPoint(corners[1]);
Vector2 botRight = HandleUtility.WorldToGUIPoint(corners[3]);
float rectScreenW = Mathf.Abs(botRight.x - topLeft.x);
float rectW = rt.rect.width;
float psxPixelScale = (rectW > 0.01f) ? rectScreenW / rectW : 1f;
float guiX = Mathf.Min(topLeft.x, botRight.x);
float guiY = Mathf.Min(topLeft.y, botRight.y);
float guiW = Mathf.Abs(botRight.x - topLeft.x);
float guiH = Mathf.Abs(botRight.y - topLeft.y);
Color tintColor = text.TextColor;
tintColor.a = selected ? 1f : 0.8f;
if (font != null && font.FontTexture != null && font.SourceFont != null)
{
Texture2D fontTex = font.FontTexture;
int glyphsPerRow = font.GlyphsPerRow;
float cellScreenH = glyphH * psxPixelScale;
float cursorX = guiX;
GUI.color = tintColor;
foreach (char ch in label)
{
if (ch < 32 || ch > 126) continue;
int charIdx = ch - 32;
int col = charIdx % glyphsPerRow;
int row = charIdx / glyphsPerRow;
float advance = glyphW;
if (font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length)
advance = font.AdvanceWidths[charIdx];
if (ch != ' ')
{
float uvX = (float)(col * glyphW) / fontTex.width;
float uvY = 1f - (float)((row + 1) * glyphH) / fontTex.height;
float uvW = (float)glyphW / fontTex.width;
float uvH = (float)glyphH / fontTex.height;
float spriteScreenW = advance * psxPixelScale;
Rect screenRect = new Rect(cursorX, guiY, spriteScreenW, cellScreenH);
float uvWScaled = uvW * (advance / glyphW);
Rect uvRect = new Rect(uvX, uvY, uvWScaled, uvH);
if (screenRect.xMax > guiX && screenRect.x < guiX + guiW)
GUI.DrawTextureWithTexCoords(screenRect, fontTex, uvRect);
}
cursorX += advance * psxPixelScale;
}
GUI.color = Color.white;
}
else
{
int fSize = Mathf.Clamp(Mathf.RoundToInt(glyphH * psxPixelScale * 0.75f), 6, 72);
GUIStyle style = new GUIStyle(EditorStyles.label);
style.normal.textColor = tintColor;
style.alignment = TextAnchor.UpperLeft;
style.fontSize = fSize;
style.wordWrap = false;
style.clipping = TextClipping.Clip;
Rect guiRect = new Rect(guiX, guiY, guiW, guiH);
GUI.color = tintColor;
GUI.Label(guiRect, label, style);
GUI.color = Color.white;
}
Handles.EndGUI();
}
static void DrawProgressBar(PSXUIProgressBar bar, bool selected)
{
RectTransform rt = bar.GetComponent<RectTransform>();
if (rt == null) return;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Color bgColor = bar.BackgroundColor;
bgColor.a = selected ? 1f : 0.9f;
Handles.DrawSolidRectangleWithOutline(corners, bgColor, selected ? Color.white : new Color(1, 1, 1, 0.5f));
float t = bar.InitialValue / 100f;
if (t > 0.001f)
{
Vector3[] fillCorners = new Vector3[4];
fillCorners[0] = corners[0];
fillCorners[1] = corners[1];
fillCorners[2] = Vector3.Lerp(corners[1], corners[2], t);
fillCorners[3] = Vector3.Lerp(corners[0], corners[3], t);
Color fillColor = bar.FillColor;
fillColor.a = selected ? 1f : 0.9f;
Handles.DrawSolidRectangleWithOutline(fillCorners, fillColor, Color.clear);
}
}
}
/// <summary>
/// Custom inspector for PSXCanvas component.
/// Shows canvas name, visibility, sort order, font, and a summary of child elements.
/// </summary>
[CustomEditor(typeof(PSXCanvas))]
public class PSXCanvasEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
Vector2 res = PSXCanvas.PSXResolution;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField($"PSX Canvas ({res.x}x{res.y})", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("canvasName"), new GUIContent("Canvas Name",
"Name used from Lua: UI.FindCanvas(\"name\"). Max 24 chars."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"), new GUIContent("Start Visible",
"Whether the canvas is visible when the scene loads."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("sortOrder"), new GUIContent("Sort Order",
"Render priority (0 = back, 255 = front)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("defaultFont"), new GUIContent("Default Font",
"Default custom font for text elements. If empty, uses built-in system font (8x16)."));
PSXEditorStyles.DrawSeparator(6, 6);
if (GUILayout.Button($"Reset Canvas to {res.x}x{res.y}", PSXEditorStyles.SecondaryButton))
{
PSXCanvas.InvalidateResolutionCache();
((PSXCanvas)target).ConfigureCanvas();
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Element summary card
PSXCanvas canvas = (PSXCanvas)target;
int imageCount = canvas.GetComponentsInChildren<PSXUIImage>(true).Length;
int boxCount = canvas.GetComponentsInChildren<PSXUIBox>(true).Length;
int textCount = canvas.GetComponentsInChildren<PSXUIText>(true).Length;
int progressCount = canvas.GetComponentsInChildren<PSXUIProgressBar>(true).Length;
int total = imageCount + boxCount + textCount + progressCount;
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField(
$"Elements: {total} total\n" +
$" Images: {imageCount} | Boxes: {boxCount}\n" +
$" Texts: {textCount} | Progress Bars: {progressCount}",
PSXEditorStyles.InfoBox);
if (total > 128)
EditorGUILayout.LabelField("PS1 UI system supports max 128 elements total across all canvases.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIImage component.
/// </summary>
[CustomEditor(typeof(PSXUIImage))]
public class PSXUIImageEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Image", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"), new GUIContent("Element Name",
"Name used from Lua: UI.FindElement(canvas, \"name\"). Max 24 chars."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("sourceTexture"), new GUIContent("Source Texture",
"Texture to quantize and pack into VRAM."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("bitDepth"), new GUIContent("Bit Depth",
"VRAM storage depth. 4-bit = 16 colors, 8-bit = 256 colors, 16-bit = direct color."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("tintColor"), new GUIContent("Tint Color",
"Color multiply applied to the image (white = no tint)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.EndCard();
// Texture size warning
PSXUIImage img = (PSXUIImage)target;
if (img.SourceTexture != null)
{
if (img.SourceTexture.width > 256 || img.SourceTexture.height > 256)
{
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Texture exceeds 256x256. It will be resized during export.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
}
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIBox component.
/// </summary>
[CustomEditor(typeof(PSXUIBox))]
public class PSXUIBoxEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Box", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("boxColor"), new GUIContent("Box Color",
"Solid fill color rendered as a FastFill primitive."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIText component.
/// </summary>
[CustomEditor(typeof(PSXUIText))]
public class PSXUITextEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Text", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("defaultText"), new GUIContent("Default Text",
"Initial text content. Max 63 chars. Change at runtime via UI.SetText()."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("textColor"), new GUIContent("Text Color",
"Text render color."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontOverride"), new GUIContent("Font Override",
"Custom font for this text element. If empty, uses the canvas default font or built-in system font (8x16)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Warnings and info
PSXUIText txt = (PSXUIText)target;
if (!string.IsNullOrEmpty(txt.DefaultText) && txt.DefaultText.Length > 63)
{
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Text exceeds 63 characters and will be truncated.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
}
PSXEditorStyles.BeginCard();
PSXFontAsset font = txt.GetEffectiveFont();
if (font != null)
{
EditorGUILayout.LabelField(
$"Font: {font.name} ({font.GlyphWidth}x{font.GlyphHeight} glyphs)",
PSXEditorStyles.InfoBox);
}
else
{
EditorGUILayout.LabelField("Using built-in system font (8x16 glyphs).", PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXUIProgressBar component.
/// </summary>
[CustomEditor(typeof(PSXUIProgressBar))]
public class PSXUIProgressBarEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX UI Progress Bar", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(serializedObject.FindProperty("elementName"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("backgroundColor"), new GUIContent("Background Color",
"Color shown behind the fill bar."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("fillColor"), new GUIContent("Fill Color",
"Color of the progress fill."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("initialValue"), new GUIContent("Initial Value",
"Starting progress (0-100). Change via UI.SetProgress()."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible"));
PSXEditorStyles.DrawSeparator(6, 4);
// Preview bar
PSXUIProgressBar bar = (PSXUIProgressBar)target;
PSXEditorStyles.DrawProgressBar(bar.InitialValue / 100f, "Preview", bar.FillColor, 16);
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// Custom inspector for PSXFontAsset ScriptableObject.
/// Shows font metrics, auto-conversion from TTF/OTF, and a preview of the glyph layout.
/// </summary>
[CustomEditor(typeof(PSXFontAsset))]
public class PSXFontAssetEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
PSXFontAsset font = (PSXFontAsset)target;
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Font Asset", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Source font card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Auto-Convert from Font", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("sourceFont"), new GUIContent("Source Font (TTF/OTF)",
"Assign a Unity Font (TrueType/OpenType). Click 'Generate Bitmap' to rasterize it.\n" +
"Glyph cell dimensions are auto-derived from the font metrics."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontSize"), new GUIContent("Font Size",
"Pixel height for rasterization. Determines glyph cell height.\n" +
"Glyph cell width is auto-derived from the widest character.\n" +
"Changing this and re-generating will update both the bitmap AND the glyph dimensions."));
if (font.SourceFont != null)
{
EditorGUILayout.Space(2);
if (GUILayout.Button("Generate Bitmap from Font", PSXEditorStyles.PrimaryButton, GUILayout.Height(28)))
{
Undo.RecordObject(font, "Generate PSX Font Bitmap");
font.GenerateBitmapFromFont();
}
if (font.FontTexture == null)
EditorGUILayout.LabelField(
"Click 'Generate Bitmap' to create the font texture.\n" +
"If generation fails, check that the font's import settings have " +
"'Character' set to 'ASCII Default Set'.", PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Manual bitmap card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Manual Bitmap Source", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
EditorGUILayout.PropertyField(serializedObject.FindProperty("fontTexture"), new GUIContent("Font Texture",
"256px wide bitmap. Glyphs in ASCII order from 0x20 (space). " +
"Transparent = background, opaque = foreground."));
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Glyph metrics card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Glyph Metrics", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
if (font.SourceFont != null && font.FontTexture != null)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.IntField(new GUIContent("Glyph Width", "Auto-derived from font. Re-generate to change."), font.GlyphWidth);
EditorGUILayout.IntField(new GUIContent("Glyph Height", "Auto-derived from font. Re-generate to change."), font.GlyphHeight);
EditorGUI.EndDisabledGroup();
EditorGUILayout.LabelField("Glyph dimensions are auto-derived when generating from a font.\n" +
"Change the Font Size slider and re-generate to adjust.", PSXEditorStyles.InfoBox);
}
else
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphWidth"), new GUIContent("Glyph Width",
"Width of each glyph cell in pixels. Must divide 256 evenly (4, 8, 16, or 32)."));
EditorGUILayout.PropertyField(serializedObject.FindProperty("glyphHeight"), new GUIContent("Glyph Height",
"Height of each glyph cell in pixels."));
}
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Layout info card
PSXEditorStyles.BeginCard();
int glyphsPerRow = font.GlyphsPerRow;
int rowCount = font.RowCount;
int totalH = font.TextureHeight;
int vramBytes = totalH * 128;
EditorGUILayout.LabelField(
$"Layout: {glyphsPerRow} glyphs/row, {rowCount} rows\n" +
$"Texture: 256 x {totalH} pixels (4bpp)\n" +
$"VRAM: {vramBytes} bytes ({vramBytes / 1024f:F1} KB)\n" +
$"Glyph cell: {font.GlyphWidth} x {font.GlyphHeight}",
PSXEditorStyles.InfoBox);
if (font.AdvanceWidths != null && font.AdvanceWidths.Length >= 95)
{
int minAdv = 255, maxAdv = 0;
for (int i = 1; i < 95; i++)
{
if (font.AdvanceWidths[i] < minAdv) minAdv = font.AdvanceWidths[i];
if (font.AdvanceWidths[i] > maxAdv) maxAdv = font.AdvanceWidths[i];
}
EditorGUILayout.LabelField(
$"Advance widths: {minAdv}-{maxAdv}px (proportional spacing stored)",
PSXEditorStyles.InfoBox);
}
else if (font.FontTexture != null)
{
EditorGUILayout.LabelField(
"No advance widths stored. Click 'Generate Bitmap' to compute them.",
PSXEditorStyles.InfoBox);
}
PSXEditorStyles.EndCard();
// Validation
if (font.FontTexture != null)
{
if (font.FontTexture.width != 256)
{
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField($"Font texture must be 256 pixels wide (currently {font.FontTexture.width}).", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
if (256 % font.GlyphWidth != 0)
{
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField($"Glyph width ({font.GlyphWidth}) must divide 256 evenly. " +
"Valid values: 4, 8, 16, 32.", PSXEditorStyles.InfoBox);
PSXEditorStyles.EndCard();
}
// Preview
EditorGUILayout.Space(4);
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("Preview", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.DrawSeparator(2, 4);
Rect previewRect = EditorGUILayout.GetControlRect(false, 64);
GUI.DrawTexture(previewRect, font.FontTexture, ScaleMode.ScaleToFit);
PSXEditorStyles.EndCard();
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 385b3916e29dc0e48b2866851d1fc1a9

View File

@@ -0,0 +1,43 @@
using SplashEdit.RuntimeCode;
using UnityEditor;
using UnityEngine;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Custom inspector for <see cref="LuaFile"/> assets that displays the
/// embedded Lua source code in a read-only text area with an option to
/// open the source file in an external editor.
/// </summary>
[CustomEditor(typeof(LuaFile))]
public class LuaScriptAssetEditor : Editor
{
private Vector2 _scrollPosition;
public override void OnInspectorGUI()
{
LuaFile luaScriptAsset = (LuaFile)target;
// Open in external editor button
string assetPath = AssetDatabase.GetAssetPath(target);
if (!string.IsNullOrEmpty(assetPath))
{
if (GUILayout.Button("Open in External Editor"))
{
// Opens the .lua source file in the OS-configured editor
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(assetPath, 1);
}
EditorGUILayout.Space(4);
}
// Read-only source view
EditorGUILayout.LabelField("Lua Source", EditorStyles.boldLabel);
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition,
GUILayout.MaxHeight(400));
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextArea(luaScriptAsset.LuaScript, GUILayout.ExpandHeight(true));
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndScrollView();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 66e212c64ebd0a34f9c23febe3e8545d

25
Editor/LuaImporter.cs Normal file
View File

@@ -0,0 +1,25 @@
using UnityEngine;
using System.IO;
using UnityEditor;
using UnityEditor.AssetImporters;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
[ScriptedImporter(2, "lua")]
class LuaImporter : ScriptedImporter
{
public override void OnImportAsset(AssetImportContext ctx)
{
var asset = ScriptableObject.CreateInstance<LuaFile>();
var luaCode = File.ReadAllText(ctx.assetPath);
asset.Init(luaCode);
asset.name = Path.GetFileName(ctx.assetPath);
var text = new TextAsset(asset.LuaScript);
ctx.AddObjectToAsset("Text", text);
ctx.AddObjectToAsset("Script", asset);
ctx.SetMainObject(asset); // LuaFile is the main object, not TextAsset
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 74e983e6cf3376944af7b469023d6e4d

789
Editor/PSXCutsceneEditor.cs Normal file
View File

@@ -0,0 +1,789 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace SplashEdit.RuntimeCode
{
[CustomEditor(typeof(PSXCutsceneClip))]
public class PSXCutsceneEditor : Editor
{
// ── Preview state ──
private bool _showAudioEvents = true;
private bool _previewing;
private bool _playing;
private float _previewFrame;
private double _playStartEditorTime;
private float _playStartFrame;
private HashSet<int> _firedAudioEventIndices = new HashSet<int>();
// Saved scene-view state so we can restore after preview
private bool _hasSavedSceneView;
private Vector3 _savedPivot;
private Quaternion _savedRotation;
private float _savedSize;
// Saved object transforms
private Dictionary<string, Vector3> _savedObjectPositions = new Dictionary<string, Vector3>();
private Dictionary<string, Quaternion> _savedObjectRotations = new Dictionary<string, Quaternion>();
private Dictionary<string, bool> _savedObjectActive = new Dictionary<string, bool>();
// Audio preview
private Dictionary<string, AudioClip> _audioClipCache = new Dictionary<string, AudioClip>();
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
if (_previewing) StopPreview();
}
private void OnEditorUpdate()
{
if (!_playing) return;
PSXCutsceneClip clip = (PSXCutsceneClip)target;
double elapsed = EditorApplication.timeSinceStartup - _playStartEditorTime;
_previewFrame = _playStartFrame + (float)(elapsed * 30.0);
if (_previewFrame >= clip.DurationFrames)
{
_previewFrame = clip.DurationFrames;
_playing = false;
}
ApplyPreview(clip);
Repaint();
}
public override void OnInspectorGUI()
{
PSXCutsceneClip clip = (PSXCutsceneClip)target;
Undo.RecordObject(clip, "Edit Cutscene");
// ── Header ──
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Cutscene Settings", EditorStyles.boldLabel);
clip.CutsceneName = EditorGUILayout.TextField("Cutscene Name", clip.CutsceneName);
if (!string.IsNullOrEmpty(clip.CutsceneName) && clip.CutsceneName.Length > 24)
EditorGUILayout.HelpBox("Name exceeds 24 characters and will be truncated on export.", MessageType.Warning);
clip.DurationFrames = EditorGUILayout.IntField("Duration (frames)", clip.DurationFrames);
if (clip.DurationFrames < 1) clip.DurationFrames = 1;
float seconds = clip.DurationFrames / 30f;
EditorGUILayout.LabelField($" = {seconds:F2} seconds at 30fps", EditorStyles.miniLabel);
// ── Preview Controls ──
EditorGUILayout.Space(6);
DrawPreviewControls(clip);
// Collect scene references for validation
var exporterNames = new HashSet<string>();
var audioNames = new HashSet<string>();
var canvasNames = new HashSet<string>();
var elementNames = new Dictionary<string, HashSet<string>>(); // canvas → element names
var exporters = Object.FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
foreach (var e in exporters)
exporterNames.Add(e.gameObject.name);
var audioSources = Object.FindObjectsByType<PSXAudioClip>(FindObjectsSortMode.None);
foreach (var a in audioSources)
if (!string.IsNullOrEmpty(a.ClipName))
audioNames.Add(a.ClipName);
var canvases = Object.FindObjectsByType<PSXCanvas>(FindObjectsSortMode.None);
foreach (var c in canvases)
{
string cName = c.CanvasName ?? "";
if (!string.IsNullOrEmpty(cName))
{
canvasNames.Add(cName);
if (!elementNames.ContainsKey(cName))
elementNames[cName] = new HashSet<string>();
// Gather all UI element names under this canvas
foreach (var box in c.GetComponentsInChildren<PSXUIBox>())
if (!string.IsNullOrEmpty(box.ElementName)) elementNames[cName].Add(box.ElementName);
foreach (var txt in c.GetComponentsInChildren<PSXUIText>())
if (!string.IsNullOrEmpty(txt.ElementName)) elementNames[cName].Add(txt.ElementName);
foreach (var bar in c.GetComponentsInChildren<PSXUIProgressBar>())
if (!string.IsNullOrEmpty(bar.ElementName)) elementNames[cName].Add(bar.ElementName);
foreach (var img in c.GetComponentsInChildren<PSXUIImage>())
if (!string.IsNullOrEmpty(img.ElementName)) elementNames[cName].Add(img.ElementName);
}
}
// ── Tracks ──
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Tracks", EditorStyles.boldLabel);
if (clip.Tracks == null) clip.Tracks = new List<PSXCutsceneTrack>();
int removeTrackIdx = -1;
for (int ti = 0; ti < clip.Tracks.Count; ti++)
{
var track = clip.Tracks[ti];
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
track.TrackType = (PSXTrackType)EditorGUILayout.EnumPopup("Type", track.TrackType);
if (GUILayout.Button("Remove", GUILayout.Width(65)))
removeTrackIdx = ti;
EditorGUILayout.EndHorizontal();
bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
bool isUITrack = track.IsUITrack;
bool isUIElementTrack = track.IsUIElementTrack;
if (isCameraTrack)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextField("Target", "(camera)");
EditorGUI.EndDisabledGroup();
}
else if (isUITrack)
{
track.UICanvasName = EditorGUILayout.TextField("Canvas Name", track.UICanvasName);
if (!string.IsNullOrEmpty(track.UICanvasName) && !canvasNames.Contains(track.UICanvasName))
EditorGUILayout.HelpBox($"No PSXCanvas with name '{track.UICanvasName}' in scene.", MessageType.Error);
if (isUIElementTrack)
{
track.UIElementName = EditorGUILayout.TextField("Element Name", track.UIElementName);
if (!string.IsNullOrEmpty(track.UICanvasName) && !string.IsNullOrEmpty(track.UIElementName))
{
if (elementNames.TryGetValue(track.UICanvasName, out var elNames) && !elNames.Contains(track.UIElementName))
EditorGUILayout.HelpBox($"No UI element '{track.UIElementName}' found under canvas '{track.UICanvasName}'.", MessageType.Error);
}
}
}
else
{
track.ObjectName = EditorGUILayout.TextField("Object Name", track.ObjectName);
// Validation
if (!string.IsNullOrEmpty(track.ObjectName) && !exporterNames.Contains(track.ObjectName))
EditorGUILayout.HelpBox($"No PSXObjectExporter found for '{track.ObjectName}' in scene.", MessageType.Error);
}
// ── Keyframes ──
if (track.Keyframes == null) track.Keyframes = new List<PSXKeyframe>();
EditorGUI.indentLevel++;
EditorGUILayout.LabelField($"Keyframes ({track.Keyframes.Count})", EditorStyles.miniLabel);
int removeKfIdx = -1;
for (int ki = 0; ki < track.Keyframes.Count; ki++)
{
var kf = track.Keyframes[ki];
// Row 1: frame number + interp mode + buttons
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Frame", GUILayout.Width(42));
kf.Frame = EditorGUILayout.IntField(kf.Frame, GUILayout.Width(60));
kf.Interp = (PSXInterpMode)EditorGUILayout.EnumPopup(kf.Interp, GUILayout.Width(80));
GUILayout.FlexibleSpace();
// Capture from scene
if (isCameraTrack)
{
if (GUILayout.Button("Capture Cam", GUILayout.Width(90)))
{
var sv = SceneView.lastActiveSceneView;
if (sv != null)
kf.Value = track.TrackType == PSXTrackType.CameraPosition
? sv.camera.transform.position : sv.camera.transform.eulerAngles;
else Debug.LogWarning("No active Scene View.");
}
}
else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotation))
{
// Capture from the named object in scene
if (!string.IsNullOrEmpty(track.ObjectName) && GUILayout.Button("From Object", GUILayout.Width(85)))
{
var go = GameObject.Find(track.ObjectName);
if (go != null)
kf.Value = track.TrackType == PSXTrackType.ObjectPosition
? go.transform.position : go.transform.eulerAngles;
else Debug.LogWarning($"Object '{track.ObjectName}' not found in scene.");
}
}
if (GUILayout.Button("\u2212", GUILayout.Width(22)))
removeKfIdx = ki;
EditorGUILayout.EndHorizontal();
// Row 2: value on its own line
EditorGUI.indentLevel++;
switch (track.TrackType)
{
case PSXTrackType.ObjectActive:
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
{
string label = track.TrackType == PSXTrackType.ObjectActive ? "Active" : "Visible";
bool active = EditorGUILayout.Toggle(label, kf.Value.x > 0.5f);
kf.Value = new Vector3(active ? 1f : 0f, 0, 0);
break;
}
case PSXTrackType.ObjectRotation:
case PSXTrackType.CameraRotation:
{
kf.Value = EditorGUILayout.Vector3Field("Rotation\u00b0", kf.Value);
break;
}
case PSXTrackType.UIProgress:
{
float progress = EditorGUILayout.Slider("Progress %", kf.Value.x, 0f, 100f);
kf.Value = new Vector3(progress, 0, 0);
break;
}
case PSXTrackType.UIPosition:
{
Vector2 pos = EditorGUILayout.Vector2Field("Position (px)", new Vector2(kf.Value.x, kf.Value.y));
kf.Value = new Vector3(pos.x, pos.y, 0);
break;
}
case PSXTrackType.UIColor:
{
// Show as RGB 0-255 integers
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("R", GUILayout.Width(14));
float r = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 255), GUILayout.Width(40));
EditorGUILayout.LabelField("G", GUILayout.Width(14));
float g = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.y), 0, 255), GUILayout.Width(40));
EditorGUILayout.LabelField("B", GUILayout.Width(14));
float b = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.z), 0, 255), GUILayout.Width(40));
EditorGUILayout.EndHorizontal();
kf.Value = new Vector3(r, g, b);
break;
}
default:
kf.Value = EditorGUILayout.Vector3Field("Value", kf.Value);
break;
}
EditorGUI.indentLevel--;
if (ki < track.Keyframes.Count - 1)
{
EditorGUILayout.Space(1);
var rect = EditorGUILayout.GetControlRect(false, 1);
EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f));
}
}
if (removeKfIdx >= 0) track.Keyframes.RemoveAt(removeKfIdx);
// Add keyframe buttons
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Keyframe", GUILayout.Width(90)))
{
int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0;
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = Vector3.zero });
}
if (isCameraTrack)
{
if (GUILayout.Button("+ from Scene Cam", GUILayout.Width(130)))
{
var sv = SceneView.lastActiveSceneView;
Vector3 val = Vector3.zero;
if (sv != null)
val = track.TrackType == PSXTrackType.CameraPosition
? sv.camera.transform.position : sv.camera.transform.eulerAngles;
int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0;
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val });
}
}
else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotation))
{
if (!string.IsNullOrEmpty(track.ObjectName))
{
if (GUILayout.Button("+ from Object", GUILayout.Width(110)))
{
var go = GameObject.Find(track.ObjectName);
Vector3 val = Vector3.zero;
if (go != null)
val = track.TrackType == PSXTrackType.ObjectPosition
? go.transform.position : go.transform.eulerAngles;
int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0;
track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val });
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUI.indentLevel--;
EditorGUILayout.EndVertical();
EditorGUILayout.Space(2);
}
if (removeTrackIdx >= 0) clip.Tracks.RemoveAt(removeTrackIdx);
if (clip.Tracks.Count < 8)
{
if (GUILayout.Button("+ Add Track"))
clip.Tracks.Add(new PSXCutsceneTrack());
}
else
{
EditorGUILayout.HelpBox("Maximum 8 tracks per cutscene.", MessageType.Info);
}
// ── Audio Events ──
EditorGUILayout.Space(8);
_showAudioEvents = EditorGUILayout.Foldout(_showAudioEvents, "Audio Events", true);
if (_showAudioEvents)
{
if (clip.AudioEvents == null) clip.AudioEvents = new List<PSXAudioEvent>();
int removeEventIdx = -1;
for (int ei = 0; ei < clip.AudioEvents.Count; ei++)
{
var evt = clip.AudioEvents[ei];
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Frame", GUILayout.Width(42));
evt.Frame = EditorGUILayout.IntField(evt.Frame, GUILayout.Width(60));
GUILayout.FlexibleSpace();
if (GUILayout.Button("\u2212", GUILayout.Width(22)))
removeEventIdx = ei;
EditorGUILayout.EndHorizontal();
evt.ClipName = EditorGUILayout.TextField("Clip Name", evt.ClipName);
if (!string.IsNullOrEmpty(evt.ClipName) && !audioNames.Contains(evt.ClipName))
EditorGUILayout.HelpBox($"No PSXAudioClip with ClipName '{evt.ClipName}' in scene.", MessageType.Error);
evt.Volume = EditorGUILayout.IntSlider("Volume", evt.Volume, 0, 128);
evt.Pan = EditorGUILayout.IntSlider("Pan", evt.Pan, 0, 127);
EditorGUILayout.EndVertical();
}
if (removeEventIdx >= 0) clip.AudioEvents.RemoveAt(removeEventIdx);
if (clip.AudioEvents.Count < 64)
{
if (GUILayout.Button("+ Add Audio Event"))
clip.AudioEvents.Add(new PSXAudioEvent());
}
else
{
EditorGUILayout.HelpBox("Maximum 64 audio events per cutscene.", MessageType.Info);
}
}
if (GUI.changed)
EditorUtility.SetDirty(clip);
}
// =====================================================================
// Preview Controls
// =====================================================================
private void DrawPreviewControls(PSXCutsceneClip clip)
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
// Transport bar
EditorGUILayout.BeginHorizontal();
bool wasPlaying = _playing;
if (_playing)
{
if (GUILayout.Button("\u275A\u275A Pause", GUILayout.Width(70)))
_playing = false;
}
else
{
if (GUILayout.Button("\u25B6 Play", GUILayout.Width(70)))
{
if (!_previewing) StartPreview(clip);
_playing = true;
_playStartEditorTime = EditorApplication.timeSinceStartup;
_playStartFrame = _previewFrame;
_firedAudioEventIndices.Clear();
// Mark already-passed events so they won't fire again
if (clip.AudioEvents != null)
for (int i = 0; i < clip.AudioEvents.Count; i++)
if (clip.AudioEvents[i].Frame < (int)_previewFrame)
_firedAudioEventIndices.Add(i);
}
}
if (GUILayout.Button("\u25A0 Stop", GUILayout.Width(60)))
{
_playing = false;
_previewFrame = 0;
if (_previewing) StopPreview();
}
if (_previewing)
{
GUI.color = new Color(1f, 0.4f, 0.4f);
if (GUILayout.Button("End Preview", GUILayout.Width(90)))
{
_playing = false;
StopPreview();
}
GUI.color = Color.white;
}
EditorGUILayout.EndHorizontal();
// Timeline scrubber
EditorGUI.BeginChangeCheck();
float newFrame = EditorGUILayout.Slider("Frame", _previewFrame, 0, clip.DurationFrames);
if (EditorGUI.EndChangeCheck())
{
if (!_previewing) StartPreview(clip);
_previewFrame = newFrame;
_playing = false;
_firedAudioEventIndices.Clear();
ApplyPreview(clip);
}
float previewSec = _previewFrame / 30f;
EditorGUILayout.LabelField(
$" {(int)_previewFrame} / {clip.DurationFrames} ({previewSec:F2}s / {seconds(clip):F2}s)",
EditorStyles.miniLabel);
if (_previewing)
EditorGUILayout.HelpBox(
"PREVIEWING: Scene View camera & objects are being driven. " +
"Click \u201cEnd Preview\u201d or \u201cStop\u201d to restore original positions.",
MessageType.Warning);
EditorGUILayout.EndVertical();
}
private static float seconds(PSXCutsceneClip clip) => clip.DurationFrames / 30f;
// =====================================================================
// Preview Lifecycle
// =====================================================================
private void StartPreview(PSXCutsceneClip clip)
{
if (_previewing) return;
_previewing = true;
_firedAudioEventIndices.Clear();
// Save scene view camera
var sv = SceneView.lastActiveSceneView;
if (sv != null)
{
_hasSavedSceneView = true;
_savedPivot = sv.pivot;
_savedRotation = sv.rotation;
_savedSize = sv.size;
}
// Save object transforms
_savedObjectPositions.Clear();
_savedObjectRotations.Clear();
_savedObjectActive.Clear();
if (clip.Tracks != null)
{
foreach (var track in clip.Tracks)
{
if (string.IsNullOrEmpty(track.ObjectName)) continue;
bool isCam = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation;
if (isCam) continue;
var go = GameObject.Find(track.ObjectName);
if (go == null) continue;
if (!_savedObjectPositions.ContainsKey(track.ObjectName))
{
_savedObjectPositions[track.ObjectName] = go.transform.position;
_savedObjectRotations[track.ObjectName] = go.transform.rotation;
_savedObjectActive[track.ObjectName] = go.activeSelf;
}
}
}
// Build audio clip lookup
_audioClipCache.Clear();
var audioSources = Object.FindObjectsByType<PSXAudioClip>(FindObjectsSortMode.None);
foreach (var a in audioSources)
if (!string.IsNullOrEmpty(a.ClipName) && a.Clip != null)
_audioClipCache[a.ClipName] = a.Clip;
}
private void StopPreview()
{
if (!_previewing) return;
_previewing = false;
_playing = false;
// Restore scene view camera
if (_hasSavedSceneView)
{
var sv = SceneView.lastActiveSceneView;
if (sv != null)
{
sv.pivot = _savedPivot;
sv.rotation = _savedRotation;
sv.size = _savedSize;
sv.Repaint();
}
_hasSavedSceneView = false;
}
// Restore object transforms
foreach (var kvp in _savedObjectPositions)
{
var go = GameObject.Find(kvp.Key);
if (go == null) continue;
go.transform.position = kvp.Value;
if (_savedObjectRotations.ContainsKey(kvp.Key))
go.transform.rotation = _savedObjectRotations[kvp.Key];
if (_savedObjectActive.ContainsKey(kvp.Key))
go.SetActive(_savedObjectActive[kvp.Key]);
}
_savedObjectPositions.Clear();
_savedObjectRotations.Clear();
_savedObjectActive.Clear();
SceneView.RepaintAll();
Repaint();
}
// =====================================================================
// Apply Preview at Current Frame
// =====================================================================
private void ApplyPreview(PSXCutsceneClip clip)
{
if (!_previewing) return;
float frame = _previewFrame;
var sv = SceneView.lastActiveSceneView;
Vector3? camPos = null;
Quaternion? camRot = null;
if (clip.Tracks != null)
{
foreach (var track in clip.Tracks)
{
// Compute initial value for pre-first-keyframe blending
Vector3 initialVal = Vector3.zero;
switch (track.TrackType)
{
case PSXTrackType.CameraPosition:
if (sv != null)
// Recover position from saved pivot/rotation/size
initialVal = _savedPivot - _savedRotation * Vector3.forward * _savedSize;
break;
case PSXTrackType.CameraRotation:
initialVal = _savedRotation.eulerAngles;
break;
case PSXTrackType.ObjectPosition:
if (_savedObjectPositions.ContainsKey(track.ObjectName ?? ""))
initialVal = _savedObjectPositions[track.ObjectName];
break;
case PSXTrackType.ObjectRotation:
if (_savedObjectRotations.ContainsKey(track.ObjectName ?? ""))
initialVal = _savedObjectRotations[track.ObjectName].eulerAngles;
break;
case PSXTrackType.ObjectActive:
if (_savedObjectActive.ContainsKey(track.ObjectName ?? ""))
initialVal = new Vector3(_savedObjectActive[track.ObjectName] ? 1f : 0f, 0, 0);
break;
// UI tracks: initial values stay zero (no scene preview state to capture)
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
initialVal = new Vector3(1f, 0, 0); // assume visible by default
break;
case PSXTrackType.UIProgress:
case PSXTrackType.UIPosition:
case PSXTrackType.UIColor:
break; // zero is fine
}
Vector3 val = EvaluateTrack(track, frame, initialVal);
switch (track.TrackType)
{
case PSXTrackType.CameraPosition:
camPos = val;
break;
case PSXTrackType.CameraRotation:
camRot = Quaternion.Euler(val);
break;
case PSXTrackType.ObjectPosition:
{
var go = GameObject.Find(track.ObjectName);
if (go != null) go.transform.position = val;
break;
}
case PSXTrackType.ObjectRotation:
{
var go = GameObject.Find(track.ObjectName);
if (go != null) go.transform.rotation = Quaternion.Euler(val);
break;
}
case PSXTrackType.ObjectActive:
{
var go = GameObject.Find(track.ObjectName);
if (go != null) go.SetActive(val.x > 0.5f);
break;
}
// UI tracks: no scene preview, values are applied on PS1 only
case PSXTrackType.UICanvasVisible:
case PSXTrackType.UIElementVisible:
case PSXTrackType.UIProgress:
case PSXTrackType.UIPosition:
case PSXTrackType.UIColor:
break;
}
}
}
// Drive scene view camera
if (sv != null && (camPos.HasValue || camRot.HasValue))
{
Vector3 pos = camPos ?? sv.camera.transform.position;
Quaternion rot = camRot ?? sv.camera.transform.rotation;
// SceneView needs pivot and rotation set — pivot = position + forward * size
sv.rotation = rot;
sv.pivot = pos + rot * Vector3.forward * sv.cameraDistance;
sv.Repaint();
}
// Fire audio events (only during playback, not scrubbing)
if (_playing && clip.AudioEvents != null)
{
for (int i = 0; i < clip.AudioEvents.Count; i++)
{
if (_firedAudioEventIndices.Contains(i)) continue;
var evt = clip.AudioEvents[i];
if (frame >= evt.Frame)
{
_firedAudioEventIndices.Add(i);
PlayAudioPreview(evt);
}
}
}
}
// =====================================================================
// Track Evaluation (linear interpolation, matching C++ runtime)
// =====================================================================
private static Vector3 EvaluateTrack(PSXCutsceneTrack track, float frame, Vector3 initialValue)
{
if (track.Keyframes == null || track.Keyframes.Count == 0)
return Vector3.zero;
// Step interpolation tracks: ObjectActive, UICanvasVisible, UIElementVisible
if (track.TrackType == PSXTrackType.ObjectActive ||
track.TrackType == PSXTrackType.UICanvasVisible ||
track.TrackType == PSXTrackType.UIElementVisible)
{
if (track.Keyframes.Count > 0 && track.Keyframes[0].Frame > 0 && frame < track.Keyframes[0].Frame)
return initialValue;
return EvaluateStep(track.Keyframes, frame);
}
// Find surrounding keyframes
PSXKeyframe before = null, after = null;
for (int i = 0; i < track.Keyframes.Count; i++)
{
if (track.Keyframes[i].Frame <= frame)
before = track.Keyframes[i];
if (track.Keyframes[i].Frame >= frame && after == null)
after = track.Keyframes[i];
}
if (before == null && after == null) return Vector3.zero;
// Pre-first-keyframe: blend from initial value to first keyframe
if (before == null && after != null && after.Frame > 0 && frame < after.Frame)
{
float rawT = frame / after.Frame;
float t = ApplyInterpCurve(rawT, after.Interp);
return Vector3.Lerp(initialValue, after.Value, t);
}
if (before == null) return after.Value;
if (after == null) return before.Value;
if (before == after) return before.Value;
float span = after.Frame - before.Frame;
float rawT2 = (frame - before.Frame) / span;
float t2 = ApplyInterpCurve(rawT2, after.Interp);
// Linear interpolation for all tracks including rotation.
// No shortest-path wrapping: a keyframe from 0 to 360 rotates the full circle.
return Vector3.Lerp(before.Value, after.Value, t2);
}
/// <summary>
/// Apply easing curve to a linear t value (0..1). Matches the C++ applyCurve().
/// </summary>
private static float ApplyInterpCurve(float t, PSXInterpMode mode)
{
switch (mode)
{
default:
case PSXInterpMode.Linear:
return t;
case PSXInterpMode.Step:
return 0f;
case PSXInterpMode.EaseIn:
return t * t;
case PSXInterpMode.EaseOut:
return t * (2f - t);
case PSXInterpMode.EaseInOut:
return t * t * (3f - 2f * t);
}
}
private static Vector3 EvaluateStep(List<PSXKeyframe> keyframes, float frame)
{
Vector3 result = Vector3.zero;
for (int i = 0; i < keyframes.Count; i++)
{
if (keyframes[i].Frame <= frame)
result = keyframes[i].Value;
}
return result;
}
// =====================================================================
// Audio Preview
// =====================================================================
private void PlayAudioPreview(PSXAudioEvent evt)
{
if (string.IsNullOrEmpty(evt.ClipName)) return;
if (!_audioClipCache.TryGetValue(evt.ClipName, out AudioClip clip)) return;
// Use Unity's editor audio playback utility via reflection
// (PlayClipAtPoint doesn't work in edit mode)
var unityEditorAssembly = typeof(AudioImporter).Assembly;
var audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
if (audioUtilClass == null) return;
// Stop any previous preview
var stopMethod = audioUtilClass.GetMethod("StopAllPreviewClips",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
stopMethod?.Invoke(null, null);
// Play the clip
var playMethod = audioUtilClass.GetMethod("PlayPreviewClip",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null, new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) }, null);
playMethod?.Invoke(null, new object[] { clip, 0, false });
}
}
}
#endif

View File

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

80
Editor/PSXMenuItems.cs Normal file
View File

@@ -0,0 +1,80 @@
using UnityEditor;
using UnityEngine;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Minimal menu items — everything goes through the unified Control Panel.
/// Only keeps: Control Panel shortcut + GameObject creation helpers.
/// </summary>
public static class PSXMenuItems
{
private const string MENU_ROOT = "PlayStation 1/";
// ───── Main Entry Point ─────
[MenuItem(MENU_ROOT + "SplashEdit Control Panel %#l", false, 0)]
public static void OpenControlPanel()
{
SplashControlPanel.ShowWindow();
}
// ───── GameObject Menu ─────
[MenuItem("GameObject/PlayStation 1/Scene Exporter", false, 10)]
public static void CreateSceneExporter(MenuCommand menuCommand)
{
var existing = Object.FindFirstObjectByType<PSXSceneExporter>();
if (existing != null)
{
EditorUtility.DisplayDialog(
"Scene Exporter Exists",
"A PSXSceneExporter already exists in this scene.\n\n" +
"Only one exporter is needed per scene.",
"OK");
Selection.activeGameObject = existing.gameObject;
return;
}
var go = new GameObject("PSXSceneExporter");
go.AddComponent<PSXSceneExporter>();
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
Undo.RegisterCreatedObjectUndo(go, "Create PSX Scene Exporter");
Selection.activeGameObject = go;
}
[MenuItem("GameObject/PlayStation 1/Exportable Object", false, 12)]
public static void CreateExportableObject(MenuCommand menuCommand)
{
var go = new GameObject("PSXObject");
go.AddComponent<PSXObjectExporter>();
GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
Undo.RegisterCreatedObjectUndo(go, "Create PSX Object");
Selection.activeGameObject = go;
}
// ───── Context Menu ─────
[MenuItem("CONTEXT/MeshFilter/Add PSX Object Exporter")]
public static void AddPSXObjectExporterFromMesh(MenuCommand command)
{
var meshFilter = command.context as MeshFilter;
if (meshFilter != null && meshFilter.GetComponent<PSXObjectExporter>() == null)
{
Undo.AddComponent<PSXObjectExporter>(meshFilter.gameObject);
}
}
[MenuItem("CONTEXT/MeshRenderer/Add PSX Object Exporter")]
public static void AddPSXObjectExporterFromRenderer(MenuCommand command)
{
var renderer = command.context as MeshRenderer;
if (renderer != null && renderer.GetComponent<PSXObjectExporter>() == null)
{
Undo.AddComponent<PSXObjectExporter>(renderer.gameObject);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 174ee99c9e9aafd4ea9002fc3548f53d

View File

@@ -1,31 +0,0 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
[CustomEditor(typeof(PSXNavMesh))]
public class PSXNavMeshEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
PSXNavMesh comp = (PSXNavMesh)target;
if (GUILayout.Button("Create preview"))
{
PSXSceneExporter exporter = FindObjectsByType<PSXSceneExporter>(FindObjectsSortMode.None).FirstOrDefault();
if(exporter != null)
{
comp.CreateNavmesh(exporter.GTEScaling);
}
else
{
Debug.LogError("No PSXSceneExporter found in the scene. We can't pull the GTE scaling from the exporter.");
}
}
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 9d3bd83aac4c3ce9ab1698a6a2bc735d

View File

@@ -0,0 +1,401 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Collections.Generic;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Editor window for PS1 navigation mesh generation.
/// Uses DotRecast (C# Recast) to voxelize scene geometry and build
/// convex navigation regions for the PS1 runtime.
/// All nav settings live on the PSXPlayer component so the editor
/// preview and the scene export always use the same values.
/// </summary>
public class PSXNavRegionEditor : EditorWindow
{
private PSXNavRegionBuilder _builder;
private bool _previewRegions = true;
private bool _previewPortals = true;
private bool _previewLabels = true;
private int _selectedRegion = -1;
private bool _showAdvanced = false;
[MenuItem("PlayStation 1/Nav Region Builder")]
public static void ShowWindow()
{
GetWindow<PSXNavRegionEditor>("Nav Region Builder");
}
private void OnEnable()
{
SceneView.duringSceneGui += OnSceneGUI;
}
private void OnDisable()
{
SceneView.duringSceneGui -= OnSceneGUI;
}
private void OnGUI()
{
EditorGUILayout.Space(5);
GUILayout.Label("PSX Nav Region Builder", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
var players = FindObjectsByType<PSXPlayer>(FindObjectsSortMode.None);
if (players.Length == 0)
{
EditorGUILayout.HelpBox(
"No PSXPlayer in scene. Add a PSXPlayer component to configure navigation settings.",
MessageType.Warning);
return;
}
var player = players[0];
var so = new SerializedObject(player);
so.Update();
// Info
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.HelpBox(
"Uses DotRecast (Recast voxelization) to build PS1 nav regions.\n" +
"Settings are on the PSXPlayer component so editor preview\n" +
"and scene export always match.\n" +
"1. Configure settings below (saved on PSXPlayer)\n" +
"2. Click 'Build Nav Regions' to preview\n" +
"3. Results export automatically with the scene",
MessageType.Info);
}
EditorGUILayout.Space(5);
// Agent settings (from PSXPlayer serialized fields)
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
GUILayout.Label("Agent Settings (PSXPlayer)", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(so.FindProperty("playerHeight"),
new GUIContent("Agent Height", "Camera eye height above feet"));
EditorGUILayout.PropertyField(so.FindProperty("playerRadius"),
new GUIContent("Agent Radius", "Collision radius for wall sliding"));
EditorGUILayout.PropertyField(so.FindProperty("maxStepHeight"),
new GUIContent("Max Step Height", "Maximum height the agent can step up"));
EditorGUILayout.PropertyField(so.FindProperty("walkableSlopeAngle"),
new GUIContent("Max Slope", "Maximum walkable slope angle in degrees"));
}
EditorGUILayout.Space(5);
// Advanced settings
_showAdvanced = EditorGUILayout.Foldout(_showAdvanced, "Advanced Settings");
if (_showAdvanced)
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.PropertyField(so.FindProperty("navCellSize"),
new GUIContent("Cell Size", "Voxel size in XZ plane. Smaller = more accurate but slower."));
EditorGUILayout.PropertyField(so.FindProperty("navCellHeight"),
new GUIContent("Cell Height", "Voxel height. Smaller = more accurate vertical resolution."));
EditorGUILayout.Space(3);
float cs = player.NavCellSize;
float ch = player.NavCellHeight;
int walkH = (int)System.Math.Ceiling(player.PlayerHeight / ch);
int walkR = (int)System.Math.Ceiling(player.PlayerRadius / cs);
int walkC = (int)System.Math.Floor(player.MaxStepHeight / ch);
EditorGUILayout.LabelField("Voxel walkable height", $"{walkH} cells");
EditorGUILayout.LabelField("Voxel walkable radius", $"{walkR} cells");
EditorGUILayout.LabelField("Voxel walkable climb", $"{walkC} cells ({walkC * ch:F3} units)");
}
}
so.ApplyModifiedProperties();
EditorGUILayout.Space(5);
// Build button
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
GUILayout.Label("Generation", EditorStyles.boldLabel);
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.4f);
if (GUILayout.Button("Build Nav Regions", GUILayout.Height(35)))
{
BuildNavRegions(player);
}
GUI.backgroundColor = Color.white;
if (_builder != null && _builder.RegionCount > 0)
{
EditorGUILayout.Space(3);
if (GUILayout.Button("Clear Regions"))
{
_builder = null;
_selectedRegion = -1;
SceneView.RepaintAll();
}
}
}
EditorGUILayout.Space(5);
// Visualization
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
GUILayout.Label("Visualization", EditorStyles.boldLabel);
_previewRegions = EditorGUILayout.Toggle("Show Regions", _previewRegions);
_previewPortals = EditorGUILayout.Toggle("Show Portals", _previewPortals);
_previewLabels = EditorGUILayout.Toggle("Show Labels", _previewLabels);
}
EditorGUILayout.Space(5);
// Statistics
if (_builder != null && _builder.RegionCount > 0)
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
GUILayout.Label("Statistics", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Regions", _builder.RegionCount.ToString());
EditorGUILayout.LabelField("Portals", _builder.PortalCount.ToString());
var rooms = new HashSet<byte>();
for (int i = 0; i < _builder.RegionCount; i++)
rooms.Add(_builder.Regions[i].roomIndex);
EditorGUILayout.LabelField("Rooms", rooms.Count.ToString());
int exportSize = _builder.GetBinarySize();
EditorGUILayout.LabelField("Export Size",
$"{exportSize:N0} bytes ({exportSize / 1024f:F1} KB)");
int flat = 0, ramp = 0, stairs = 0;
for (int i = 0; i < _builder.RegionCount; i++)
{
switch (_builder.Regions[i].surfaceType)
{
case NavSurfaceType.Flat: flat++; break;
case NavSurfaceType.Ramp: ramp++; break;
case NavSurfaceType.Stairs: stairs++; break;
}
}
EditorGUILayout.LabelField("Types",
$"{flat} flat, {ramp} ramp, {stairs} stairs");
if (_selectedRegion >= 0 && _selectedRegion < _builder.RegionCount)
{
EditorGUILayout.Space(3);
GUILayout.Label($"Selected Region #{_selectedRegion}",
EditorStyles.miniLabel);
var region = _builder.Regions[_selectedRegion];
EditorGUILayout.LabelField(" Vertices",
region.vertsXZ.Count.ToString());
EditorGUILayout.LabelField(" Portals",
region.portalCount.ToString());
EditorGUILayout.LabelField(" Surface",
region.surfaceType.ToString());
EditorGUILayout.LabelField(" Room",
region.roomIndex.ToString());
EditorGUILayout.LabelField(" Floor Y",
$"{region.planeD:F2} (A={region.planeA:F3}, B={region.planeB:F3})");
}
}
ValidateRegions();
}
else
{
EditorGUILayout.HelpBox(
"No nav regions built. Click 'Build Nav Regions' to generate.",
MessageType.Warning);
}
}
// ====================================================================
// Build
// ====================================================================
private void BuildNavRegions(PSXPlayer player)
{
EditorUtility.DisplayProgressBar("Nav Region Builder", "Building nav regions with DotRecast...", 0.3f);
_builder = new PSXNavRegionBuilder();
_builder.AgentHeight = player.PlayerHeight;
_builder.AgentRadius = player.PlayerRadius;
_builder.MaxStepHeight = player.MaxStepHeight;
_builder.WalkableSlopeAngle = player.WalkableSlopeAngle;
_builder.CellSize = player.NavCellSize;
_builder.CellHeight = player.NavCellHeight;
Vector3 playerSpawn = player.transform.position;
player.FindNavmesh();
playerSpawn = player.CamPoint;
PSXObjectExporter[] exporters =
FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
_builder.Build(exporters, playerSpawn);
_selectedRegion = -1;
EditorUtility.ClearProgressBar();
SceneView.RepaintAll();
}
// ====================================================================
// Validation
// ====================================================================
private void ValidateRegions()
{
if (_builder == null) return;
List<string> warnings = new List<string>();
for (int i = 0; i < _builder.RegionCount; i++)
{
var region = _builder.Regions[i];
if (region.vertsXZ.Count < 3)
warnings.Add($"Region {i}: degenerate ({region.vertsXZ.Count} verts)");
if (region.portalCount == 0 && _builder.RegionCount > 1)
warnings.Add($"Region {i}: isolated (no portals)");
if (region.vertsXZ.Count > 8)
warnings.Add($"Region {i}: too many verts ({region.vertsXZ.Count} > 8)");
}
int exportSize = _builder.GetBinarySize();
if (exportSize > 8192)
warnings.Add($"Export size {exportSize} bytes is large for PS1 (> 8KB)");
if (warnings.Count > 0)
{
EditorGUILayout.Space(5);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
GUILayout.Label("Warnings", EditorStyles.boldLabel);
foreach (string w in warnings)
EditorGUILayout.LabelField(w, EditorStyles.miniLabel);
}
}
}
// ====================================================================
// Scene view drawing
// ====================================================================
private static readonly Color[] RoomColors = new Color[]
{
new Color(0.2f, 0.8f, 0.2f),
new Color(0.2f, 0.6f, 0.9f),
new Color(0.9f, 0.7f, 0.1f),
new Color(0.8f, 0.2f, 0.8f),
new Color(0.1f, 0.9f, 0.9f),
new Color(0.9f, 0.5f, 0.2f),
new Color(0.5f, 0.9f, 0.3f),
new Color(0.9f, 0.3f, 0.5f),
new Color(0.4f, 0.4f, 0.9f),
new Color(0.7f, 0.9f, 0.7f),
new Color(0.9f, 0.9f, 0.4f),
new Color(0.6f, 0.3f, 0.6f),
new Color(0.3f, 0.7f, 0.7f),
new Color(0.8f, 0.6f, 0.4f),
new Color(0.4f, 0.8f, 0.6f),
new Color(0.7f, 0.4f, 0.4f),
};
private void OnSceneGUI(SceneView sceneView)
{
if (_builder == null || _builder.RegionCount == 0) return;
var regions = _builder.Regions;
if (_previewRegions)
{
for (int i = 0; i < regions.Count; i++)
{
var region = regions[i];
bool selected = (i == _selectedRegion);
Color baseColor = RoomColors[region.roomIndex % RoomColors.Length];
float fillAlpha = selected ? 0.4f : 0.15f;
if (region.vertsXZ.Count >= 3)
{
Vector3[] worldVerts = new Vector3[region.vertsXZ.Count];
for (int v = 0; v < region.vertsXZ.Count; v++)
{
float y = region.planeA * region.vertsXZ[v].x +
region.planeB * region.vertsXZ[v].y +
region.planeD;
worldVerts[v] = new Vector3(
region.vertsXZ[v].x, y + 0.05f, region.vertsXZ[v].y);
}
Handles.color = selected
? Color.white
: new Color(baseColor.r, baseColor.g, baseColor.b, 0.8f);
for (int v = 0; v < worldVerts.Length; v++)
Handles.DrawLine(worldVerts[v],
worldVerts[(v + 1) % worldVerts.Length]);
Handles.color = new Color(baseColor.r, baseColor.g, baseColor.b,
fillAlpha);
for (int v = 1; v < worldVerts.Length - 1; v++)
Handles.DrawAAConvexPolygon(
worldVerts[0], worldVerts[v], worldVerts[v + 1]);
if (_previewLabels)
{
Vector3 center = Vector3.zero;
foreach (var wv in worldVerts) center += wv;
center /= worldVerts.Length;
string label = $"R{i}";
if (region.roomIndex != 0xFF)
label += $"\nRm{region.roomIndex}";
Handles.Label(center, label, EditorStyles.whiteBoldLabel);
if (Handles.Button(center, Quaternion.identity,
0.2f, 0.3f, Handles.SphereHandleCap))
{
_selectedRegion = i;
Repaint();
}
}
}
}
}
if (_previewPortals && _builder.Portals != null)
{
for (int i = 0; i < regions.Count; i++)
{
var region = regions[i];
int pStart = region.portalStart;
int pCount = region.portalCount;
for (int p = pStart;
p < pStart + pCount && p < _builder.Portals.Count; p++)
{
var portal = _builder.Portals[p];
float yA = region.planeA * portal.a.x +
region.planeB * portal.a.y + region.planeD;
float yB = region.planeA * portal.b.x +
region.planeB * portal.b.y + region.planeD;
Vector3 worldA = new Vector3(portal.a.x, yA + 0.08f, portal.a.y);
Vector3 worldB = new Vector3(portal.b.x, yB + 0.08f, portal.b.y);
if (Mathf.Abs(portal.heightDelta) <= 0.35f)
Handles.color = new Color(1f, 1f, 1f, 0.9f);
else
Handles.color = new Color(1f, 0.9f, 0.2f, 0.9f);
Handles.DrawLine(worldA, worldB, 3f);
}
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,247 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
[CustomEditor(typeof(PSXObjectExporter))]
[CanEditMultipleObjects]
public class PSXObjectExporterEditor : UnityEditor.Editor
{
private SerializedProperty isActiveProp;
private SerializedProperty bitDepthProp;
private SerializedProperty luaFileProp;
private SerializedProperty collisionTypeProp;
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private int triangleCount;
private int vertexCount;
private bool showExport = true;
private bool showCollision = true;
private void OnEnable()
{
isActiveProp = serializedObject.FindProperty("isActive");
bitDepthProp = serializedObject.FindProperty("bitDepth");
luaFileProp = serializedObject.FindProperty("luaFile");
collisionTypeProp = serializedObject.FindProperty("collisionType");
CacheMeshInfo();
}
private void CacheMeshInfo()
{
var exporter = target as PSXObjectExporter;
if (exporter == null) return;
meshFilter = exporter.GetComponent<MeshFilter>();
meshRenderer = exporter.GetComponent<MeshRenderer>();
if (meshFilter != null && meshFilter.sharedMesh != null)
{
triangleCount = meshFilter.sharedMesh.triangles.Length / 3;
vertexCount = meshFilter.sharedMesh.vertexCount;
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawHeader();
EditorGUILayout.Space(4);
if (!isActiveProp.boolValue)
{
EditorGUILayout.LabelField("Object will be skipped during export.", PSXEditorStyles.InfoBox);
serializedObject.ApplyModifiedProperties();
return;
}
DrawMeshSummary();
PSXEditorStyles.DrawSeparator(6, 6);
DrawExportSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawCollisionSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawActions();
serializedObject.ApplyModifiedProperties();
}
private new void DrawHeader()
{
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(isActiveProp, GUIContent.none, GUILayout.Width(18));
var exporter = target as PSXObjectExporter;
EditorGUILayout.LabelField(exporter.gameObject.name, PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawMeshSummary()
{
if (meshFilter == null || meshFilter.sharedMesh == null)
{
EditorGUILayout.LabelField("No mesh on this object.", PSXEditorStyles.InfoBox);
return;
}
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 DrawExportSection()
{
showExport = EditorGUILayout.Foldout(showExport, "Export", true, PSXEditorStyles.FoldoutHeader);
if (!showExport) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
if (luaFileProp.objectReferenceValue != null)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
if (GUILayout.Button("Clear", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
luaFileProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Create Lua Script", PSXEditorStyles.SecondaryButton, 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.Static)
{
EditorGUILayout.LabelField(
"<color=#88cc88>Only bakes holes in the navregions</color>",
PSXEditorStyles.RichLabel);
}
else if (collType == PSXCollisionType.Dynamic)
{
EditorGUILayout.LabelField(
"<color=#88aaff>Runtime AABB collider. Pushes player back + fires Lua events.</color>",
PSXEditorStyles.RichLabel);
}
EditorGUI.indentLevel--;
}
private void DrawActions()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select Scene Exporter", PSXEditorStyles.SecondaryButton))
{
var se = FindFirstObjectByType<PSXSceneExporter>();
if (se != null)
Selection.activeGameObject = se.gameObject;
else
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
EditorGUILayout.EndHorizontal();
}
private void CreateNewLuaScript()
{
var exporter = target as PSXObjectExporter;
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script", defaultName + ".lua", "lua",
"Create a new Lua script for this object");
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)
{
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
private static void DrawColliderGizmo(PSXObjectExporter exporter, GizmoType gizmoType)
{
if (exporter.CollisionType != PSXCollisionType.Dynamic) return;
MeshFilter mf = exporter.GetComponent<MeshFilter>();
Mesh mesh = mf?.sharedMesh;
if (mesh == null) return;
Bounds local = mesh.bounds;
Matrix4x4 worldMatrix = exporter.transform.localToWorldMatrix;
Vector3 ext = local.extents;
Vector3 center = local.center;
Vector3 aabbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 aabbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int i = 0; i < 8; i++)
{
Vector3 corner = center + new Vector3(
(i & 1) != 0 ? ext.x : -ext.x,
(i & 2) != 0 ? ext.y : -ext.y,
(i & 4) != 0 ? ext.z : -ext.z
);
Vector3 world = worldMatrix.MultiplyPoint3x4(corner);
aabbMin = Vector3.Min(aabbMin, world);
aabbMax = Vector3.Max(aabbMax, world);
}
bool selected = (gizmoType & GizmoType.Selected) != 0;
Gizmos.color = selected ? new Color(0.2f, 0.8f, 1f, 0.8f) : new Color(0.2f, 0.8f, 1f, 0.3f);
Vector3 c = (aabbMin + aabbMax) * 0.5f;
Vector3 s = aabbMax - aabbMin;
Gizmos.DrawWireCube(c, s);
}
}
}

View File

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

View File

@@ -1,22 +1,192 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
using System.Linq;
namespace SplashEdit.EditorCode
{
[CustomEditor(typeof(PSXSceneExporter))]
public class PSXSceneExporterEditor : Editor
public class PSXSceneExporterEditor : UnityEditor.Editor
{
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 previewRoomsPortalsProp;
private SerializedProperty bvhDepthProp;
private bool showFog = true;
private bool showCutscenes = true;
private bool showDebug = false;
private void OnEnable()
{
gteScalingProp = serializedObject.FindProperty("GTEScaling");
sceneLuaProp = serializedObject.FindProperty("SceneLuaFile");
fogEnabledProp = serializedObject.FindProperty("FogEnabled");
fogColorProp = serializedObject.FindProperty("FogColor");
fogDensityProp = serializedObject.FindProperty("FogDensity");
sceneTypeProp = serializedObject.FindProperty("SceneType");
cutscenesProp = serializedObject.FindProperty("Cutscenes");
loadingScreenProp = serializedObject.FindProperty("LoadingScreenPrefab");
previewBVHProp = serializedObject.FindProperty("PreviewBVH");
previewRoomsPortalsProp = serializedObject.FindProperty("PreviewRoomsPortals");
bvhDepthProp = serializedObject.FindProperty("BVHPreviewDepth");
}
private void OnDisable()
{
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
serializedObject.Update();
var exporter = (PSXSceneExporter)target;
PSXSceneExporter comp = (PSXSceneExporter)target;
if (GUILayout.Button("Export"))
DrawExporterHeader();
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 DrawExporterHeader()
{
comp.Export();
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 & Background", true, PSXEditorStyles.FoldoutHeader);
if (!showFog) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(fogColorProp, new GUIContent("Background Color",
"Background clear color. Also used as the fog blend target when fog is enabled."));
EditorGUILayout.PropertyField(fogEnabledProp, new GUIContent("Distance Fog"));
if (fogEnabledProp.boolValue)
{
EditorGUI.indentLevel++;
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>GTE range: {fogNearUnity:F1} - {fogFarUnity:F1} units | " +
$"{8000f / (density * 3f):F0} - {8000f / density:F0} SZ</color>",
PSXEditorStyles.RichLabel);
EditorGUI.indentLevel--;
}
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"));
EditorGUILayout.PropertyField(previewRoomsPortalsProp, new GUIContent("Preview Rooms/Portals"));
EditorGUI.indentLevel--;
}
private void DrawSceneStats()
{
var exporters = FindObjectsByType<PSXObjectExporter>(FindObjectsSortMode.None);
int total = exporters.Length;
int active = exporters.Count(e => e.IsActive);
int staticCol = exporters.Count(e => e.CollisionType == PSXCollisionType.Static);
int dynamicCol = exporters.Count(e => e.CollisionType == PSXCollisionType.Dynamic);
int triggerBoxes = FindObjectsByType<PSXTriggerBox>(FindObjectsSortMode.None).Length;
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.LabelField(
$"<b>{active}</b>/{total} objects | <b>{staticCol}</b> static <b>{dynamicCol}</b> dynamic <b>{triggerBoxes}</b> triggers",
PSXEditorStyles.RichLabel);
EditorGUILayout.EndVertical();
}
}
}
}

View File

@@ -1,2 +1,2 @@
fileFormatVersion: 2
guid: becf2eb607e7a60baaf3bebe4683d66f
guid: 738efb5c0ed755b45991d2067957b997

View File

@@ -0,0 +1,449 @@
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Manages downloading and updating the psxsplash native project from GitHub releases.
/// Uses the GitHub REST API (HTTP) to list releases and git to clone/checkout
/// (required for recursive submodule support).
/// </summary>
public static class PSXSplashInstaller
{
// ───── Public config ─────
public static readonly string RepoOwner = "psxsplash";
public static readonly string RepoName = "psxsplash";
public static readonly string RepoUrl = "https://github.com/psxsplash/psxsplash.git";
public static readonly string InstallPath = "Assets/psxsplash";
public static readonly string FullInstallPath;
private static readonly string GitHubApiReleasesUrl =
$"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases";
// ───── Cached release list ─────
private static List<ReleaseInfo> _cachedReleases = new List<ReleaseInfo>();
private static bool _isFetchingReleases;
/// <summary>
/// Represents a GitHub release.
/// </summary>
[Serializable]
public class ReleaseInfo
{
public string TagName; // e.g. "v1.2.0"
public string Name; // human-readable name
public string Body; // release notes (markdown)
public string PublishedAt; // ISO 8601 date
public bool IsPrerelease;
public bool IsDraft;
}
static PSXSplashInstaller()
{
FullInstallPath = Path.Combine(Application.dataPath, "psxsplash");
}
// ═══════════════════════════════════════════════════════════════
// Queries
// ═══════════════════════════════════════════════════════════════
/// <summary>Is the native project cloned on disk?</summary>
public static bool IsInstalled()
{
return Directory.Exists(FullInstallPath) &&
Directory.EnumerateFileSystemEntries(FullInstallPath).Any();
}
/// <summary>Are we currently fetching releases from GitHub?</summary>
public static bool IsFetchingReleases => _isFetchingReleases;
/// <summary>Cached list of releases (call FetchReleasesAsync to populate).</summary>
public static IReadOnlyList<ReleaseInfo> CachedReleases => _cachedReleases;
/// <summary>
/// Returns the tag currently checked out, or null if unknown / not a git repo.
/// </summary>
public static string GetCurrentTag()
{
if (!IsInstalled()) return null;
try
{
string result = RunGitCommandSync("describe --tags --exact-match HEAD", FullInstallPath);
return string.IsNullOrWhiteSpace(result) ? null : result.Trim();
}
catch
{
return null;
}
}
// ═══════════════════════════════════════════════════════════════
// Fetch Releases (HTTP — no git required)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Fetches the list of releases from the GitHub REST API.
/// Does NOT require git — uses UnityWebRequest.
/// </summary>
public static async Task<List<ReleaseInfo>> FetchReleasesAsync()
{
_isFetchingReleases = true;
try
{
string json = await HttpGetAsync(GitHubApiReleasesUrl);
if (string.IsNullOrEmpty(json))
{
UnityEngine.Debug.LogWarning("[PSXSplashInstaller] Failed to fetch releases from GitHub.");
return _cachedReleases;
}
var releases = ParseReleasesJson(json);
// Filter out drafts, sort by newest first
releases = releases
.Where(r => !r.IsDraft)
.OrderByDescending(r => r.PublishedAt)
.ToList();
_cachedReleases = releases;
return releases;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Error fetching releases: {ex.Message}");
return _cachedReleases;
}
finally
{
_isFetchingReleases = false;
}
}
// ═══════════════════════════════════════════════════════════════
// Install / Clone at a specific release tag
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Clones the repository at the specified release tag with --recursive.
/// Uses a shallow clone (--depth 1) for speed.
/// Requires git to be installed (submodules cannot be fetched via HTTP archives).
/// </summary>
/// <param name="tag">The release tag to clone, e.g. "v1.2.0". If null, clones the default branch.</param>
/// <param name="onProgress">Optional progress callback.</param>
public static async Task<bool> InstallRelease(string tag, Action<string> onProgress = null)
{
if (IsInstalled())
{
onProgress?.Invoke("Already installed. Use SwitchToRelease to change version.");
return true;
}
if (!IsGitAvailable())
{
UnityEngine.Debug.LogError(
"[PSXSplashInstaller] git is required for recursive submodule clone but was not found on PATH.\n" +
"Please install git: https://git-scm.com/downloads");
return false;
}
try
{
Directory.CreateDirectory(Path.GetDirectoryName(FullInstallPath));
string branchArg = string.IsNullOrEmpty(tag) ? "" : $"--branch {tag}";
string cmd = $"clone --recursive --depth 1 {branchArg} {RepoUrl} \"{FullInstallPath}\"";
onProgress?.Invoke($"Cloning {RepoUrl} at {tag ?? "HEAD"}...");
string result = await RunGitCommandAsync(cmd, Application.dataPath, onProgress);
if (!IsInstalled())
{
UnityEngine.Debug.LogError("[PSXSplashInstaller] Clone completed but directory is empty.");
return false;
}
onProgress?.Invoke("Clone complete.");
return true;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Clone failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Switches an existing clone to a different release tag.
/// Fetches tags, checks out the tag, and updates submodules recursively.
/// </summary>
public static async Task<bool> SwitchToReleaseAsync(string tag, Action<string> onProgress = null)
{
if (!IsInstalled())
{
UnityEngine.Debug.LogError("[PSXSplashInstaller] Not installed — clone first.");
return false;
}
if (!IsGitAvailable())
{
UnityEngine.Debug.LogError("[PSXSplashInstaller] git not found on PATH.");
return false;
}
try
{
onProgress?.Invoke("Fetching tags...");
await RunGitCommandAsync("fetch --tags --depth=1", FullInstallPath, onProgress);
await RunGitCommandAsync($"fetch origin tag {tag} --no-tags", FullInstallPath, onProgress);
onProgress?.Invoke($"Checking out {tag}...");
await RunGitCommandAsync($"checkout {tag}", FullInstallPath, onProgress);
onProgress?.Invoke("Updating submodules...");
await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath, onProgress);
onProgress?.Invoke($"Switched to {tag}.");
return true;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Switch failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Legacy compatibility: Install without specifying a tag (clones default branch).
/// </summary>
public static Task<bool> Install()
{
return InstallRelease(null);
}
/// <summary>
/// Fetches latest remote data (tags, branches).
/// Requires git.
/// </summary>
public static async Task<bool> FetchLatestAsync()
{
if (!IsInstalled()) return false;
try
{
await RunGitCommandAsync("fetch --all --tags", FullInstallPath);
return true;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PSXSplashInstaller] Fetch failed: {ex.Message}");
return false;
}
}
// ═══════════════════════════════════════════════════════════════
// Git helpers
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Checks whether git is available on the system PATH.
/// </summary>
public static bool IsGitAvailable()
{
try
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var p = Process.Start(psi))
{
p.WaitForExit(5000);
return p.ExitCode == 0;
}
}
catch
{
return false;
}
}
private static string RunGitCommandSync(string arguments, string workingDirectory)
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var process = Process.Start(psi))
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit(10000);
return output;
}
}
private static async Task<string> RunGitCommandAsync(
string arguments, string workingDirectory, Action<string> onProgress = null)
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var process = new Process())
{
process.StartInfo = psi;
process.EnableRaisingEvents = true;
var stdout = new System.Text.StringBuilder();
var stderr = new System.Text.StringBuilder();
process.OutputDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
stdout.AppendLine(e.Data);
onProgress?.Invoke(e.Data);
}
};
process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
stderr.AppendLine(e.Data);
// git writes progress to stderr (clone progress, etc.)
onProgress?.Invoke(e.Data);
}
};
var tcs = new TaskCompletionSource<int>();
process.Exited += (s, e) => tcs.TrySetResult(process.ExitCode);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(10));
var completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
if (completedTask == timeoutTask)
{
try { process.Kill(); } catch { }
throw new TimeoutException("Git command timed out after 10 minutes.");
}
int exitCode = await tcs.Task;
process.Dispose();
string output = stdout.ToString();
string error = stderr.ToString();
if (exitCode != 0)
{
UnityEngine.Debug.LogError($"[git {arguments}] exit code {exitCode}\n{error}");
}
return output + error;
}
}
// ═══════════════════════════════════════════════════════════════
// HTTP helpers (no git needed)
// ═══════════════════════════════════════════════════════════════
private static Task<string> HttpGetAsync(string url)
{
var tcs = new TaskCompletionSource<string>();
var request = UnityWebRequest.Get(url);
request.SetRequestHeader("User-Agent", "SplashEdit-Unity");
request.SetRequestHeader("Accept", "application/vnd.github.v3+json");
var op = request.SendWebRequest();
op.completed += _ =>
{
if (request.result == UnityWebRequest.Result.Success)
tcs.TrySetResult(request.downloadHandler.text);
else
{
UnityEngine.Debug.LogWarning($"[PSXSplashInstaller] HTTP GET {url} failed: {request.error}");
tcs.TrySetResult(null);
}
request.Dispose();
};
return tcs.Task;
}
// ═══════════════════════════════════════════════════════════════
// JSON parsing (minimal, avoids external dependency)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Minimal JSON parser for the GitHub releases API response.
/// Uses Unity's JsonUtility via a wrapper since it can't parse top-level arrays.
/// </summary>
private static List<ReleaseInfo> ParseReleasesJson(string json)
{
var releases = new List<ReleaseInfo>();
string wrapped = "{\"items\":" + json + "}";
var wrapper = JsonUtility.FromJson<GitHubReleaseArrayWrapper>(wrapped);
if (wrapper?.items == null) return releases;
foreach (var item in wrapper.items)
{
releases.Add(new ReleaseInfo
{
TagName = item.tag_name ?? "",
Name = item.name ?? item.tag_name ?? "",
Body = item.body ?? "",
PublishedAt = item.published_at ?? "",
IsPrerelease = item.prerelease,
IsDraft = item.draft
});
}
return releases;
}
[Serializable]
private class GitHubReleaseArrayWrapper
{
public GitHubReleaseJson[] items;
}
[Serializable]
private class GitHubReleaseJson
{
public string tag_name;
public string name;
public string body;
public string published_at;
public bool prerelease;
public bool draft;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 72d1da27a16f0794cb1ad49c00799e74

View File

@@ -0,0 +1,104 @@
using UnityEngine;
using UnityEditor;
using SplashEdit.RuntimeCode;
namespace SplashEdit.EditorCode
{
[CustomEditor(typeof(PSXTriggerBox))]
public class PSXTriggerBoxEditor : UnityEditor.Editor
{
private SerializedProperty sizeProp;
private SerializedProperty luaFileProp;
private void OnEnable()
{
sizeProp = serializedObject.FindProperty("size");
luaFileProp = serializedObject.FindProperty("luaFile");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Header card
PSXEditorStyles.BeginCard();
EditorGUILayout.LabelField("PSX Trigger Box", PSXEditorStyles.CardHeaderStyle);
PSXEditorStyles.EndCard();
EditorGUILayout.Space(4);
// Properties card
PSXEditorStyles.BeginCard();
EditorGUILayout.PropertyField(sizeProp, new GUIContent("Size"));
PSXEditorStyles.DrawSeparator(4, 4);
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
if (luaFileProp.objectReferenceValue != null)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
if (GUILayout.Button("Clear", PSXEditorStyles.SecondaryButton, GUILayout.Width(50)))
luaFileProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Create Lua Script", PSXEditorStyles.SecondaryButton, GUILayout.Width(130)))
CreateNewLuaScript();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
PSXEditorStyles.EndCard();
serializedObject.ApplyModifiedProperties();
}
private void CreateNewLuaScript()
{
var trigger = target as PSXTriggerBox;
string defaultName = trigger.gameObject.name.ToLower().Replace(" ", "_");
string path = EditorUtility.SaveFilePanelInProject(
"Create Lua Script", defaultName + ".lua", "lua",
"Create a new Lua script for this trigger box");
if (string.IsNullOrEmpty(path)) return;
string template =
"function onTriggerEnter(triggerIndex)\nend\n\nfunction onTriggerExit(triggerIndex)\nend\n";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
private static void DrawTriggerGizmo(PSXTriggerBox trigger, GizmoType gizmoType)
{
bool selected = (gizmoType & GizmoType.Selected) != 0;
Gizmos.color = selected ? new Color(0.2f, 1f, 0.3f, 0.8f) : new Color(0.2f, 1f, 0.3f, 0.25f);
Gizmos.matrix = trigger.transform.localToWorldMatrix;
Gizmos.DrawWireCube(Vector3.zero, trigger.Size);
if (selected)
{
Gizmos.color = new Color(0.2f, 1f, 0.3f, 0.08f);
Gizmos.DrawCube(Vector3.zero, trigger.Size);
}
Gizmos.matrix = Matrix4x4.identity;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5bdf647efcaa11a469e2e99025e3a20e

View File

@@ -13,11 +13,10 @@ namespace SplashEdit.EditorCode
private Texture2D quantizedTexture;
private Texture2D vramTexture; // VRAM representation of the texture
private List<VRAMPixel> clut; // Color Lookup Table (CLUT), stored as a 1D list
private ushort[] indexedPixelData; // Indexed pixel data for VRAM storage
private PSXBPP bpp = PSXBPP.TEX_4BIT;
private readonly int previewSize = 256;
[MenuItem("Window/Quantized Preview")]
[MenuItem("PlayStation 1/Quantized Preview")]
public static void ShowWindow()
{
// Creates and displays the window
@@ -27,19 +26,25 @@ namespace SplashEdit.EditorCode
private void OnGUI()
{
GUILayout.Label("Quantized Preview", EditorStyles.boldLabel);
GUILayout.Label("Quantized Preview", PSXEditorStyles.WindowHeader);
// Texture input field
PSXEditorStyles.BeginCard();
originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
// Dropdown for bit depth selection
bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp);
EditorGUILayout.Space(4);
// Button to generate the quantized preview
if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null)
if (GUILayout.Button("Generate Quantized Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(26)) && originalTexture != null)
{
GenerateQuantizedPreview();
}
PSXEditorStyles.EndCard();
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.BeginHorizontal();
@@ -47,8 +52,8 @@ namespace SplashEdit.EditorCode
if (originalTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("Original Texture");
DrawTexturePreview(originalTexture, previewSize, false);
GUILayout.Label("Original Texture", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(originalTexture, previewSize);
GUILayout.EndVertical();
}
@@ -56,7 +61,7 @@ namespace SplashEdit.EditorCode
if (vramTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("VRAM View (Indexed Data as 16bpp)");
GUILayout.Label("VRAM View (Indexed Data as 16bpp)", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(vramTexture, previewSize);
GUILayout.EndVertical();
}
@@ -65,7 +70,7 @@ namespace SplashEdit.EditorCode
if (quantizedTexture != null)
{
GUILayout.BeginVertical();
GUILayout.Label("Quantized Texture");
GUILayout.Label("Quantized Texture", PSXEditorStyles.CardHeaderStyle);
DrawTexturePreview(quantizedTexture, previewSize);
GUILayout.EndVertical();
}
@@ -75,37 +80,17 @@ namespace SplashEdit.EditorCode
// Display the Color Lookup Table (CLUT)
if (clut != null)
{
GUILayout.Label("Color Lookup Table (CLUT)");
PSXEditorStyles.DrawSeparator(4, 4);
GUILayout.Label("Color Lookup Table (CLUT)", PSXEditorStyles.SectionHeader);
DrawCLUT();
}
GUILayout.Space(10);
// Export indexed pixel data
if (indexedPixelData != null)
{
if (GUILayout.Button("Export texture data"))
{
string path = EditorUtility.SaveFilePanel("Save texture data", "", "pixel_data", "bin");
if (!string.IsNullOrEmpty(path))
{
using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
using (BinaryWriter writer = new BinaryWriter(fileStream))
{
foreach (ushort value in indexedPixelData)
{
writer.Write(value);
}
}
}
}
}
PSXEditorStyles.DrawSeparator(4, 4);
// Export CLUT data
if (clut != null)
{
if (GUILayout.Button("Export CLUT data"))
if (GUILayout.Button("Export CLUT data", PSXEditorStyles.SecondaryButton, GUILayout.Height(24)))
{
string path = EditorUtility.SaveFilePanel("Save CLUT data", "", "clut_data", "bin");
@@ -139,7 +124,7 @@ namespace SplashEdit.EditorCode
clut = psxTex.ColorPalette;
}
private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true)
private void DrawTexturePreview(Texture2D texture, int size)
{
// Renders a texture preview within the editor window
Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false));

View File

@@ -0,0 +1,84 @@
using UnityEngine;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System;
/// <summary>
/// Utility that detects whether required build tools (MIPS cross-compiler,
/// GNU Make, GDB, etc.) are available on the host system by probing the
/// PATH via <c>where</c> (Windows) or <c>which</c> (Unix).
/// </summary>
namespace SplashEdit.EditorCode
{
public static class ToolchainChecker
{
private static readonly string[] mipsToolSuffixes = new[]
{
"addr2line", "ar", "as", "cpp", "elfedit", "g++", "gcc", "gcc-ar", "gcc-nm",
"gcc-ranlib", "gcov", "ld", "nm", "objcopy", "objdump", "ranlib", "readelf",
"size", "strings", "strip"
};
/// <summary>
/// Returns the full tool names to be checked, based on platform.
/// </summary>
public static string[] GetRequiredTools()
{
string prefix = Application.platform == RuntimePlatform.WindowsEditor
? "mipsel-none-elf-"
: "mipsel-linux-gnu-";
return mipsToolSuffixes.Select(s => prefix + s).ToArray();
}
/// <summary>
/// Checks for availability of any tool (either full name like "make" or "mipsel-*").
/// </summary>
public static bool IsToolAvailable(string toolName)
{
string command = Application.platform == RuntimePlatform.WindowsEditor ? "where" : "which";
try
{
Process process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = toolName,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
if (!string.IsNullOrEmpty(output))
return true;
// Additional fallback for MIPS tools on Windows in local MIPS path
if (Application.platform == RuntimePlatform.WindowsEditor &&
toolName.StartsWith("mipsel-none-elf-"))
{
string localMipsBin = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"mips", "mips", "bin");
string fullPath = Path.Combine(localMipsBin, toolName + ".exe");
return File.Exists(fullPath);
}
return false;
}
catch
{
return false;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 142296fdef504c64bb08110e6f28e581

View File

@@ -0,0 +1,137 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
using UnityEditor;
using System.IO;
namespace SplashEdit.EditorCode
{
/// <summary>
/// Installs the MIPS cross-compiler toolchain and GNU Make.
/// Supports Windows and Linux only.
/// </summary>
public static class ToolchainInstaller
{
private static bool _installing;
public static string MipsVersion = "14.2.0";
/// <summary>
/// Runs an external process and waits for it to exit.
/// </summary>
public static async Task RunCommandAsync(string fileName, string arguments, string workingDirectory = "")
{
if (fileName.Equals("mips", StringComparison.OrdinalIgnoreCase))
{
fileName = "powershell";
string roamingPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string scriptPath = Path.Combine(roamingPath, "mips", "mips.ps1");
arguments = $"-ExecutionPolicy Bypass -File \"{scriptPath}\" {arguments}";
}
var tcs = new TaskCompletionSource<int>();
Process process = new Process();
process.StartInfo.FileName = fileName;
process.StartInfo.Arguments = arguments;
process.StartInfo.CreateNoWindow = false;
process.StartInfo.UseShellExecute = true;
if (!string.IsNullOrEmpty(workingDirectory))
process.StartInfo.WorkingDirectory = workingDirectory;
process.EnableRaisingEvents = true;
process.Exited += (sender, args) =>
{
tcs.SetResult(process.ExitCode);
process.Dispose();
};
process.Start();
int exitCode = await tcs.Task;
if (exitCode != 0)
throw new Exception($"Process '{fileName}' exited with code {exitCode}");
}
/// <summary>
/// Installs the MIPS GCC cross-compiler for the current platform.
/// </summary>
public static async Task<bool> InstallToolchain()
{
if (_installing) return false;
_installing = true;
try
{
if (Application.platform == RuntimePlatform.WindowsEditor)
{
if (!ToolchainChecker.IsToolAvailable("mips"))
{
await RunCommandAsync("powershell",
"-c \"& { iwr -UseBasicParsing https://raw.githubusercontent.com/grumpycoders/pcsx-redux/main/mips.ps1 | iex }\"");
EditorUtility.DisplayDialog("Reboot Required",
"Installing the MIPS toolchain requires a reboot. Please reboot and try again.",
"OK");
return false;
}
else
{
await RunCommandAsync("mips", $"install {MipsVersion}");
}
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
if (ToolchainChecker.IsToolAvailable("apt"))
await RunCommandAsync("pkexec", "apt install g++-mipsel-linux-gnu -y");
else if (ToolchainChecker.IsToolAvailable("trizen"))
await RunCommandAsync("trizen", "-S cross-mipsel-linux-gnu-binutils cross-mipsel-linux-gnu-gcc");
else
throw new Exception("Unsupported Linux distribution. Install mipsel-linux-gnu-gcc manually.");
}
else
{
throw new Exception("Only Windows and Linux are supported.");
}
return true;
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Error",
$"Toolchain installation failed: {ex.Message}", "OK");
return false;
}
finally
{
_installing = false;
}
}
/// <summary>
/// Installs GNU Make. On Windows it is bundled with the MIPS toolchain.
/// </summary>
public static async Task InstallMake()
{
if (Application.platform == RuntimePlatform.WindowsEditor)
{
bool proceed = EditorUtility.DisplayDialog(
"Install GNU Make",
"On Windows, GNU Make is included with the MIPS toolchain installer. Install the full toolchain?",
"Yes", "No");
if (proceed) await InstallToolchain();
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
if (ToolchainChecker.IsToolAvailable("apt"))
await RunCommandAsync("pkexec", "apt install build-essential -y");
else
throw new Exception("Unsupported Linux distribution. Install 'make' manually.");
}
else
{
throw new Exception("Only Windows and Linux are supported.");
}
}
}
}

View File

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

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SplashEdit.RuntimeCode;
using Unity.Collections;
@@ -19,25 +18,16 @@ namespace SplashEdit.EditorCode
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
private Vector2 scrollPosition;
private Texture2D vramImage;
private Vector2 selectedResolution = new Vector2(320, 240);
private bool dualBuffering = true;
private bool verticalLayout = true;
private static readonly Vector2 selectedResolution = new Vector2(320, 240);
private const bool dualBuffering = true;
private const bool verticalLayout = true;
private Color bufferColor1 = new Color(1, 0, 0, 0.5f);
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
private PSXData _psxData;
private PSXFontData[] _cachedFonts;
private static readonly Vector2[] resolutions =
{
new Vector2(256, 240), new Vector2(256, 480),
new Vector2(320, 240), new Vector2(320, 480),
new Vector2(368, 240), new Vector2(368, 480),
new Vector2(512, 240), new Vector2(512, 480),
new Vector2(640, 240), new Vector2(640, 480)
};
private static string[] resolutionsStrings => resolutions.Select(c => $"{c.x}x{c.y}").ToArray();
[MenuItem("Window/VRAM Editor")]
[MenuItem("PlayStation 1/VRAM Editor")]
public static void ShowWindow()
{
VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
@@ -57,7 +47,9 @@ namespace SplashEdit.EditorCode
// Ensure minimum window size is applied.
this.minSize = MinSize;
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
Vector2 ignoredRes;
bool ignoredDb, ignoredVl;
_psxData = DataStorage.LoadData(out ignoredRes, out ignoredDb, out ignoredVl, out prohibitedAreas);
}
/// <summary>
@@ -144,65 +136,75 @@ namespace SplashEdit.EditorCode
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor());
}
}
// Overlay custom font textures into the VRAM preview.
// Fonts live at x=960 (4bpp = 64 VRAM hwords wide), stacking from y=0.
PSXFontData[] fonts;
PSXUIExporter.CollectCanvases(selectedResolution, out fonts);
_cachedFonts = fonts;
if (fonts != null && fonts.Length > 0)
{
foreach (var font in fonts)
{
if (font.PixelData == null || font.PixelData.Length == 0) continue;
int vramX = font.VramX;
int vramY = font.VramY;
int texH = font.TextureHeight;
int bytesPerRow = 256 / 2; // 4bpp: 2 pixels per byte, 256 pixels wide = 128 bytes/row
// Each byte holds two 4bpp pixels. In VRAM, 4 4bpp pixels = 1 16-bit hword.
// So 256 4bpp pixels = 64 VRAM hwords.
for (int y = 0; y < texH && (vramY + y) < VramHeight; y++)
{
for (int x = 0; x < 64 && (vramX + x) < VramWidth; x++)
{
// Read 4 4bpp pixels from this VRAM hword position
int byteIdx = y * bytesPerRow + x * 2;
if (byteIdx + 1 >= font.PixelData.Length) continue;
byte b0 = font.PixelData[byteIdx];
byte b1 = font.PixelData[byteIdx + 1];
// Each byte: low nibble = first pixel, high nibble = second
// 4 pixels per hword: b0 low, b0 high, b1 low, b1 high
bool anyOpaque = ((b0 & 0x0F) | (b0 >> 4) | (b1 & 0x0F) | (b1 >> 4)) != 0;
if (anyOpaque)
{
int px = vramX + x;
int py = VramHeight - 1 - (vramY + y);
if (px < VramWidth && py >= 0)
vramImage.SetPixel(px, py, new Color(0.8f, 0.8f, 1f));
}
}
}
}
}
// Also show system font area (960, 464)-(1023, 511) = 64x48
for (int y = 464; y < 512 && y < VramHeight; y++)
{
for (int x = 960; x < 1024 && x < VramWidth; x++)
{
int py = VramHeight - 1 - y;
Color existing = vramImage.GetPixel(x, py);
if (existing.r < 0.01f && existing.g < 0.01f && existing.b < 0.01f)
vramImage.SetPixel(x, py, new Color(0.3f, 0.3f, 0.5f));
}
}
vramImage.Apply();
// Prompt the user to select a file location and save the VRAM data.
string path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin");
if (path != string.Empty)
{
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
{
for (int y = 0; y < VramHeight; y++)
{
for (int x = 0; x < VramWidth; x++)
{
writer.Write(packed.vramPixels[x, y].Pack());
}
}
}
}
}
private void OnGUI()
{
GUILayout.BeginHorizontal();
GUILayout.BeginVertical();
GUILayout.Label("VRAM Editor", EditorStyles.boldLabel);
GUILayout.Label("VRAM Editor", PSXEditorStyles.WindowHeader);
GUILayout.Label("320x240, dual-buffered, vertical layout", PSXEditorStyles.InfoBox);
// Dropdown for resolution selection.
selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), resolutionsStrings)];
// Check resolution constraints for dual buffering.
bool canDBHorizontal = selectedResolution.x * 2 <= VramWidth;
bool canDBVertical = selectedResolution.y * 2 <= VramHeight;
if (canDBHorizontal || canDBVertical)
{
dualBuffering = EditorGUILayout.Toggle("Dual Buffering", dualBuffering);
}
else
{
dualBuffering = false;
}
if (canDBVertical && canDBHorizontal)
{
verticalLayout = EditorGUILayout.Toggle("Vertical", verticalLayout);
}
else if (canDBVertical)
{
verticalLayout = true;
}
else
{
verticalLayout = false;
}
GUILayout.Space(10);
GUILayout.Label("Prohibited Areas", EditorStyles.boldLabel);
GUILayout.Space(10);
PSXEditorStyles.DrawSeparator(6, 6);
GUILayout.Label("Prohibited Areas", PSXEditorStyles.SectionHeader);
GUILayout.Space(4);
scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true));
@@ -213,10 +215,7 @@ namespace SplashEdit.EditorCode
{
var area = prohibitedAreas[i];
GUI.backgroundColor = new Color(0.95f, 0.95f, 0.95f);
GUILayout.BeginVertical("box");
GUI.backgroundColor = Color.white;
PSXEditorStyles.BeginCard();
// Display fields for editing the area
area.X = EditorGUILayout.IntField("X Coordinate", area.X);
@@ -224,17 +223,16 @@ namespace SplashEdit.EditorCode
area.Width = EditorGUILayout.IntField("Width", area.Width);
area.Height = EditorGUILayout.IntField("Height", area.Height);
if (GUILayout.Button("Remove", GUILayout.Height(30)))
EditorGUILayout.Space(2);
if (GUILayout.Button("Remove", PSXEditorStyles.DangerButton, GUILayout.Height(24)))
{
toRemove.Add(i); // Mark for removal
}
prohibitedAreas[i] = area;
GUILayout.EndVertical();
GUILayout.Space(10);
PSXEditorStyles.EndCard();
GUILayout.Space(4);
}
// Remove the areas marked for deletion outside the loop to avoid skipping elements
@@ -246,19 +244,23 @@ namespace SplashEdit.EditorCode
GUILayout.EndScrollView();
GUILayout.Space(10);
if (GUILayout.Button("Add Prohibited Area"))
if (GUILayout.Button("Add Prohibited Area", PSXEditorStyles.SecondaryButton))
{
prohibitedAreas.Add(new ProhibitedArea());
}
// Button to initiate texture packing.
if (GUILayout.Button("Pack Textures"))
PSXEditorStyles.DrawSeparator(4, 4);
// Button to pack and preview VRAM layout.
if (GUILayout.Button("Pack Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(28)))
{
PackTextures();
}
// Button to save settings; saving now occurs only on button press.
if (GUILayout.Button("Save Settings"))
EditorGUILayout.Space(2);
// Button to save prohibited areas.
if (GUILayout.Button("Save Settings", PSXEditorStyles.SuccessButton, GUILayout.Height(28)))
{
_psxData.OutputResolution = selectedResolution;
_psxData.DualBuffering = dualBuffering;
@@ -297,13 +299,25 @@ namespace SplashEdit.EditorCode
EditorGUI.DrawRect(areaRect, prohibitedColor);
}
// Draw font region overlays.
if (_cachedFonts != null)
{
Color fontColor = new Color(0.2f, 0.4f, 0.9f, 0.25f);
foreach (var font in _cachedFonts)
{
if (font.PixelData == null || font.PixelData.Length == 0) continue;
Rect fontRect = new Rect(vramRect.x + font.VramX, vramRect.y + font.VramY, 64, font.TextureHeight);
EditorGUI.DrawRect(fontRect, fontColor);
GUI.Label(new Rect(fontRect.x + 2, fontRect.y + 2, 60, 16), "Font", EditorStyles.miniLabel);
}
// System font overlay
Rect sysFontRect = new Rect(vramRect.x + 960, vramRect.y + 464, 64, 48);
EditorGUI.DrawRect(sysFontRect, new Color(0.4f, 0.2f, 0.9f, 0.25f));
GUI.Label(new Rect(sysFontRect.x + 2, sysFontRect.y + 2, 60, 16), "SysFont", EditorStyles.miniLabel);
}
GUILayout.EndHorizontal();
}
/// <summary>
/// Stores current configuration to the PSX data asset.
/// This is now triggered manually via the "Save Settings" button.
/// </summary>
}
}

View File

@@ -1,10 +1,13 @@
{
"name": "net.psxplash.splashedit.Editor",
"name": "net.psxsplash.splashedit.Editor",
"rootNamespace": "",
"references": [
"net.psxsplash.splashedit.Runtime"
"net.psxsplash.splashedit.Runtime",
"Unity.AI.Navigation"
],
"includePlatforms": [
"Editor"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7e3500b5974da9723bdd0d457348ea2d
guid: 8bf64a45e6e447140a68258cd60d0ec1
AssemblyDefinitionImporter:
externalObjects: {}
userData:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ab7e1dbd79d3e1101b7d44cdf06a2991
guid: f1210e43ecf5c354486bc01af97ba9eb
folderAsset: yes
DefaultImporter:
externalObjects: {}

BIN
Icons/LuaFile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,12 +1,12 @@
fileFormatVersion: 2
guid: e11677149a517ca5186e32dfda3ec088
guid: 607cfdcd926623447afba2249593f87b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,10 +37,10 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 2
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -94,7 +94,20 @@ TextureImporter:
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1

BIN
Icons/PSXAudioClip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,12 +1,12 @@
fileFormatVersion: 2
guid: d695ef52da250cdcea6c30ab1122c56e
guid: c1ac35b4ac561a6479df60ee4440f138
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,10 +37,10 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 2
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -94,7 +94,20 @@ TextureImporter:
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1

BIN
Icons/PSXCanvas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

143
Icons/PSXCanvas.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 356cfa78fb65c4141a6163492c5a70c9
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXCutsceneClip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 2e44d4c108f1b3b4bbb11d764ee322ba
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXData.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

143
Icons/PSXData.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 56495f2f7c3b793479704907f633cc9f
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXFontAsset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

143
Icons/PSXFontAsset.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 7e7ebf02d9a128040a98c0e8a77f318b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXInteractable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 2693d84ea56d55f41841bccc513aef7a
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

BIN
Icons/PSXObjectExporter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 5dae4156e1023c34db04e1a0133e8366
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,12 +1,12 @@
fileFormatVersion: 2
guid: 4d7bd095e76e6f3df976224b15405059
guid: 67d31a8682e9646419734c5412403992
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,10 +37,10 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 2
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -94,7 +94,20 @@ TextureImporter:
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1

BIN
Icons/PSXPortalLink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: e2c33bdfa2d4f6841abb6f1bd2c3ce4c
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXRoom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

143
Icons/PSXRoom.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: e2a0da16256de3a419a3848add40def9
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,12 +1,12 @@
fileFormatVersion: 2
guid: 0be7a2d4700082dbc83b9274837c70bc
guid: 44fe191eb81c12e4698ab9e87406b878
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
@@ -37,10 +37,10 @@ TextureImporter:
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
@@ -52,9 +52,9 @@ TextureImporter:
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 2
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
@@ -94,7 +94,20 @@ TextureImporter:
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1

BIN
Icons/PSXTriggerBox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 661ef445800490d48bb6486c6b48d7bb
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIBox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

143
Icons/PSXUIBox.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 11ce7fce378375c49a29f10d2c8e1695
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIImage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

143
Icons/PSXUIImage.png.meta Normal file
View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: fbae166a0556be14c906804e97f8ce15
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

BIN
Icons/PSXUIProgressBar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 4ba32ceba8e78ae4dbad95d3fd57c674
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More