Compare commits
32 Commits
a825d5a707
...
lua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01b636f3e2 | ||
|
|
61fbca17a7 | ||
| 275b4e891d | |||
| 132ab479c2 | |||
| eff03e0e1a | |||
| 5e862f8c0b | |||
| 62bf7d8b2d | |||
|
|
a251eeaed5 | ||
|
|
13ed569eaf | ||
|
|
45a552be5a | ||
|
|
24d0c1fa07 | ||
|
|
1c48b8b425 | ||
|
|
d29ef569b3 | ||
|
|
6bf74fa929 | ||
|
|
d5be174247 | ||
|
|
a8aa674a9c | ||
|
|
5fffcea6cf | ||
|
|
8914ba35cc | ||
|
|
bb8e0804f5 | ||
|
|
4aa4e49424 | ||
| 53e993f58e | |||
| 0d1e363dbb | |||
| ac0e4d8420 | |||
| 9af5d7dd1a | |||
| dc9bfcb155 | |||
| 849e221b32 | |||
| 2013e31b04 | |||
|
|
4cebe93c34 | ||
|
|
551eb4c0de | ||
|
|
ecb1422937 | ||
| a07a715d19 | |||
| b3da188438 |
7
.gitattributes
vendored
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 311ff9868024b5078bd12a6c2252a4ef
|
||||
guid: d7e9b1c3e60e2ff48be3cd61902ba6f1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
BIN
Data/SPLASHLICENSE.DAT
Normal file
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 871e1f08910e9f329ad3fce5d77c8785
|
||||
guid: 244f6913a02805e4aa3cebdd1240cab7
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d64fb2a2412d7958ca13d15956c4182b
|
||||
guid: f7b9a2e33a4c4754997cf0dd0f20acc8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf7149f85c29d3f4d9a7f968d8825ffa
|
||||
guid: 8e74ebc4b575d27499f7abd4d82b8849
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
197
Editor/Core/MkpsxisoDownloader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/MkpsxisoDownloader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45aea686b641c474dba05b83956d8947
|
||||
241
Editor/Core/PCSXReduxDownloader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/PCSXReduxDownloader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3eaffbb1caed9648b5b57d211ead4d6
|
||||
780
Editor/Core/PCdrvSerialHost.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/PCdrvSerialHost.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d27c6a94b1c1f07418799b65d13f7097
|
||||
283
Editor/Core/PSXAudioConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/PSXAudioConverter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 372b2ef07e125584ba43312b0662d7ac
|
||||
435
Editor/Core/PSXConsoleWindow.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/PSXConsoleWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4e13fc5b859ac14099eb9f259ba11f0
|
||||
777
Editor/Core/PSXEditorStyles.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/PSXEditorStyles.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8aefa79a412d32c4f8bc8249bb4cd118
|
||||
188
Editor/Core/SceneMemoryReport.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/SceneMemoryReport.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f68ba273eb88c3b4796e43f40b226c71
|
||||
251
Editor/Core/SplashBuildPaths.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/SplashBuildPaths.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3988772ca929eb14ea3bee6b643de4d0
|
||||
2677
Editor/Core/SplashControlPanel.cs
Normal file
2
Editor/Core/SplashControlPanel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5540e6cbefeb70d48a0c1e3843719784
|
||||
214
Editor/Core/SplashSettings.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/SplashSettings.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4765dbe728569d84699a22347e7c14ff
|
||||
549
Editor/Core/UniromUploader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Core/UniromUploader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e39963a5097ad6a48952a0a9d04d1563
|
||||
39
Editor/DependencyCheckInitializer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/DependencyCheckInitializer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7043b9e1acbfbe40b9bd9be80e764e5
|
||||
@@ -1,6 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1944ac962a00b23c2a880b5134cdc7ab
|
||||
TextScriptImporter:
|
||||
guid: 0279126b700b37d4485c1f4f1ae44e54
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
122
Editor/Inspectors/PSXComponentEditors.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Inspectors/PSXComponentEditors.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bd9caaf5a0cb90409cf0acdf17d8d89
|
||||
271
Editor/Inspectors/PSXComponentEditors2.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Inspectors/PSXComponentEditors2.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fd7a7bcc7d0ff841b158f2744d48010
|
||||
617
Editor/Inspectors/PSXUIEditors.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/Inspectors/PSXUIEditors.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 385b3916e29dc0e48b2866851d1fc1a9
|
||||
43
Editor/LuaFileAssetEditor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/LuaFileAssetEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66e212c64ebd0a34f9c23febe3e8545d
|
||||
25
Editor/LuaImporter.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/LuaImporter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74e983e6cf3376944af7b469023d6e4d
|
||||
789
Editor/PSXCutsceneEditor.cs
Normal 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
|
||||
2
Editor/PSXCutsceneEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad1b0e43d59aa0446b4e1d6497e8ee94
|
||||
80
Editor/PSXMenuItems.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/PSXMenuItems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 174ee99c9e9aafd4ea9002fc3548f53d
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d3bd83aac4c3ce9ab1698a6a2bc735d
|
||||
401
Editor/PSXNavRegionEditor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/PSXNavRegionEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6ea40b4c8e02314c9388c86b2920403
|
||||
247
Editor/PSXObjectExporterEditor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/PSXObjectExporterEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d45032f12fc4b614783ad30927846e6c
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: becf2eb607e7a60baaf3bebe4683d66f
|
||||
guid: 738efb5c0ed755b45991d2067957b997
|
||||
449
Editor/PSXSplashInstaller.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/PSXSplashInstaller.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72d1da27a16f0794cb1ad49c00799e74
|
||||
104
Editor/PSXTriggerBoxEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/PSXTriggerBoxEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5bdf647efcaa11a469e2e99025e3a20e
|
||||
@@ -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));
|
||||
|
||||
84
Editor/ToolchainChecker.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/ToolchainChecker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 142296fdef504c64bb08110e6f28e581
|
||||
137
Editor/ToolchainInstaller.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/ToolchainInstaller.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5aa88b01a3eef145806c8e9e59f4e9d
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e3500b5974da9723bdd0d457348ea2d
|
||||
guid: 8bf64a45e6e447140a68258cd60d0ec1
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab7e1dbd79d3e1101b7d44cdf06a2991
|
||||
guid: f1210e43ecf5c354486bc01af97ba9eb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
|
||||
BIN
Icons/LuaFile.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
@@ -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
|
After Width: | Height: | Size: 16 KiB |
@@ -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
|
After Width: | Height: | Size: 13 KiB |
143
Icons/PSXCanvas.png.meta
Normal 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
|
After Width: | Height: | Size: 12 KiB |
143
Icons/PSXCutsceneClip.png.meta
Normal 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
|
After Width: | Height: | Size: 19 KiB |
143
Icons/PSXData.png.meta
Normal 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
|
After Width: | Height: | Size: 15 KiB |
143
Icons/PSXFontAsset.png.meta
Normal 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
|
After Width: | Height: | Size: 12 KiB |
143
Icons/PSXInteractable.png.meta
Normal 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:
|
||||
BIN
Icons/PSXNavmesh.png
LFS
BIN
Icons/PSXObjectExporter.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
143
Icons/PSXObjectExporter.png.meta
Normal 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:
|
||||
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 13 KiB |
@@ -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
|
After Width: | Height: | Size: 13 KiB |
143
Icons/PSXPortalLink.png.meta
Normal 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
|
After Width: | Height: | Size: 11 KiB |
143
Icons/PSXRoom.png.meta
Normal 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:
|
||||
BIN
Icons/PSXSObjectExporter.png
LFS
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 13 KiB |
@@ -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
|
After Width: | Height: | Size: 13 KiB |
143
Icons/PSXTriggerBox.png.meta
Normal 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
|
After Width: | Height: | Size: 10 KiB |
143
Icons/PSXUIBox.png.meta
Normal 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
|
After Width: | Height: | Size: 14 KiB |
143
Icons/PSXUIImage.png.meta
Normal 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
|
After Width: | Height: | Size: 13 KiB |
143
Icons/PSXUIProgressBar.png.meta
Normal 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:
|
||||