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