Broken RUntime

This commit is contained in:
Jan Racek
2026-03-27 13:47:18 +01:00
parent 6bf74fa929
commit d29ef569b3
16 changed files with 1168 additions and 1371 deletions

View File

@@ -2,512 +2,233 @@ 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 staticColliderProp;
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 SerializedProperty generateNavigationProp;
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 bool showExport = true;
private bool showCollision = 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");
staticColliderProp = serializedObject.FindProperty("staticCollider");
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
generateNavigationProp = serializedObject.FindProperty("generateNavigation");
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;
triangleCount = meshFilter.sharedMesh.triangles.Length / 3;
vertexCount = meshFilter.sharedMesh.vertexCount;
}
}
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"));
DrawHeader();
EditorGUILayout.Space(4);
if (!isActiveProp.boolValue)
{
EditorGUILayout.HelpBox("This object will be skipped during export.", MessageType.Info);
EditorGUILayout.LabelField("Object will be skipped during export.", PSXEditorStyles.InfoBox);
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;
}
DrawMeshSummary();
PSXEditorStyles.DrawSeparator(6, 6);
DrawExportSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawCollisionSection();
PSXEditorStyles.DrawSeparator(6, 6);
DrawActions();
serializedObject.ApplyModifiedProperties();
}
private void InitStyles()
private new void DrawHeader()
{
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);
}
EditorGUILayout.BeginVertical(PSXEditorStyles.CardStyle);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(isActiveProp, GUIContent.none, GUILayout.Width(18));
var exporter = target as PSXObjectExporter;
EditorGUILayout.LabelField(exporter.gameObject.name, PSXEditorStyles.CardHeaderStyle);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private void DrawMeshInfoSection()
private void DrawMeshSummary()
{
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information");
if (showMeshInfo)
if (meshFilter == null || meshFilter.sharedMesh == null)
{
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)
EditorGUILayout.HelpBox("No mesh on this object.", MessageType.Warning);
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();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"{triangleCount} tris", PSXEditorStyles.RichLabel, GUILayout.Width(60));
EditorGUILayout.LabelField($"{vertexCount} verts", PSXEditorStyles.RichLabel, GUILayout.Width(70));
int subMeshCount = meshFilter.sharedMesh.subMeshCount;
if (subMeshCount > 1)
EditorGUILayout.LabelField($"{subMeshCount} submeshes", PSXEditorStyles.RichLabel, GUILayout.Width(90));
int matCount = meshRenderer != null ? meshRenderer.sharedMaterials.Length : 0;
int textured = meshRenderer != null
? meshRenderer.sharedMaterials.Count(m => m != null && m.mainTexture != null)
: 0;
if (textured > 0)
EditorGUILayout.LabelField($"{textured}/{matCount} textured", PSXEditorStyles.RichLabel);
else
EditorGUILayout.LabelField("untextured", PSXEditorStyles.RichLabel);
EditorGUILayout.EndHorizontal();
}
private void DrawActionButtons()
private void DrawExportSection()
{
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
showExport = EditorGUILayout.Foldout(showExport, "Export", true, PSXEditorStyles.FoldoutHeader);
if (!showExport) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
if (luaFileProp.objectReferenceValue != null)
{
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();
}
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Edit", EditorStyles.miniButtonLeft, GUILayout.Width(50)))
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
if (GUILayout.Button("Clear", EditorStyles.miniButtonRight, GUILayout.Width(50)))
luaFileProp.objectReferenceValue = null;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
if (GUILayout.Button("Create Lua Script", EditorStyles.miniButton, GUILayout.Width(130)))
CreateNewLuaScript();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
}
private void DrawCollisionSection()
{
showCollision = EditorGUILayout.Foldout(showCollision, "Collision", true, PSXEditorStyles.FoldoutHeader);
if (!showCollision) return;
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Type"));
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
if (collType != PSXCollisionType.None)
{
EditorGUILayout.PropertyField(staticColliderProp, new GUIContent("Static"));
bool isStatic = staticColliderProp.boolValue;
if (isStatic)
{
EditorGUILayout.LabelField(
"<color=#88cc88>Baked into world collision mesh. No runtime cost.</color>",
PSXEditorStyles.RichLabel);
}
else
{
EditorGUILayout.LabelField(
"<color=#88aaff>Runtime AABB collider. Fires Lua collision events.</color>",
PSXEditorStyles.RichLabel);
}
EditorGUILayout.Space(2);
EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Mesh"));
EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Layer"));
}
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(generateNavigationProp, new GUIContent("Generate Navigation"));
EditorGUI.indentLevel--;
}
private void DrawActions()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select Scene Exporter", EditorStyles.miniButton))
{
var se = FindFirstObjectByType<PSXSceneExporter>();
if (se != null)
Selection.activeGameObject = se.gameObject;
else
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
}
EditorGUILayout.EndHorizontal();
}
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 Lua Script", defaultName + ".lua", "lua",
"Create a new Lua script for this object");
if (!string.IsNullOrEmpty(path))
if (string.IsNullOrEmpty(path)) return;
string template =
$"function onCreate(self)\nend\n\nfunction onUpdate(self, dt)\nend\n";
System.IO.File.WriteAllText(path, template);
AssetDatabase.Refresh();
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
if (luaFile != null)
{
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);
}
luaFileProp.objectReferenceValue = luaFile;
serializedObject.ApplyModifiedProperties();
}
}
}