Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a825d5a707 | |||
| 69d366aaa1 | |||
|
|
7b040f7a3c | ||
|
|
81310d3ac1 | ||
| b65b6ca150 |
7
.gitattributes
vendored
@@ -1,7 +1,6 @@
|
|||||||
#
|
#
|
||||||
# Auto Generated
|
# Auto Generated
|
||||||
#
|
#
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||||
# Unity Binary Assets
|
# Unity Binary Assets
|
||||||
@@ -26,6 +25,7 @@
|
|||||||
*.hdr filter=lfs diff=lfs merge=lfs -text
|
*.hdr filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jpeg 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
|
*.apng filter=lfs diff=lfs merge=lfs -text
|
||||||
*.atsc filter=lfs diff=lfs merge=lfs -text
|
*.atsc filter=lfs diff=lfs merge=lfs -text
|
||||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -69,18 +69,15 @@
|
|||||||
*.wmv filter=lfs diff=lfs merge=lfs -text
|
*.wmv filter=lfs diff=lfs merge=lfs -text
|
||||||
# Unity-Specific
|
# Unity-Specific
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
# Some assets such as lighting data, nav meshes, etc will always be binary,
|
# Some assets such as lighting data, nav meshes, etc will always be binary,
|
||||||
# as will anything with [PreferBinarySerialization]
|
# as will anything with [PreferBinarySerialization]
|
||||||
# (Even if you force it to text mode serialization)
|
# (Even if you force it to text mode serialization)
|
||||||
# Meaning autoCRLF on git will fuck up your project.
|
# Meaning autoCRLF on git will fuck up your project.
|
||||||
*.asset auto
|
*.asset auto
|
||||||
TimeManager.asset -text
|
TimeManager.asset -text
|
||||||
|
|
||||||
#
|
#
|
||||||
# The following should be text to allow git merges
|
# The following should be text to allow git merges
|
||||||
#
|
#
|
||||||
|
|
||||||
*.anim text
|
*.anim text
|
||||||
*.controller text
|
*.controller text
|
||||||
*.mat text
|
*.mat text
|
||||||
@@ -91,5 +88,3 @@ TimeManager.asset -text
|
|||||||
*.unity text
|
*.unity text
|
||||||
*.preset text
|
*.preset text
|
||||||
*.lfs_test filter=lfs diff=lfs merge=lfs -text
|
*.lfs_test filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: f7b9a2e33a4c4754997cf0dd0f20acc8
|
guid: d64fb2a2412d7958ca13d15956c4182b
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 45aea686b641c474dba05b83956d8947
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: b3eaffbb1caed9648b5b57d211ead4d6
|
|
||||||
@@ -1,780 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d27c6a94b1c1f07418799b65d13f7097
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 372b2ef07e125584ba43312b0662d7ac
|
|
||||||
@@ -1,435 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c4e13fc5b859ac14099eb9f259ba11f0
|
|
||||||
@@ -1,777 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8aefa79a412d32c4f8bc8249bb4cd118
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: f68ba273eb88c3b4796e43f40b226c71
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3988772ca929eb14ea3bee6b643de4d0
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5540e6cbefeb70d48a0c1e3843719784
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4765dbe728569d84699a22347e7c14ff
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e39963a5097ad6a48952a0a9d04d1563
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c7043b9e1acbfbe40b9bd9be80e764e5
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 7bd9caaf5a0cb90409cf0acdf17d8d89
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3fd7a7bcc7d0ff841b158f2744d48010
|
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 385b3916e29dc0e48b2866851d1fc1a9
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 66e212c64ebd0a34f9c23febe3e8545d
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 74e983e6cf3376944af7b469023d6e4d
|
|
||||||
@@ -1,789 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: ad1b0e43d59aa0446b4e1d6497e8ee94
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 174ee99c9e9aafd4ea9002fc3548f53d
|
|
||||||
31
Editor/PSXNavMeshEditor.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Editor/PSXNavMeshEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9d3bd83aac4c3ce9ab1698a6a2bc735d
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e6ea40b4c8e02314c9388c86b2920403
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d45032f12fc4b614783ad30927846e6c
|
|
||||||
@@ -1,192 +1,22 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using SplashEdit.RuntimeCode;
|
using SplashEdit.RuntimeCode;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace SplashEdit.EditorCode
|
namespace SplashEdit.EditorCode
|
||||||
{
|
{
|
||||||
[CustomEditor(typeof(PSXSceneExporter))]
|
[CustomEditor(typeof(PSXSceneExporter))]
|
||||||
public class PSXSceneExporterEditor : UnityEditor.Editor
|
public class PSXSceneExporterEditor : 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()
|
public override void OnInspectorGUI()
|
||||||
{
|
{
|
||||||
serializedObject.Update();
|
DrawDefaultInspector();
|
||||||
var exporter = (PSXSceneExporter)target;
|
|
||||||
|
|
||||||
DrawExporterHeader();
|
PSXSceneExporter comp = (PSXSceneExporter)target;
|
||||||
EditorGUILayout.Space(4);
|
if (GUILayout.Button("Export"))
|
||||||
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
|
comp.Export();
|
||||||
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
|
fileFormatVersion: 2
|
||||||
guid: 738efb5c0ed755b45991d2067957b997
|
guid: becf2eb607e7a60baaf3bebe4683d66f
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 72d1da27a16f0794cb1ad49c00799e74
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5bdf647efcaa11a469e2e99025e3a20e
|
|
||||||
@@ -13,10 +13,11 @@ namespace SplashEdit.EditorCode
|
|||||||
private Texture2D quantizedTexture;
|
private Texture2D quantizedTexture;
|
||||||
private Texture2D vramTexture; // VRAM representation of the texture
|
private Texture2D vramTexture; // VRAM representation of the texture
|
||||||
private List<VRAMPixel> clut; // Color Lookup Table (CLUT), stored as a 1D list
|
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 PSXBPP bpp = PSXBPP.TEX_4BIT;
|
||||||
private readonly int previewSize = 256;
|
private readonly int previewSize = 256;
|
||||||
|
|
||||||
[MenuItem("PlayStation 1/Quantized Preview")]
|
[MenuItem("Window/Quantized Preview")]
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
// Creates and displays the window
|
// Creates and displays the window
|
||||||
@@ -26,25 +27,19 @@ namespace SplashEdit.EditorCode
|
|||||||
|
|
||||||
private void OnGUI()
|
private void OnGUI()
|
||||||
{
|
{
|
||||||
GUILayout.Label("Quantized Preview", PSXEditorStyles.WindowHeader);
|
GUILayout.Label("Quantized Preview", EditorStyles.boldLabel);
|
||||||
|
|
||||||
// Texture input field
|
// Texture input field
|
||||||
PSXEditorStyles.BeginCard();
|
|
||||||
originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
|
originalTexture = (Texture2D)EditorGUILayout.ObjectField("Original Texture", originalTexture, typeof(Texture2D), false);
|
||||||
|
|
||||||
// Dropdown for bit depth selection
|
// Dropdown for bit depth selection
|
||||||
bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp);
|
bpp = (PSXBPP)EditorGUILayout.EnumPopup("Bit Depth", bpp);
|
||||||
|
|
||||||
EditorGUILayout.Space(4);
|
|
||||||
|
|
||||||
// Button to generate the quantized preview
|
// Button to generate the quantized preview
|
||||||
if (GUILayout.Button("Generate Quantized Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(26)) && originalTexture != null)
|
if (GUILayout.Button("Generate Quantized Preview") && originalTexture != null)
|
||||||
{
|
{
|
||||||
GenerateQuantizedPreview();
|
GenerateQuantizedPreview();
|
||||||
}
|
}
|
||||||
PSXEditorStyles.EndCard();
|
|
||||||
|
|
||||||
PSXEditorStyles.DrawSeparator(4, 4);
|
|
||||||
|
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
|
|
||||||
@@ -52,8 +47,8 @@ namespace SplashEdit.EditorCode
|
|||||||
if (originalTexture != null)
|
if (originalTexture != null)
|
||||||
{
|
{
|
||||||
GUILayout.BeginVertical();
|
GUILayout.BeginVertical();
|
||||||
GUILayout.Label("Original Texture", PSXEditorStyles.CardHeaderStyle);
|
GUILayout.Label("Original Texture");
|
||||||
DrawTexturePreview(originalTexture, previewSize);
|
DrawTexturePreview(originalTexture, previewSize, false);
|
||||||
GUILayout.EndVertical();
|
GUILayout.EndVertical();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +56,7 @@ namespace SplashEdit.EditorCode
|
|||||||
if (vramTexture != null)
|
if (vramTexture != null)
|
||||||
{
|
{
|
||||||
GUILayout.BeginVertical();
|
GUILayout.BeginVertical();
|
||||||
GUILayout.Label("VRAM View (Indexed Data as 16bpp)", PSXEditorStyles.CardHeaderStyle);
|
GUILayout.Label("VRAM View (Indexed Data as 16bpp)");
|
||||||
DrawTexturePreview(vramTexture, previewSize);
|
DrawTexturePreview(vramTexture, previewSize);
|
||||||
GUILayout.EndVertical();
|
GUILayout.EndVertical();
|
||||||
}
|
}
|
||||||
@@ -70,7 +65,7 @@ namespace SplashEdit.EditorCode
|
|||||||
if (quantizedTexture != null)
|
if (quantizedTexture != null)
|
||||||
{
|
{
|
||||||
GUILayout.BeginVertical();
|
GUILayout.BeginVertical();
|
||||||
GUILayout.Label("Quantized Texture", PSXEditorStyles.CardHeaderStyle);
|
GUILayout.Label("Quantized Texture");
|
||||||
DrawTexturePreview(quantizedTexture, previewSize);
|
DrawTexturePreview(quantizedTexture, previewSize);
|
||||||
GUILayout.EndVertical();
|
GUILayout.EndVertical();
|
||||||
}
|
}
|
||||||
@@ -80,17 +75,37 @@ namespace SplashEdit.EditorCode
|
|||||||
// Display the Color Lookup Table (CLUT)
|
// Display the Color Lookup Table (CLUT)
|
||||||
if (clut != null)
|
if (clut != null)
|
||||||
{
|
{
|
||||||
PSXEditorStyles.DrawSeparator(4, 4);
|
GUILayout.Label("Color Lookup Table (CLUT)");
|
||||||
GUILayout.Label("Color Lookup Table (CLUT)", PSXEditorStyles.SectionHeader);
|
|
||||||
DrawCLUT();
|
DrawCLUT();
|
||||||
}
|
}
|
||||||
|
|
||||||
PSXEditorStyles.DrawSeparator(4, 4);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export CLUT data
|
// Export CLUT data
|
||||||
if (clut != null)
|
if (clut != null)
|
||||||
{
|
{
|
||||||
if (GUILayout.Button("Export CLUT data", PSXEditorStyles.SecondaryButton, GUILayout.Height(24)))
|
if (GUILayout.Button("Export CLUT data"))
|
||||||
{
|
{
|
||||||
string path = EditorUtility.SaveFilePanel("Save CLUT data", "", "clut_data", "bin");
|
string path = EditorUtility.SaveFilePanel("Save CLUT data", "", "clut_data", "bin");
|
||||||
|
|
||||||
@@ -124,7 +139,7 @@ namespace SplashEdit.EditorCode
|
|||||||
clut = psxTex.ColorPalette;
|
clut = psxTex.ColorPalette;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTexturePreview(Texture2D texture, int size)
|
private void DrawTexturePreview(Texture2D texture, int size, bool flipY = true)
|
||||||
{
|
{
|
||||||
// Renders a texture preview within the editor window
|
// Renders a texture preview within the editor window
|
||||||
Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false));
|
Rect rect = GUILayoutUtility.GetRect(size, size, GUILayout.ExpandWidth(false));
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 142296fdef504c64bb08110e6f28e581
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c5aa88b01a3eef145806c8e9e59f4e9d
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using SplashEdit.RuntimeCode;
|
using SplashEdit.RuntimeCode;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
@@ -18,16 +19,25 @@ namespace SplashEdit.EditorCode
|
|||||||
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
|
private List<ProhibitedArea> prohibitedAreas = new List<ProhibitedArea>();
|
||||||
private Vector2 scrollPosition;
|
private Vector2 scrollPosition;
|
||||||
private Texture2D vramImage;
|
private Texture2D vramImage;
|
||||||
private static readonly Vector2 selectedResolution = new Vector2(320, 240);
|
private Vector2 selectedResolution = new Vector2(320, 240);
|
||||||
private const bool dualBuffering = true;
|
private bool dualBuffering = true;
|
||||||
private const bool verticalLayout = true;
|
private bool verticalLayout = true;
|
||||||
private Color bufferColor1 = new Color(1, 0, 0, 0.5f);
|
private Color bufferColor1 = new Color(1, 0, 0, 0.5f);
|
||||||
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
|
private Color bufferColor2 = new Color(0, 1, 0, 0.5f);
|
||||||
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
|
private Color prohibitedColor = new Color(1, 0, 0, 0.3f);
|
||||||
private PSXData _psxData;
|
private PSXData _psxData;
|
||||||
private PSXFontData[] _cachedFonts;
|
|
||||||
|
|
||||||
[MenuItem("PlayStation 1/VRAM Editor")]
|
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")]
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
|
VRAMEditorWindow window = GetWindow<VRAMEditorWindow>("VRAM Editor");
|
||||||
@@ -47,9 +57,7 @@ namespace SplashEdit.EditorCode
|
|||||||
// Ensure minimum window size is applied.
|
// Ensure minimum window size is applied.
|
||||||
this.minSize = MinSize;
|
this.minSize = MinSize;
|
||||||
|
|
||||||
Vector2 ignoredRes;
|
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
|
||||||
bool ignoredDb, ignoredVl;
|
|
||||||
_psxData = DataStorage.LoadData(out ignoredRes, out ignoredDb, out ignoredVl, out prohibitedAreas);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -136,75 +144,65 @@ namespace SplashEdit.EditorCode
|
|||||||
vramImage.SetPixel(x, VramHeight - y - 1, packed.vramPixels[x, y].GetUnityColor());
|
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();
|
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()
|
private void OnGUI()
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
GUILayout.BeginVertical();
|
GUILayout.BeginVertical();
|
||||||
GUILayout.Label("VRAM Editor", PSXEditorStyles.WindowHeader);
|
GUILayout.Label("VRAM Editor", EditorStyles.boldLabel);
|
||||||
GUILayout.Label("320x240, dual-buffered, vertical layout", PSXEditorStyles.InfoBox);
|
|
||||||
|
|
||||||
PSXEditorStyles.DrawSeparator(6, 6);
|
// Dropdown for resolution selection.
|
||||||
GUILayout.Label("Prohibited Areas", PSXEditorStyles.SectionHeader);
|
selectedResolution = resolutions[EditorGUILayout.Popup("Resolution", System.Array.IndexOf(resolutions, selectedResolution), resolutionsStrings)];
|
||||||
GUILayout.Space(4);
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true));
|
scrollPosition = GUILayout.BeginScrollView(scrollPosition, false, true, GUILayout.MinHeight(300f), GUILayout.ExpandWidth(true));
|
||||||
|
|
||||||
@@ -215,7 +213,10 @@ namespace SplashEdit.EditorCode
|
|||||||
{
|
{
|
||||||
var area = prohibitedAreas[i];
|
var area = prohibitedAreas[i];
|
||||||
|
|
||||||
PSXEditorStyles.BeginCard();
|
GUI.backgroundColor = new Color(0.95f, 0.95f, 0.95f);
|
||||||
|
GUILayout.BeginVertical("box");
|
||||||
|
|
||||||
|
GUI.backgroundColor = Color.white;
|
||||||
|
|
||||||
// Display fields for editing the area
|
// Display fields for editing the area
|
||||||
area.X = EditorGUILayout.IntField("X Coordinate", area.X);
|
area.X = EditorGUILayout.IntField("X Coordinate", area.X);
|
||||||
@@ -223,16 +224,17 @@ namespace SplashEdit.EditorCode
|
|||||||
area.Width = EditorGUILayout.IntField("Width", area.Width);
|
area.Width = EditorGUILayout.IntField("Width", area.Width);
|
||||||
area.Height = EditorGUILayout.IntField("Height", area.Height);
|
area.Height = EditorGUILayout.IntField("Height", area.Height);
|
||||||
|
|
||||||
EditorGUILayout.Space(2);
|
|
||||||
if (GUILayout.Button("Remove", PSXEditorStyles.DangerButton, GUILayout.Height(24)))
|
if (GUILayout.Button("Remove", GUILayout.Height(30)))
|
||||||
{
|
{
|
||||||
toRemove.Add(i); // Mark for removal
|
toRemove.Add(i); // Mark for removal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
prohibitedAreas[i] = area;
|
prohibitedAreas[i] = area;
|
||||||
|
|
||||||
PSXEditorStyles.EndCard();
|
GUILayout.EndVertical();
|
||||||
GUILayout.Space(4);
|
GUILayout.Space(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the areas marked for deletion outside the loop to avoid skipping elements
|
// Remove the areas marked for deletion outside the loop to avoid skipping elements
|
||||||
@@ -244,23 +246,19 @@ namespace SplashEdit.EditorCode
|
|||||||
GUILayout.EndScrollView();
|
GUILayout.EndScrollView();
|
||||||
GUILayout.Space(10);
|
GUILayout.Space(10);
|
||||||
|
|
||||||
if (GUILayout.Button("Add Prohibited Area", PSXEditorStyles.SecondaryButton))
|
if (GUILayout.Button("Add Prohibited Area"))
|
||||||
{
|
{
|
||||||
prohibitedAreas.Add(new ProhibitedArea());
|
prohibitedAreas.Add(new ProhibitedArea());
|
||||||
}
|
}
|
||||||
|
|
||||||
PSXEditorStyles.DrawSeparator(4, 4);
|
// Button to initiate texture packing.
|
||||||
|
if (GUILayout.Button("Pack Textures"))
|
||||||
// Button to pack and preview VRAM layout.
|
|
||||||
if (GUILayout.Button("Pack Preview", PSXEditorStyles.PrimaryButton, GUILayout.Height(28)))
|
|
||||||
{
|
{
|
||||||
PackTextures();
|
PackTextures();
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorGUILayout.Space(2);
|
// Button to save settings; saving now occurs only on button press.
|
||||||
|
if (GUILayout.Button("Save Settings"))
|
||||||
// Button to save prohibited areas.
|
|
||||||
if (GUILayout.Button("Save Settings", PSXEditorStyles.SuccessButton, GUILayout.Height(28)))
|
|
||||||
{
|
{
|
||||||
_psxData.OutputResolution = selectedResolution;
|
_psxData.OutputResolution = selectedResolution;
|
||||||
_psxData.DualBuffering = dualBuffering;
|
_psxData.DualBuffering = dualBuffering;
|
||||||
@@ -299,25 +297,13 @@ namespace SplashEdit.EditorCode
|
|||||||
EditorGUI.DrawRect(areaRect, prohibitedColor);
|
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();
|
GUILayout.EndHorizontal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores current configuration to the PSX data asset.
|
||||||
|
/// This is now triggered manually via the "Save Settings" button.
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "net.psxsplash.splashedit.Editor",
|
"name": "net.psxplash.splashedit.Editor",
|
||||||
"rootNamespace": "",
|
"rootNamespace": "",
|
||||||
"references": [
|
"references": [
|
||||||
"net.psxsplash.splashedit.Runtime",
|
"net.psxsplash.splashedit.Runtime"
|
||||||
"Unity.AI.Navigation"
|
|
||||||
],
|
|
||||||
"includePlatforms": [
|
|
||||||
"Editor"
|
|
||||||
],
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
"allowUnsafeCode": false,
|
"allowUnsafeCode": false,
|
||||||
"overrideReferences": false,
|
"overrideReferences": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 8bf64a45e6e447140a68258cd60d0ec1
|
guid: 7e3500b5974da9723bdd0d457348ea2d
|
||||||
AssemblyDefinitionImporter:
|
AssemblyDefinitionImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: f1210e43ecf5c354486bc01af97ba9eb
|
guid: ab7e1dbd79d3e1101b7d44cdf06a2991
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,143 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 607cfdcd926623447afba2249593f87b
|
|
||||||
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: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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
Normal file
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 2e44d4c108f1b3b4bbb11d764ee322ba
|
guid: d695ef52da250cdcea6c30ab1122c56e
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 1
|
enableMipMap: 0
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 0
|
wrapU: 1
|
||||||
wrapV: 0
|
wrapV: 1
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 1
|
nPOTScale: 0
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 0
|
alphaIsTransparency: 1
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 0
|
textureType: 2
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,20 +94,7 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: Android
|
buildTarget: WebGL
|
||||||
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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: 13 KiB After Width: | Height: | Size: 129 B |
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 67d31a8682e9646419734c5412403992
|
guid: 4d7bd095e76e6f3df976224b15405059
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 1
|
enableMipMap: 0
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 0
|
wrapU: 1
|
||||||
wrapV: 0
|
wrapV: 1
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 1
|
nPOTScale: 0
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 0
|
alphaIsTransparency: 1
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 0
|
textureType: 2
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,20 +94,7 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: Android
|
buildTarget: WebGL
|
||||||
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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
Normal file
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: c1ac35b4ac561a6479df60ee4440f138
|
guid: e11677149a517ca5186e32dfda3ec088
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 1
|
enableMipMap: 0
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 0
|
wrapU: 1
|
||||||
wrapV: 0
|
wrapV: 1
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 1
|
nPOTScale: 0
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 0
|
alphaIsTransparency: 1
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 0
|
textureType: 2
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,20 +94,7 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: Android
|
buildTarget: WebGL
|
||||||
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 129 B |
@@ -1,12 +1,12 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 44fe191eb81c12e4698ab9e87406b878
|
guid: 0be7a2d4700082dbc83b9274837c70bc
|
||||||
TextureImporter:
|
TextureImporter:
|
||||||
internalIDToNameTable: []
|
internalIDToNameTable: []
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 13
|
serializedVersion: 13
|
||||||
mipmaps:
|
mipmaps:
|
||||||
mipMapMode: 0
|
mipMapMode: 0
|
||||||
enableMipMap: 1
|
enableMipMap: 0
|
||||||
sRGBTexture: 1
|
sRGBTexture: 1
|
||||||
linearTexture: 0
|
linearTexture: 0
|
||||||
fadeOut: 0
|
fadeOut: 0
|
||||||
@@ -37,10 +37,10 @@ TextureImporter:
|
|||||||
filterMode: 1
|
filterMode: 1
|
||||||
aniso: 1
|
aniso: 1
|
||||||
mipBias: 0
|
mipBias: 0
|
||||||
wrapU: 0
|
wrapU: 1
|
||||||
wrapV: 0
|
wrapV: 1
|
||||||
wrapW: 0
|
wrapW: 0
|
||||||
nPOTScale: 1
|
nPOTScale: 0
|
||||||
lightmap: 0
|
lightmap: 0
|
||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 0
|
spriteMode: 0
|
||||||
@@ -52,9 +52,9 @@ TextureImporter:
|
|||||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
spriteGenerateFallbackPhysicsShape: 1
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
alphaUsage: 1
|
alphaUsage: 1
|
||||||
alphaIsTransparency: 0
|
alphaIsTransparency: 1
|
||||||
spriteTessellationDetail: -1
|
spriteTessellationDetail: -1
|
||||||
textureType: 0
|
textureType: 2
|
||||||
textureShape: 1
|
textureShape: 1
|
||||||
singleChannelComponent: 0
|
singleChannelComponent: 0
|
||||||
flipbookRows: 1
|
flipbookRows: 1
|
||||||
@@ -94,20 +94,7 @@ TextureImporter:
|
|||||||
androidETC2FallbackOverride: 0
|
androidETC2FallbackOverride: 0
|
||||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
- serializedVersion: 4
|
- serializedVersion: 4
|
||||||
buildTarget: Android
|
buildTarget: WebGL
|
||||||
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
|
maxTextureSize: 2048
|
||||||
resizeAlgorithm: 0
|
resizeAlgorithm: 0
|
||||||
textureFormat: -1
|
textureFormat: -1
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,143 +0,0 @@
|
|||||||
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:
|
|
||||||
|
Before Width: | Height: | Size: 9.2 KiB |
@@ -1,143 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 38a1d4112773e114c969699f94844e6a
|
|
||||||
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:
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 07933442bdb4ee14f83fd0b0d0144b8a
|
guid: c1679c9d58898f14494d614dfe5f76a6
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: afedc2b61a424884b90aeb912c54fe50
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||