515 lines
20 KiB
C#
515 lines
20 KiB
C#
using UnityEngine;
|
|
using UnityEditor;
|
|
using SplashEdit.RuntimeCode;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
|
|
namespace SplashEdit.EditorCode
|
|
{
|
|
/// <summary>
|
|
/// Custom inspector for PSXObjectExporter with enhanced UX.
|
|
/// Shows mesh info, texture preview, collision visualization, and validation.
|
|
/// </summary>
|
|
[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<string> validationErrors = new List<string>();
|
|
private List<string> validationWarnings = new List<string>();
|
|
|
|
// 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<MeshFilter>();
|
|
meshRenderer = exporter.GetComponent<MeshRenderer>();
|
|
|
|
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<PSXSceneExporter>();
|
|
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<LuaFile>(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<PSXObjectExporter>();
|
|
if (target != null && target != source)
|
|
{
|
|
Undo.RecordObject(target, "Copy PSX Settings");
|
|
// Copy via serialized object
|
|
EditorUtility.CopySerialized(source, target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|