using UnityEngine; using UnityEditor; using SplashEdit.RuntimeCode; using System.Linq; using System.Collections.Generic; namespace SplashEdit.EditorCode { /// /// Custom inspector for PSXObjectExporter with enhanced UX. /// Shows mesh info, texture preview, collision visualization, and validation. /// [CustomEditor(typeof(PSXObjectExporter))] [CanEditMultipleObjects] public class PSXObjectExporterEditor : UnityEditor.Editor { // Serialized properties private SerializedProperty isActiveProp; private SerializedProperty bitDepthProp; private SerializedProperty luaFileProp; private SerializedProperty objectFlagsProp; private SerializedProperty collisionTypeProp; private SerializedProperty exportCollisionMeshProp; private SerializedProperty customCollisionMeshProp; private SerializedProperty collisionLayerProp; private SerializedProperty previewNormalsProp; private SerializedProperty normalPreviewLengthProp; private SerializedProperty showCollisionBoundsProp; private SerializedProperty textureProp; // UI State private bool showMeshInfo = true; private bool showTextureInfo = true; private bool showExportSettings = true; private bool showCollisionSettings = true; private bool showGizmoSettings = false; private bool showValidation = true; // Cached data private MeshFilter meshFilter; private MeshRenderer meshRenderer; private int triangleCount; private int vertexCount; private Bounds meshBounds; private List validationErrors = new List(); private List validationWarnings = new List(); // Styles private GUIStyle headerStyle; private GUIStyle errorStyle; private GUIStyle warningStyle; // Validation private bool _validationDirty = true; private void OnEnable() { // Get serialized properties isActiveProp = serializedObject.FindProperty("isActive"); bitDepthProp = serializedObject.FindProperty("bitDepth"); luaFileProp = serializedObject.FindProperty("luaFile"); objectFlagsProp = serializedObject.FindProperty("objectFlags"); collisionTypeProp = serializedObject.FindProperty("collisionType"); exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh"); customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh"); collisionLayerProp = serializedObject.FindProperty("collisionLayer"); previewNormalsProp = serializedObject.FindProperty("previewNormals"); normalPreviewLengthProp = serializedObject.FindProperty("normalPreviewLength"); showCollisionBoundsProp = serializedObject.FindProperty("showCollisionBounds"); textureProp = serializedObject.FindProperty("texture"); // Cache mesh info CacheMeshInfo(); // Defer validation to first inspector draw _validationDirty = true; } private void CacheMeshInfo() { var exporter = target as PSXObjectExporter; if (exporter == null) return; meshFilter = exporter.GetComponent(); meshRenderer = exporter.GetComponent(); if (meshFilter != null && meshFilter.sharedMesh != null) { var mesh = meshFilter.sharedMesh; triangleCount = mesh.triangles.Length / 3; vertexCount = mesh.vertexCount; meshBounds = mesh.bounds; } } private void RunValidation() { validationErrors.Clear(); validationWarnings.Clear(); var exporter = target as PSXObjectExporter; if (exporter == null) return; // Check mesh if (meshFilter == null || meshFilter.sharedMesh == null) { validationErrors.Add("No mesh assigned to MeshFilter"); } else { if (triangleCount > 100) { validationWarnings.Add($"High triangle count ({triangleCount}). PS1 recommended: <100 per object"); } // Check vertex bounds var mesh = meshFilter.sharedMesh; var verts = mesh.vertices; bool hasOutOfBounds = false; foreach (var v in verts) { var world = exporter.transform.TransformPoint(v); float scaled = Mathf.Max(Mathf.Abs(world.x), Mathf.Abs(world.y), Mathf.Abs(world.z)) * 4096f; if (scaled > 32767f) { hasOutOfBounds = true; break; } } if (hasOutOfBounds) { validationErrors.Add("Vertices exceed PS1 coordinate limits (±8 units from origin)"); } } // Check renderer if (meshRenderer == null) { validationWarnings.Add("No MeshRenderer - object will not be visible"); } else if (meshRenderer.sharedMaterial == null) { validationWarnings.Add("No material assigned - will use default colors"); } } public override void OnInspectorGUI() { serializedObject.Update(); // Run deferred validation if (_validationDirty) { RunValidation(); _validationDirty = false; } InitStyles(); // Active toggle at top EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object")); if (!isActiveProp.boolValue) { EditorGUILayout.HelpBox("This object will be skipped during export.", MessageType.Info); serializedObject.ApplyModifiedProperties(); return; } EditorGUILayout.Space(5); // Mesh Info Section DrawMeshInfoSection(); // Texture Section DrawTextureSection(); // Export Settings Section DrawExportSettingsSection(); // Collision Settings Section DrawCollisionSettingsSection(); // Gizmo Settings Section DrawGizmoSettingsSection(); // Validation Section DrawValidationSection(); // Action Buttons DrawActionButtons(); if (serializedObject.ApplyModifiedProperties()) { _validationDirty = true; } } private void InitStyles() { if (headerStyle == null) { headerStyle = new GUIStyle(EditorStyles.foldoutHeader); } if (errorStyle == null) { errorStyle = new GUIStyle(EditorStyles.label); errorStyle.normal.textColor = Color.red; } if (warningStyle == null) { warningStyle = new GUIStyle(EditorStyles.label); warningStyle.normal.textColor = new Color(1f, 0.7f, 0f); } } private void DrawMeshInfoSection() { showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information"); if (showMeshInfo) { EditorGUI.indentLevel++; if (meshFilter != null && meshFilter.sharedMesh != null) { EditorGUILayout.LabelField("Mesh", meshFilter.sharedMesh.name); EditorGUILayout.LabelField("Triangles", triangleCount.ToString()); EditorGUILayout.LabelField("Vertices", vertexCount.ToString()); EditorGUILayout.LabelField("Bounds Size", meshBounds.size.ToString("F2")); // Triangle budget bar float budgetPercent = triangleCount / 100f; Rect rect = EditorGUILayout.GetControlRect(false, 20); EditorGUI.ProgressBar(rect, Mathf.Clamp01(budgetPercent), $"Triangle Budget: {triangleCount}/100"); } else { EditorGUILayout.HelpBox("No mesh assigned", MessageType.Warning); } EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); } private void DrawTextureSection() { showTextureInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showTextureInfo, "Texture Settings"); if (showTextureInfo) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(textureProp, new GUIContent("Override Texture")); EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth")); // Show texture preview if assigned var tex = textureProp.objectReferenceValue as Texture2D; if (tex != null) { EditorGUILayout.Space(5); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); Rect previewRect = GUILayoutUtility.GetRect(64, 64, GUILayout.Width(64)); EditorGUI.DrawPreviewTexture(previewRect, tex); GUILayout.FlexibleSpace(); } EditorGUILayout.LabelField($"Size: {tex.width}x{tex.height}"); // VRAM estimate int bpp = bitDepthProp.enumValueIndex == 0 ? 4 : (bitDepthProp.enumValueIndex == 1 ? 8 : 16); int vramBytes = (tex.width * tex.height * bpp) / 8; EditorGUILayout.LabelField($"Est. VRAM: {vramBytes} bytes ({bpp}bpp)"); } else if (meshRenderer != null && meshRenderer.sharedMaterial != null) { var matTex = meshRenderer.sharedMaterial.mainTexture; if (matTex != null) { EditorGUILayout.HelpBox($"Using material texture: {matTex.name}", MessageType.Info); } } EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); } private void DrawExportSettingsSection() { showExportSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showExportSettings, "Export Settings"); if (showExportSettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(objectFlagsProp, new GUIContent("Object Flags")); EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script")); // Quick Lua file buttons if (luaFileProp.objectReferenceValue != null) { using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Edit Lua", GUILayout.Width(80))) { AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue); } if (GUILayout.Button("Clear", GUILayout.Width(60))) { luaFileProp.objectReferenceValue = null; } } } else { if (GUILayout.Button("Create New Lua Script")) { CreateNewLuaScript(); } } EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); } private void DrawCollisionSettingsSection() { showCollisionSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showCollisionSettings, "Collision Settings"); if (showCollisionSettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Collision Type")); var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex; if (collType != PSXCollisionType.None) { EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh")); EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Collision Mesh")); EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Collision Layer")); // Collision info EditorGUILayout.Space(5); string collisionInfo = collType switch { PSXCollisionType.Solid => "Solid: Blocks movement, fires onCollision", PSXCollisionType.Trigger => "Trigger: Fires onTriggerEnter/Exit, doesn't block", PSXCollisionType.Platform => "Platform: Solid from above only", _ => "" }; EditorGUILayout.HelpBox(collisionInfo, MessageType.Info); } EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); } private void DrawGizmoSettingsSection() { showGizmoSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showGizmoSettings, "Gizmo Settings"); if (showGizmoSettings) { EditorGUI.indentLevel++; EditorGUILayout.PropertyField(previewNormalsProp, new GUIContent("Preview Normals")); if (previewNormalsProp.boolValue) { EditorGUILayout.PropertyField(normalPreviewLengthProp, new GUIContent("Normal Length")); } EditorGUILayout.PropertyField(showCollisionBoundsProp, new GUIContent("Show Collision Bounds")); EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); } private void DrawValidationSection() { if (validationErrors.Count == 0 && validationWarnings.Count == 0) return; showValidation = EditorGUILayout.BeginFoldoutHeaderGroup(showValidation, "Validation"); if (showValidation) { foreach (var error in validationErrors) { EditorGUILayout.HelpBox(error, MessageType.Error); } foreach (var warning in validationWarnings) { EditorGUILayout.HelpBox(warning, MessageType.Warning); } if (GUILayout.Button("Refresh Validation")) { CacheMeshInfo(); RunValidation(); } } EditorGUILayout.EndFoldoutHeaderGroup(); } private void DrawActionButtons() { EditorGUILayout.Space(10); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Select Scene Exporter")) { var exporter = FindObjectOfType(); if (exporter != null) { Selection.activeGameObject = exporter.gameObject; } else { EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK"); } } if (GUILayout.Button("Open Scene Validator")) { PSXSceneValidatorWindow.ShowWindow(); } } } 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)) { string template = $@"-- Lua script for {exporter.gameObject.name} -- -- Available globals: Entity, Vec3, Input, Timer, Camera, Audio, -- Debug, Math, Scene, Persist -- -- Available events: -- onCreate(self) — called once when the object is registered -- onUpdate(self, dt) — called every frame (dt = delta frames, usually 1) -- onEnable(self) — called when the object becomes active -- onDisable(self) — called when the object becomes inactive -- onCollision(self, other) — called on collision with another object -- onTriggerEnter(self, other) -- onTriggerStay(self, other) -- onTriggerExit(self, other) -- onInteract(self) — called when the player interacts -- onButtonPress(self, btn) — called on button press (btn = Input.CROSS etc.) -- onButtonRelease(self, btn) -- onDestroy(self) — called before the object is destroyed -- -- Properties: self.position (Vec3), self.rotationY (pi-units), self.active (bool) function onCreate(self) -- Called once when this object is registered in the scene end function onUpdate(self, dt) -- Called every frame. dt = number of elapsed frames (usually 1). end function onInteract(self) -- Called when the player interacts with this object end "; System.IO.File.WriteAllText(path, template); AssetDatabase.Refresh(); var luaFile = AssetDatabase.LoadAssetAtPath(path); if (luaFile != null) { luaFileProp.objectReferenceValue = luaFile; serializedObject.ApplyModifiedProperties(); } } } [MenuItem("CONTEXT/PSXObjectExporter/Copy Settings to Selected")] private static void CopySettingsToSelected(MenuCommand command) { var source = command.context as PSXObjectExporter; if (source == null) return; foreach (var go in Selection.gameObjects) { var target = go.GetComponent(); if (target != null && target != source) { Undo.RecordObject(target, "Copy PSX Settings"); // Copy via serialized object EditorUtility.CopySerialized(source, target); } } } } }