#if UNITY_EDITOR using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace SplashEdit.RuntimeCode { [CustomEditor(typeof(PSXCutsceneClip))] public class PSXCutsceneEditor : Editor { // ── Preview state ── private bool _showAudioEvents = true; private bool _previewing; private bool _playing; private float _previewFrame; private double _playStartEditorTime; private float _playStartFrame; private HashSet _firedAudioEventIndices = new HashSet(); // Saved scene-view state so we can restore after preview private bool _hasSavedSceneView; private Vector3 _savedPivot; private Quaternion _savedRotation; private float _savedSize; // Saved object transforms private Dictionary _savedObjectPositions = new Dictionary(); private Dictionary _savedObjectRotations = new Dictionary(); private Dictionary _savedObjectActive = new Dictionary(); // Audio preview private Dictionary _audioClipCache = new Dictionary(); private void OnEnable() { EditorApplication.update += OnEditorUpdate; } private void OnDisable() { EditorApplication.update -= OnEditorUpdate; if (_previewing) StopPreview(); } private void OnEditorUpdate() { if (!_playing) return; PSXCutsceneClip clip = (PSXCutsceneClip)target; double elapsed = EditorApplication.timeSinceStartup - _playStartEditorTime; _previewFrame = _playStartFrame + (float)(elapsed * 30.0); if (_previewFrame >= clip.DurationFrames) { _previewFrame = clip.DurationFrames; _playing = false; } ApplyPreview(clip); Repaint(); } public override void OnInspectorGUI() { PSXCutsceneClip clip = (PSXCutsceneClip)target; Undo.RecordObject(clip, "Edit Cutscene"); // ── Header ── EditorGUILayout.Space(4); EditorGUILayout.LabelField("Cutscene Settings", EditorStyles.boldLabel); clip.CutsceneName = EditorGUILayout.TextField("Cutscene Name", clip.CutsceneName); if (!string.IsNullOrEmpty(clip.CutsceneName) && clip.CutsceneName.Length > 24) EditorGUILayout.HelpBox("Name exceeds 24 characters and will be truncated on export.", MessageType.Warning); clip.DurationFrames = EditorGUILayout.IntField("Duration (frames)", clip.DurationFrames); if (clip.DurationFrames < 1) clip.DurationFrames = 1; float seconds = clip.DurationFrames / 30f; EditorGUILayout.LabelField($" = {seconds:F2} seconds at 30fps", EditorStyles.miniLabel); // ── Preview Controls ── EditorGUILayout.Space(6); DrawPreviewControls(clip); // Collect scene references for validation var exporterNames = new HashSet(); var audioNames = new HashSet(); var canvasNames = new HashSet(); var elementNames = new Dictionary>(); // canvas → element names var exporters = Object.FindObjectsByType(FindObjectsSortMode.None); foreach (var e in exporters) exporterNames.Add(e.gameObject.name); var audioSources = Object.FindObjectsByType(FindObjectsSortMode.None); foreach (var a in audioSources) if (!string.IsNullOrEmpty(a.ClipName)) audioNames.Add(a.ClipName); var canvases = Object.FindObjectsByType(FindObjectsSortMode.None); foreach (var c in canvases) { string cName = c.CanvasName ?? ""; if (!string.IsNullOrEmpty(cName)) { canvasNames.Add(cName); if (!elementNames.ContainsKey(cName)) elementNames[cName] = new HashSet(); // Gather all UI element names under this canvas foreach (var box in c.GetComponentsInChildren()) if (!string.IsNullOrEmpty(box.ElementName)) elementNames[cName].Add(box.ElementName); foreach (var txt in c.GetComponentsInChildren()) if (!string.IsNullOrEmpty(txt.ElementName)) elementNames[cName].Add(txt.ElementName); foreach (var bar in c.GetComponentsInChildren()) if (!string.IsNullOrEmpty(bar.ElementName)) elementNames[cName].Add(bar.ElementName); foreach (var img in c.GetComponentsInChildren()) if (!string.IsNullOrEmpty(img.ElementName)) elementNames[cName].Add(img.ElementName); } } // ── Tracks ── EditorGUILayout.Space(8); EditorGUILayout.LabelField("Tracks", EditorStyles.boldLabel); if (clip.Tracks == null) clip.Tracks = new List(); int removeTrackIdx = -1; for (int ti = 0; ti < clip.Tracks.Count; ti++) { var track = clip.Tracks[ti]; EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); track.TrackType = (PSXTrackType)EditorGUILayout.EnumPopup("Type", track.TrackType); if (GUILayout.Button("Remove", GUILayout.Width(65))) removeTrackIdx = ti; EditorGUILayout.EndHorizontal(); bool isCameraTrack = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation; bool isUITrack = track.IsUITrack; bool isUIElementTrack = track.IsUIElementTrack; if (isCameraTrack) { EditorGUI.BeginDisabledGroup(true); EditorGUILayout.TextField("Target", "(camera)"); EditorGUI.EndDisabledGroup(); } else if (isUITrack) { track.UICanvasName = EditorGUILayout.TextField("Canvas Name", track.UICanvasName); if (!string.IsNullOrEmpty(track.UICanvasName) && !canvasNames.Contains(track.UICanvasName)) EditorGUILayout.HelpBox($"No PSXCanvas with name '{track.UICanvasName}' in scene.", MessageType.Error); if (isUIElementTrack) { track.UIElementName = EditorGUILayout.TextField("Element Name", track.UIElementName); if (!string.IsNullOrEmpty(track.UICanvasName) && !string.IsNullOrEmpty(track.UIElementName)) { if (elementNames.TryGetValue(track.UICanvasName, out var elNames) && !elNames.Contains(track.UIElementName)) EditorGUILayout.HelpBox($"No UI element '{track.UIElementName}' found under canvas '{track.UICanvasName}'.", MessageType.Error); } } } else { track.ObjectName = EditorGUILayout.TextField("Object Name", track.ObjectName); // Validation if (!string.IsNullOrEmpty(track.ObjectName) && !exporterNames.Contains(track.ObjectName)) EditorGUILayout.HelpBox($"No PSXObjectExporter found for '{track.ObjectName}' in scene.", MessageType.Error); } // ── Keyframes ── if (track.Keyframes == null) track.Keyframes = new List(); EditorGUI.indentLevel++; EditorGUILayout.LabelField($"Keyframes ({track.Keyframes.Count})", EditorStyles.miniLabel); int removeKfIdx = -1; for (int ki = 0; ki < track.Keyframes.Count; ki++) { var kf = track.Keyframes[ki]; // Row 1: frame number + interp mode + buttons EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Frame", GUILayout.Width(42)); kf.Frame = EditorGUILayout.IntField(kf.Frame, GUILayout.Width(60)); kf.Interp = (PSXInterpMode)EditorGUILayout.EnumPopup(kf.Interp, GUILayout.Width(80)); GUILayout.FlexibleSpace(); // Capture from scene if (isCameraTrack) { if (GUILayout.Button("Capture Cam", GUILayout.Width(90))) { var sv = SceneView.lastActiveSceneView; if (sv != null) kf.Value = track.TrackType == PSXTrackType.CameraPosition ? sv.camera.transform.position : sv.camera.transform.eulerAngles; else Debug.LogWarning("No active Scene View."); } } else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotationY)) { if (GUILayout.Button("From Sel.", GUILayout.Width(70))) { var sel = Selection.activeGameObject; if (sel != null) kf.Value = track.TrackType == PSXTrackType.ObjectPosition ? sel.transform.position : new Vector3(0, sel.transform.eulerAngles.y, 0); else Debug.LogWarning("No GameObject selected."); } } if (GUILayout.Button("\u2212", GUILayout.Width(22))) removeKfIdx = ki; EditorGUILayout.EndHorizontal(); // Row 2: value on its own line EditorGUI.indentLevel++; switch (track.TrackType) { case PSXTrackType.ObjectActive: case PSXTrackType.UICanvasVisible: case PSXTrackType.UIElementVisible: { string label = track.TrackType == PSXTrackType.ObjectActive ? "Active" : "Visible"; bool active = EditorGUILayout.Toggle(label, kf.Value.x > 0.5f); kf.Value = new Vector3(active ? 1f : 0f, 0, 0); break; } case PSXTrackType.ObjectRotationY: { float yRot = EditorGUILayout.FloatField("Y\u00b0", kf.Value.y); kf.Value = new Vector3(0, yRot, 0); break; } case PSXTrackType.UIProgress: { float progress = EditorGUILayout.Slider("Progress %", kf.Value.x, 0f, 100f); kf.Value = new Vector3(progress, 0, 0); break; } case PSXTrackType.UIPosition: { Vector2 pos = EditorGUILayout.Vector2Field("Position (px)", new Vector2(kf.Value.x, kf.Value.y)); kf.Value = new Vector3(pos.x, pos.y, 0); break; } case PSXTrackType.UIColor: { // Show as RGB 0-255 integers EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("R", GUILayout.Width(14)); float r = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.x), 0, 255), GUILayout.Width(40)); EditorGUILayout.LabelField("G", GUILayout.Width(14)); float g = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.y), 0, 255), GUILayout.Width(40)); EditorGUILayout.LabelField("B", GUILayout.Width(14)); float b = EditorGUILayout.IntField(Mathf.Clamp(Mathf.RoundToInt(kf.Value.z), 0, 255), GUILayout.Width(40)); EditorGUILayout.EndHorizontal(); kf.Value = new Vector3(r, g, b); break; } default: kf.Value = EditorGUILayout.Vector3Field("Value", kf.Value); break; } EditorGUI.indentLevel--; if (ki < track.Keyframes.Count - 1) { EditorGUILayout.Space(1); var rect = EditorGUILayout.GetControlRect(false, 1); EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.3f)); } } if (removeKfIdx >= 0) track.Keyframes.RemoveAt(removeKfIdx); // Add keyframe buttons EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("+ Keyframe", GUILayout.Width(90))) { int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0; track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = Vector3.zero }); } if (isCameraTrack) { if (GUILayout.Button("+ from Scene Cam", GUILayout.Width(130))) { var sv = SceneView.lastActiveSceneView; Vector3 val = Vector3.zero; if (sv != null) val = track.TrackType == PSXTrackType.CameraPosition ? sv.camera.transform.position : sv.camera.transform.eulerAngles; int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0; track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val }); } } else if (!isUITrack && (track.TrackType == PSXTrackType.ObjectPosition || track.TrackType == PSXTrackType.ObjectRotationY)) { if (GUILayout.Button("+ from Selected", GUILayout.Width(120))) { var sel = Selection.activeGameObject; Vector3 val = Vector3.zero; if (sel != null) val = track.TrackType == PSXTrackType.ObjectPosition ? sel.transform.position : new Vector3(0, sel.transform.eulerAngles.y, 0); int frame = track.Keyframes.Count > 0 ? track.Keyframes[track.Keyframes.Count - 1].Frame + 15 : 0; track.Keyframes.Add(new PSXKeyframe { Frame = frame, Value = val }); } } EditorGUILayout.EndHorizontal(); EditorGUI.indentLevel--; EditorGUILayout.EndVertical(); EditorGUILayout.Space(2); } if (removeTrackIdx >= 0) clip.Tracks.RemoveAt(removeTrackIdx); if (clip.Tracks.Count < 8) { if (GUILayout.Button("+ Add Track")) clip.Tracks.Add(new PSXCutsceneTrack()); } else { EditorGUILayout.HelpBox("Maximum 8 tracks per cutscene.", MessageType.Info); } // ── Audio Events ── EditorGUILayout.Space(8); _showAudioEvents = EditorGUILayout.Foldout(_showAudioEvents, "Audio Events", true); if (_showAudioEvents) { if (clip.AudioEvents == null) clip.AudioEvents = new List(); int removeEventIdx = -1; for (int ei = 0; ei < clip.AudioEvents.Count; ei++) { var evt = clip.AudioEvents[ei]; EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Frame", GUILayout.Width(42)); evt.Frame = EditorGUILayout.IntField(evt.Frame, GUILayout.Width(60)); GUILayout.FlexibleSpace(); if (GUILayout.Button("\u2212", GUILayout.Width(22))) removeEventIdx = ei; EditorGUILayout.EndHorizontal(); evt.ClipName = EditorGUILayout.TextField("Clip Name", evt.ClipName); if (!string.IsNullOrEmpty(evt.ClipName) && !audioNames.Contains(evt.ClipName)) EditorGUILayout.HelpBox($"No PSXAudioClip with ClipName '{evt.ClipName}' in scene.", MessageType.Error); evt.Volume = EditorGUILayout.IntSlider("Volume", evt.Volume, 0, 128); evt.Pan = EditorGUILayout.IntSlider("Pan", evt.Pan, 0, 127); EditorGUILayout.EndVertical(); } if (removeEventIdx >= 0) clip.AudioEvents.RemoveAt(removeEventIdx); if (clip.AudioEvents.Count < 64) { if (GUILayout.Button("+ Add Audio Event")) clip.AudioEvents.Add(new PSXAudioEvent()); } else { EditorGUILayout.HelpBox("Maximum 64 audio events per cutscene.", MessageType.Info); } } if (GUI.changed) EditorUtility.SetDirty(clip); } // ===================================================================== // Preview Controls // ===================================================================== private void DrawPreviewControls(PSXCutsceneClip clip) { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); // Transport bar EditorGUILayout.BeginHorizontal(); bool wasPlaying = _playing; if (_playing) { if (GUILayout.Button("\u275A\u275A Pause", GUILayout.Width(70))) _playing = false; } else { if (GUILayout.Button("\u25B6 Play", GUILayout.Width(70))) { if (!_previewing) StartPreview(clip); _playing = true; _playStartEditorTime = EditorApplication.timeSinceStartup; _playStartFrame = _previewFrame; _firedAudioEventIndices.Clear(); // Mark already-passed events so they won't fire again if (clip.AudioEvents != null) for (int i = 0; i < clip.AudioEvents.Count; i++) if (clip.AudioEvents[i].Frame < (int)_previewFrame) _firedAudioEventIndices.Add(i); } } if (GUILayout.Button("\u25A0 Stop", GUILayout.Width(60))) { _playing = false; _previewFrame = 0; if (_previewing) StopPreview(); } if (_previewing) { GUI.color = new Color(1f, 0.4f, 0.4f); if (GUILayout.Button("End Preview", GUILayout.Width(90))) { _playing = false; StopPreview(); } GUI.color = Color.white; } EditorGUILayout.EndHorizontal(); // Timeline scrubber EditorGUI.BeginChangeCheck(); float newFrame = EditorGUILayout.Slider("Frame", _previewFrame, 0, clip.DurationFrames); if (EditorGUI.EndChangeCheck()) { if (!_previewing) StartPreview(clip); _previewFrame = newFrame; _playing = false; _firedAudioEventIndices.Clear(); ApplyPreview(clip); } float previewSec = _previewFrame / 30f; EditorGUILayout.LabelField( $" {(int)_previewFrame} / {clip.DurationFrames} ({previewSec:F2}s / {seconds(clip):F2}s)", EditorStyles.miniLabel); if (_previewing) EditorGUILayout.HelpBox( "PREVIEWING: Scene View camera & objects are being driven. " + "Click \u201cEnd Preview\u201d or \u201cStop\u201d to restore original positions.", MessageType.Warning); EditorGUILayout.EndVertical(); } private static float seconds(PSXCutsceneClip clip) => clip.DurationFrames / 30f; // ===================================================================== // Preview Lifecycle // ===================================================================== private void StartPreview(PSXCutsceneClip clip) { if (_previewing) return; _previewing = true; _firedAudioEventIndices.Clear(); // Save scene view camera var sv = SceneView.lastActiveSceneView; if (sv != null) { _hasSavedSceneView = true; _savedPivot = sv.pivot; _savedRotation = sv.rotation; _savedSize = sv.size; } // Save object transforms _savedObjectPositions.Clear(); _savedObjectRotations.Clear(); _savedObjectActive.Clear(); if (clip.Tracks != null) { foreach (var track in clip.Tracks) { if (string.IsNullOrEmpty(track.ObjectName)) continue; bool isCam = track.TrackType == PSXTrackType.CameraPosition || track.TrackType == PSXTrackType.CameraRotation; if (isCam) continue; var go = GameObject.Find(track.ObjectName); if (go == null) continue; if (!_savedObjectPositions.ContainsKey(track.ObjectName)) { _savedObjectPositions[track.ObjectName] = go.transform.position; _savedObjectRotations[track.ObjectName] = go.transform.rotation; _savedObjectActive[track.ObjectName] = go.activeSelf; } } } // Build audio clip lookup _audioClipCache.Clear(); var audioSources = Object.FindObjectsByType(FindObjectsSortMode.None); foreach (var a in audioSources) if (!string.IsNullOrEmpty(a.ClipName) && a.Clip != null) _audioClipCache[a.ClipName] = a.Clip; } private void StopPreview() { if (!_previewing) return; _previewing = false; _playing = false; // Restore scene view camera if (_hasSavedSceneView) { var sv = SceneView.lastActiveSceneView; if (sv != null) { sv.pivot = _savedPivot; sv.rotation = _savedRotation; sv.size = _savedSize; sv.Repaint(); } _hasSavedSceneView = false; } // Restore object transforms foreach (var kvp in _savedObjectPositions) { var go = GameObject.Find(kvp.Key); if (go == null) continue; go.transform.position = kvp.Value; if (_savedObjectRotations.ContainsKey(kvp.Key)) go.transform.rotation = _savedObjectRotations[kvp.Key]; if (_savedObjectActive.ContainsKey(kvp.Key)) go.SetActive(_savedObjectActive[kvp.Key]); } _savedObjectPositions.Clear(); _savedObjectRotations.Clear(); _savedObjectActive.Clear(); SceneView.RepaintAll(); Repaint(); } // ===================================================================== // Apply Preview at Current Frame // ===================================================================== private void ApplyPreview(PSXCutsceneClip clip) { if (!_previewing) return; float frame = _previewFrame; var sv = SceneView.lastActiveSceneView; Vector3? camPos = null; Quaternion? camRot = null; if (clip.Tracks != null) { foreach (var track in clip.Tracks) { // Compute initial value for pre-first-keyframe blending Vector3 initialVal = Vector3.zero; switch (track.TrackType) { case PSXTrackType.CameraPosition: if (sv != null) // Recover position from saved pivot/rotation/size initialVal = _savedPivot - _savedRotation * Vector3.forward * _savedSize; break; case PSXTrackType.CameraRotation: initialVal = _savedRotation.eulerAngles; break; case PSXTrackType.ObjectPosition: if (_savedObjectPositions.ContainsKey(track.ObjectName ?? "")) initialVal = _savedObjectPositions[track.ObjectName]; break; case PSXTrackType.ObjectRotationY: if (_savedObjectRotations.ContainsKey(track.ObjectName ?? "")) initialVal = new Vector3(0, _savedObjectRotations[track.ObjectName].eulerAngles.y, 0); break; case PSXTrackType.ObjectActive: if (_savedObjectActive.ContainsKey(track.ObjectName ?? "")) initialVal = new Vector3(_savedObjectActive[track.ObjectName] ? 1f : 0f, 0, 0); break; // UI tracks: initial values stay zero (no scene preview state to capture) case PSXTrackType.UICanvasVisible: case PSXTrackType.UIElementVisible: initialVal = new Vector3(1f, 0, 0); // assume visible by default break; case PSXTrackType.UIProgress: case PSXTrackType.UIPosition: case PSXTrackType.UIColor: break; // zero is fine } Vector3 val = EvaluateTrack(track, frame, initialVal); switch (track.TrackType) { case PSXTrackType.CameraPosition: camPos = val; break; case PSXTrackType.CameraRotation: camRot = Quaternion.Euler(val); break; case PSXTrackType.ObjectPosition: { var go = GameObject.Find(track.ObjectName); if (go != null) go.transform.position = val; break; } case PSXTrackType.ObjectRotationY: { var go = GameObject.Find(track.ObjectName); if (go != null) go.transform.rotation = Quaternion.Euler(0, val.y, 0); break; } case PSXTrackType.ObjectActive: { var go = GameObject.Find(track.ObjectName); if (go != null) go.SetActive(val.x > 0.5f); break; } // UI tracks: no scene preview, values are applied on PS1 only case PSXTrackType.UICanvasVisible: case PSXTrackType.UIElementVisible: case PSXTrackType.UIProgress: case PSXTrackType.UIPosition: case PSXTrackType.UIColor: break; } } } // Drive scene view camera if (sv != null && (camPos.HasValue || camRot.HasValue)) { Vector3 pos = camPos ?? sv.camera.transform.position; Quaternion rot = camRot ?? sv.camera.transform.rotation; // SceneView needs pivot and rotation set — pivot = position + forward * size sv.rotation = rot; sv.pivot = pos + rot * Vector3.forward * sv.cameraDistance; sv.Repaint(); } // Fire audio events (only during playback, not scrubbing) if (_playing && clip.AudioEvents != null) { for (int i = 0; i < clip.AudioEvents.Count; i++) { if (_firedAudioEventIndices.Contains(i)) continue; var evt = clip.AudioEvents[i]; if (frame >= evt.Frame) { _firedAudioEventIndices.Add(i); PlayAudioPreview(evt); } } } } // ===================================================================== // Track Evaluation (linear interpolation, matching C++ runtime) // ===================================================================== private static Vector3 EvaluateTrack(PSXCutsceneTrack track, float frame, Vector3 initialValue) { if (track.Keyframes == null || track.Keyframes.Count == 0) return Vector3.zero; // Step interpolation tracks: ObjectActive, UICanvasVisible, UIElementVisible if (track.TrackType == PSXTrackType.ObjectActive || track.TrackType == PSXTrackType.UICanvasVisible || track.TrackType == PSXTrackType.UIElementVisible) { if (track.Keyframes.Count > 0 && track.Keyframes[0].Frame > 0 && frame < track.Keyframes[0].Frame) return initialValue; return EvaluateStep(track.Keyframes, frame); } // Find surrounding keyframes PSXKeyframe before = null, after = null; for (int i = 0; i < track.Keyframes.Count; i++) { if (track.Keyframes[i].Frame <= frame) before = track.Keyframes[i]; if (track.Keyframes[i].Frame >= frame && after == null) after = track.Keyframes[i]; } if (before == null && after == null) return Vector3.zero; // Pre-first-keyframe: blend from initial value to first keyframe if (before == null && after != null && after.Frame > 0 && frame < after.Frame) { float rawT = frame / after.Frame; float t = ApplyInterpCurve(rawT, after.Interp); bool isRotation = track.TrackType == PSXTrackType.CameraRotation || track.TrackType == PSXTrackType.ObjectRotationY; if (isRotation) { return new Vector3( Mathf.LerpAngle(initialValue.x, after.Value.x, t), Mathf.LerpAngle(initialValue.y, after.Value.y, t), Mathf.LerpAngle(initialValue.z, after.Value.z, t)); } return Vector3.Lerp(initialValue, after.Value, t); } if (before == null) return after.Value; if (after == null) return before.Value; if (before == after) return before.Value; float span = after.Frame - before.Frame; float rawT2 = (frame - before.Frame) / span; float t2 = ApplyInterpCurve(rawT2, after.Interp); // Use shortest-path angle interpolation for rotation tracks bool isRot = track.TrackType == PSXTrackType.CameraRotation || track.TrackType == PSXTrackType.ObjectRotationY; if (isRot) { return new Vector3( Mathf.LerpAngle(before.Value.x, after.Value.x, t2), Mathf.LerpAngle(before.Value.y, after.Value.y, t2), Mathf.LerpAngle(before.Value.z, after.Value.z, t2)); } return Vector3.Lerp(before.Value, after.Value, t2); } /// /// Apply easing curve to a linear t value (0..1). Matches the C++ applyCurve(). /// private static float ApplyInterpCurve(float t, PSXInterpMode mode) { switch (mode) { default: case PSXInterpMode.Linear: return t; case PSXInterpMode.Step: return 0f; case PSXInterpMode.EaseIn: return t * t; case PSXInterpMode.EaseOut: return t * (2f - t); case PSXInterpMode.EaseInOut: return t * t * (3f - 2f * t); } } private static Vector3 EvaluateStep(List keyframes, float frame) { Vector3 result = Vector3.zero; for (int i = 0; i < keyframes.Count; i++) { if (keyframes[i].Frame <= frame) result = keyframes[i].Value; } return result; } // ===================================================================== // Audio Preview // ===================================================================== private void PlayAudioPreview(PSXAudioEvent evt) { if (string.IsNullOrEmpty(evt.ClipName)) return; if (!_audioClipCache.TryGetValue(evt.ClipName, out AudioClip clip)) return; // Use Unity's editor audio playback utility via reflection // (PlayClipAtPoint doesn't work in edit mode) var unityEditorAssembly = typeof(AudioImporter).Assembly; var audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil"); if (audioUtilClass == null) return; // Stop any previous preview var stopMethod = audioUtilClass.GetMethod("StopAllPreviewClips", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); stopMethod?.Invoke(null, null); // Play the clip var playMethod = audioUtilClass.GetMethod("PlayPreviewClip", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) }, null); playMethod?.Invoke(null, new object[] { clip, 0, false }); } } } #endif