using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using SplashEdit.RuntimeCode; namespace SplashEdit.EditorCode { /// /// Scene Validator Window - Validates the current scene for PS1 compatibility. /// Checks for common issues that would cause problems on real hardware. /// public class PSXSceneValidatorWindow : EditorWindow { private Vector2 scrollPosition; private List validationResults = new List(); 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("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(); 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(); 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(); 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(); 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(); 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(); 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(); int totalTris = 0; foreach (var exporter in exporters) { var mf = exporter.GetComponent(); 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(); 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; } } }