From ce52a617927f9fd778ca48c27151a23dc4a3fc31 Mon Sep 17 00:00:00 2001 From: yum Date: Mon, 18 Aug 2025 18:07:25 -0700 Subject: Improve error reporting --- Scripts/YOTSConfig.cs | 142 +++++++++++++++++++++++++++++++++++++++++++ Scripts/YOTSCore.cs | 7 ++- Scripts/YOTSNDMFConfig.cs | 142 ------------------------------------------- Scripts/YOTSNDMFGenerator.cs | 123 +++++++++++++++++-------------------- 4 files changed, 201 insertions(+), 213 deletions(-) create mode 100644 Scripts/YOTSConfig.cs delete mode 100644 Scripts/YOTSNDMFConfig.cs (limited to 'Scripts') diff --git a/Scripts/YOTSConfig.cs b/Scripts/YOTSConfig.cs new file mode 100644 index 0000000..a6a3f9c --- /dev/null +++ b/Scripts/YOTSConfig.cs @@ -0,0 +1,142 @@ +#if UNITY_EDITOR + +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; +using UnityEditor; +using System.IO; + +namespace YOTS +{ + [DisallowMultipleComponent] + [AddComponentMenu("YOTS Config")] + public class YOTSConfig : MonoBehaviour { + [Tooltip("The JSON configuration file.")] + public TextAsset jsonConfig; + + [TextArea(5, 30)] + public string jsonContent; + + [SerializeField, HideInInspector] + private TextAsset lastJsonConfig; + + void OnValidate() { + gameObject.tag = "EditorOnly"; + + // Only update jsonContent when jsonConfig actually changes + if (jsonConfig != lastJsonConfig) { + if (jsonConfig != null) { + jsonContent = jsonConfig.text; + } + lastJsonConfig = jsonConfig; + } + } + } + + [CustomEditor(typeof(YOTSConfig))] + public class YOTSConfigEditor : Editor + { + private YOTSConfig config; + + private void OnEnable() + { + config = (YOTSConfig)target; + } + + public override void OnInspectorGUI() + { + EditorGUI.BeginChangeCheck(); + + // Draw the default inspector + DrawDefaultInspector(); + + EditorGUILayout.HelpBox( + "You can inspect and edit the JSON config using the textbox above. " + + "Changes are saved automatically. Changes from external editors will " + + "appear upon tabbing back into Unity.", + MessageType.Info); + + + // If changes were made in the inspector + if (EditorGUI.EndChangeCheck()) + { + // Save changes immediately + SaveJsonToFile(); + } + // Only check for file changes if we're not currently editing + else if (config.jsonConfig != null) + { + string currentContent = config.jsonConfig.text; + if (currentContent != config.jsonContent) + { + config.jsonContent = currentContent; + GUI.changed = true; + } + } + + // Check for Ctrl+S + Event e = Event.current; + if (e.type == EventType.KeyDown && e.keyCode == KeyCode.S && e.control) + { + e.Use(); + SaveJsonToFile(); + } + } + + private void SaveJsonToFile() + { + if (config.jsonConfig == null) + { + Debug.LogWarning("No JSON config file assigned!"); + return; + } + + string assetPath = AssetDatabase.GetAssetPath(config.jsonConfig); + if (string.IsNullOrEmpty(assetPath)) + { + Debug.LogError("Could not find asset path!"); + return; + } + + try + { + // Write the modified content from our component + File.WriteAllText(assetPath, config.jsonContent); + + // Force Unity to reload the file from disk + AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); + + // Update the TextAsset to match our changes + var serializedObject = new SerializedObject(config.jsonConfig); + serializedObject.FindProperty("m_Script").stringValue = config.jsonContent; + serializedObject.ApplyModifiedProperties(); + + EditorUtility.SetDirty(config.jsonConfig); + AssetDatabase.SaveAssets(); + Debug.Log($"Successfully saved JSON to {assetPath}"); + } + catch (System.Exception ex) + { + Debug.LogError($"Error saving JSON: {ex.Message}"); + } + } + } + + // Add this new class to handle file modifications + public class JsonFileProcessor : UnityEditor.AssetModificationProcessor + { + private static void OnWillSaveAssets(string[] paths) + { + foreach (string path in paths) + { + if (path.EndsWith(".json")) + { + // Force Unity to reimport the asset + AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); + } + } + } + } +} + +#endif // UNITY_EDITOR + diff --git a/Scripts/YOTSCore.cs b/Scripts/YOTSCore.cs index 49f54d7..d76f382 100644 --- a/Scripts/YOTSCore.cs +++ b/Scripts/YOTSCore.cs @@ -673,7 +673,7 @@ namespace YOTS // Find the toggle with this dependency name var depToggle = toggleSpecs.FirstOrDefault(t => t.name == dep); if (depToggle == null) { - throw new System.Exception($"Toggle '{toggle.name}' has dependency '{dep}' that doesn't exist"); + throw new ArgumentException($"Toggle '{toggle.name}' has dependency '{dep}' that doesn't exist"); } string depParamName = depToggle.GetParameterName(); if (!graph.ContainsKey(depParamName)) @@ -731,9 +731,10 @@ namespace YOTS // Provide detailed error message if (cycleNodes.Count == 0) { - throw new System.Exception($"Dependency cycle detected but couldn't identify specific nodes. Unprocessed parameters: {string.Join(", ", unprocessedParams)}"); + // This should never happen. + throw new ArgumentException($"Dependency cycle detected but couldn't identify specific nodes. Unprocessed parameters: {string.Join(", ", unprocessedParams)}"); } else { - throw new System.Exception($"Dependency cycle detected in toggle specifications. Nodes involved: {string.Join(", ", cycleNodes)}"); + throw new ArgumentException($"Dependency cycle detected in toggle specifications. Nodes involved: {string.Join(", ", cycleNodes)}"); } } diff --git a/Scripts/YOTSNDMFConfig.cs b/Scripts/YOTSNDMFConfig.cs deleted file mode 100644 index a1d8bad..0000000 --- a/Scripts/YOTSNDMFConfig.cs +++ /dev/null @@ -1,142 +0,0 @@ -#if UNITY_EDITOR - -using UnityEngine; -using nadena.dev.ndmf; -using VRC.SDK3.Avatars.ScriptableObjects; -using UnityEditor; -using System.IO; - -namespace YOTS -{ - [DisallowMultipleComponent] - [AddComponentMenu("YOTS NDMF Config")] - public class YOTSNDMFConfig : MonoBehaviour { - [Tooltip("The JSON configuration file.")] - public TextAsset jsonConfig; - - [TextArea(5, 80)] // Min 5 lines, max 80 lines - public string jsonContent; - - [SerializeField, HideInInspector] - private TextAsset lastJsonConfig; - - void OnValidate() { - gameObject.tag = "EditorOnly"; - - // Only update jsonContent when jsonConfig actually changes - if (jsonConfig != lastJsonConfig) { - if (jsonConfig != null) { - jsonContent = jsonConfig.text; - } - lastJsonConfig = jsonConfig; - } - } - } - - [CustomEditor(typeof(YOTSNDMFConfig))] - public class YOTSNDMFConfigEditor : Editor - { - private YOTSNDMFConfig config; - - private void OnEnable() - { - config = (YOTSNDMFConfig)target; - } - - public override void OnInspectorGUI() - { - EditorGUI.BeginChangeCheck(); - - // Draw the default inspector - DrawDefaultInspector(); - - EditorGUILayout.HelpBox( - "You can inspect and edit the JSON config using the textbox above. " + - "Changes are saved automatically. Changes from external editors will " + - "appear upon tabbing back into Unity.", - MessageType.Info); - - - // If changes were made in the inspector - if (EditorGUI.EndChangeCheck()) - { - // Save changes immediately - SaveJsonToFile(); - } - // Only check for file changes if we're not currently editing - else if (config.jsonConfig != null) - { - string currentContent = config.jsonConfig.text; - if (currentContent != config.jsonContent) - { - config.jsonContent = currentContent; - GUI.changed = true; - } - } - - // Check for Ctrl+S - Event e = Event.current; - if (e.type == EventType.KeyDown && e.keyCode == KeyCode.S && e.control) - { - e.Use(); - SaveJsonToFile(); - } - } - - private void SaveJsonToFile() - { - if (config.jsonConfig == null) - { - Debug.LogWarning("No JSON config file assigned!"); - return; - } - - string assetPath = AssetDatabase.GetAssetPath(config.jsonConfig); - if (string.IsNullOrEmpty(assetPath)) - { - Debug.LogError("Could not find asset path!"); - return; - } - - try - { - // Write the modified content from our component - File.WriteAllText(assetPath, config.jsonContent); - - // Force Unity to reload the file from disk - AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); - - // Update the TextAsset to match our changes - var serializedObject = new SerializedObject(config.jsonConfig); - serializedObject.FindProperty("m_Script").stringValue = config.jsonContent; - serializedObject.ApplyModifiedProperties(); - - EditorUtility.SetDirty(config.jsonConfig); - AssetDatabase.SaveAssets(); - Debug.Log($"Successfully saved JSON to {assetPath}"); - } - catch (System.Exception ex) - { - Debug.LogError($"Error saving JSON: {ex.Message}"); - } - } - } - - // Add this new class to handle file modifications - public class JsonFileProcessor : UnityEditor.AssetModificationProcessor - { - private static void OnWillSaveAssets(string[] paths) - { - foreach (string path in paths) - { - if (path.EndsWith(".json")) - { - // Force Unity to reimport the asset - AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); - } - } - } - } -} - -#endif // UNITY_EDITOR diff --git a/Scripts/YOTSNDMFGenerator.cs b/Scripts/YOTSNDMFGenerator.cs index aa8ef84..f8cf889 100644 --- a/Scripts/YOTSNDMFGenerator.cs +++ b/Scripts/YOTSNDMFGenerator.cs @@ -17,18 +17,30 @@ using UnityEditor.Animations; namespace YOTS { public class YOTSNDMFGenerator : Plugin { - private readonly Localizer localizer = new Localizer("en-us", () => + private readonly Localizer lcl = new Localizer("en-us", () => new List<(string, Func)> { - ("en-us", key => key) + ("en-us", key => { + switch (key) { + case "json_missing": return "YOTS configuration JSON file is missing"; + case "config_missing": return "YOTS config component not found on avatar"; + case "descriptor_missing": return "VRC Avatar Descriptor is missing from avatar"; + case "expressions_missing": return "Avatar is missing Expression Parameters or Expression Menu"; + case "param_exists": return "Parameter '{0}' already exists in FX animator"; + case "layer_exists": return "Layer '{0}' already exists in FX animator"; + case "config_error": return "{0}"; + default: return null; + } + }) }); public override string DisplayName => "YOTS Animator Generator"; protected override void Configure() { - // First pass: Retrieve and stash configuration + // First pass: Retrieve and stash configuration. By the time we're in the + // Transforming phase, we can no longer access the YOTSConfig object. InPhase(BuildPhase.Resolving) .Run("Cache YOTS Config", ctx => { - var config = ctx.AvatarRootObject.GetComponentInChildren(); + var config = ctx.AvatarRootObject.GetComponentInChildren(); if (config == null) { ctx.GetState().skipGeneration = true; Debug.Log("No YOTS config found - skipping."); @@ -36,12 +48,8 @@ namespace YOTS } if (config.jsonConfig == null) { ctx.GetState().skipGeneration = true; - ErrorReport.WithContextObject(ctx.AvatarRootObject, () => { - ErrorReport.ReportException( - new Exception("No YOTS config found"), - "Missing required YOTS configuration" - ); - }); + ErrorReport.ReportError(lcl, ErrorSeverity.Error, "json_missing", + ctx.AvatarRootObject); return; } ctx.GetState().jsonConfig = config.jsonConfig.text; @@ -57,45 +65,29 @@ namespace YOTS return; } if (config == null) { - ErrorReport.WithContextObject(ctx.AvatarRootObject, () => { - ErrorReport.ReportException( - new Exception("No YOTS config component found"), - "Missing required YOTS configuration" - ); - }); + ErrorReport.ReportError(lcl, ErrorSeverity.Error, "config_missing", + ctx.AvatarRootObject); return; } if (config.jsonConfig == null) { - ErrorReport.WithContextObject(ctx.AvatarRootObject, () => { - ErrorReport.ReportException( - new Exception("Missing JSON config file"), - "YOTS config component is missing required JSON configuration" - ); - }); + ErrorReport.ReportError(lcl, ErrorSeverity.Error, "json_missing", + ctx.AvatarRootObject); return; } // Get menu and parameters var descriptor = ctx.AvatarDescriptor; if (descriptor == null) { - ErrorReport.WithContextObject(ctx.AvatarRootObject, () => { - ErrorReport.ReportException( - new Exception("Avatar descriptor is missing"), - "Cannot find VRC Avatar Descriptor" - ); - }); + ErrorReport.ReportError(lcl, ErrorSeverity.Error, "descriptor_missing", + ctx.AvatarRootObject); return; } RuntimeAnimatorController originalAnimator = descriptor.baseAnimationLayers[4].animatorController; var menu = descriptor.expressionsMenu; var parameters = descriptor.expressionParameters; if (parameters == null || menu == null) { - ErrorReport.WithContextObject(descriptor, () => { - ErrorReport.ReportException( - new Exception("Missing required VRC assets"), - "Avatar is missing required Expression Parameters or Menu" - ); - }); + ErrorReport.ReportError(lcl, ErrorSeverity.Error, "expressions_missing", + descriptor); return; } // Create copies so the originals don't get modified @@ -105,18 +97,19 @@ namespace YOTS descriptor.expressionParameters = parameters; // Generate the YOTS animator. - RuntimeAnimatorController generatedAnimator = YOTSCore.GenerateAnimator( - config.jsonConfig, - parameters, - menu - ); - if (generatedAnimator == null) { - ErrorReport.WithContextObject(ctx.AvatarRootObject, () => { - ErrorReport.ReportException( - new Exception("Failed to generate animator"), - "YOTS animator generation failed" - ); - }); + RuntimeAnimatorController generatedAnimator = null; + try { + generatedAnimator = YOTSCore.GenerateAnimator( + config.jsonConfig, + parameters, + menu + ); + } catch (ArgumentException e) { + ErrorReport.ReportError(lcl, ErrorSeverity.Error, "config_error", + e.Message, ctx.AvatarRootObject); + return; + } catch (Exception e) { + ErrorReport.ReportException(e); return; } @@ -128,39 +121,33 @@ namespace YOTS // Else append the generated animator to the original. AnimatorController originalController = originalAnimator as AnimatorController; AnimatorController generatedController = generatedAnimator as AnimatorController; - MergeAnimatorControllers(localizer, generatedController, originalController); + MergeAnimatorControllers(originalController, generatedController); descriptor.baseAnimationLayers[4].animatorController = generatedController; }); } // Simply append generated params and layers to the original animator. - private static void MergeAnimatorControllers(Localizer localizer, AnimatorController original, AnimatorController generated) { - // Merge parameters from generated into original. - foreach (var genParam in generated.parameters) { + private void MergeAnimatorControllers(AnimatorController from, AnimatorController to) { + // Merge parameters from from into to. + foreach (var genParam in from.parameters) { // This is an O(m*n) check but m and n should be small enough to not matter. - if (original.parameters.Any(p => p.name == genParam.name)) { - ErrorReport.WithContextObject(original, () => { - ErrorReport.ReportException( - new Exception($"Parameter '{genParam.name}' already exists"), - "Parameter name conflict in animator" - ); - }); + if (to.parameters.Any(p => p.name == genParam.name)) { + ErrorReport.ReportError(lcl, ErrorSeverity.Error, + "param_exists", + genParam.name, to); return; } - original.AddParameter(genParam); + to.AddParameter(genParam); } - // Append each YOTS layer after the original layers. - foreach (var genLayer in generated.layers) { + // Append each YOTS layer after the to layers. + foreach (var genLayer in from.layers) { // This isn't strictly an error but if someone already has layers named // YOTS_* that's probably not on purpose. - if (original.layers.Any(l => l.name == genLayer.name)) { - ErrorReport.WithContextObject(original, () => { - ErrorReport.ReportException( - new Exception($"Layer '{genLayer.name}' already exists"), - "Layer name conflict in animator" - ); - }); + if (to.layers.Any(l => l.name == genLayer.name)) { + ErrorReport.ReportError(lcl, ErrorSeverity.Error, + "layer_exists", + genLayer.name, to); return; } var newLayer = new AnimatorControllerLayer { @@ -168,7 +155,7 @@ namespace YOTS defaultWeight = genLayer.defaultWeight, stateMachine = genLayer.stateMachine }; - original.AddLayer(newLayer); + to.AddLayer(newLayer); } } -- cgit v1.2.3