Files
secretsplash/Editor/PSXSceneValidatorWindow.cs
Jan Racek 4aa4e49424 psst
2026-03-24 13:00:54 +01:00

497 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}