diff --git a/.gitignore b/.gitignore index f6c2350..26370b3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ !.vscode/extensions.json *.code-workspace +## VisualStudio + +.vs/ + # Local History for Visual Studio Code .history/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 866cfa4..461dea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.2.12] 2024-09-15 +* Use file-scoped namespaces in all plugin scripts to reduce indentation, by @valkyrienyanko + ## [0.2.11] 2024-09-13 * Fix an issue where the OptionsListView did not fade in properly and instead suddenly appeared at the end of the fade time. (Fix #62) * `Effects.Fade` now uses a Godot Tween. diff --git a/addons/YarnSpinner-Godot/Editor/YarnCompileErrorsPropertyEditor.cs b/addons/YarnSpinner-Godot/Editor/YarnCompileErrorsPropertyEditor.cs index 4b41f52..7185fba 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnCompileErrorsPropertyEditor.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnCompileErrorsPropertyEditor.cs @@ -3,69 +3,68 @@ using Godot; using Array = Godot.Collections.Array; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +[Tool] +public partial class YarnCompileErrorsPropertyEditor : EditorProperty { - [Tool] - public partial class YarnCompileErrorsPropertyEditor : EditorProperty - { - // The main control for editing the property. - private Label _propertyControl; + // The main control for editing the property. + private Label _propertyControl; - // An internal value of the property. - private Array _currentValue; + // An internal value of the property. + private Array _currentValue; - [Signal] - public delegate void OnErrorsUpdateEventHandler(GodotObject yarnProject); + [Signal] + public delegate void OnErrorsUpdateEventHandler(GodotObject yarnProject); + + public YarnCompileErrorsPropertyEditor() + { + _propertyControl = new Label(); + Label = "Project Errors"; + // Add the control as a direct child of EditorProperty node. + AddChild(_propertyControl); + // Make sure the control is able to retain the focus. + AddFocusable(_propertyControl); + // Setup the initial state and connect to the signal to track changes. + RefreshControlText(); + } - public YarnCompileErrorsPropertyEditor() + public override void _UpdateProperty() + { + // Read the current value from the property. + var newVariantValue = GetEditedObject().Get(GetEditedProperty()); + var newValue = (Array) newVariantValue; + if (newValue == _currentValue) { - _propertyControl = new Label(); - Label = "Project Errors"; - // Add the control as a direct child of EditorProperty node. - AddChild(_propertyControl); - // Make sure the control is able to retain the focus. - AddFocusable(_propertyControl); - // Setup the initial state and connect to the signal to track changes. - RefreshControlText(); + return; } - public override void _UpdateProperty() - { - // Read the current value from the property. - var newVariantValue = GetEditedObject().Get(GetEditedProperty()); - var newValue = (Array) newVariantValue; - if (newValue == _currentValue) - { - return; - } + _currentValue = newValue; + RefreshControlText(); + EmitSignal(SignalName.OnErrorsUpdate); + } - _currentValue = newValue; - RefreshControlText(); - EmitSignal(SignalName.OnErrorsUpdate); + private void RefreshControlText() + { + if (_currentValue == null) + { + _propertyControl.Text = ""; } - - private void RefreshControlText() + else if (_currentValue.Count == 0) { - if (_currentValue == null) - { - _propertyControl.Text = ""; - } - else if (_currentValue.Count == 0) - { - _propertyControl.Text = "None"; - } - else - { - _propertyControl.Text = - $"❌{_currentValue.Count} error{(_currentValue.Count > 1 ? "s" : "")}"; - } + _propertyControl.Text = "None"; } - - public void Refresh() + else { - EmitChanged(GetEditedProperty(), - GetEditedObject().Get(GetEditedProperty())); + _propertyControl.Text = + $"❌{_currentValue.Count} error{(_currentValue.Count > 1 ? "s" : "")}"; } } + + public void Refresh() + { + EmitChanged(GetEditedProperty(), + GetEditedObject().Get(GetEditedProperty())); + } } #endif \ No newline at end of file diff --git a/addons/YarnSpinner-Godot/Editor/YarnEditorUtility.cs b/addons/YarnSpinner-Godot/Editor/YarnEditorUtility.cs index d17d0ee..272d83e 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnEditorUtility.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnEditorUtility.cs @@ -4,110 +4,109 @@ using File = System.IO.File; using Path = System.IO.Path; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +/// +/// Contains utility methods for working with Yarn Spinner content in +/// the Godot editor. +/// +public static class YarnEditorUtility { + + + const string TemplateFilePath = "res://addons/YarnSpinner-Godot/Editor/YarnScriptTemplate.txt"; + /// - /// Contains utility methods for working with Yarn Spinner content in - /// the Godot editor. - /// - public static class YarnEditorUtility + /// Menu Item "Tools > YarnSpinner > Create Yarn Script" + /// + /// + public static void CreateYarnScript(string scriptPath) { + GD.Print($"Creating new yarn script at {scriptPath}"); + CreateYarnScriptAssetFromTemplate(scriptPath); + } - - const string TemplateFilePath = "res://addons/YarnSpinner-Godot/Editor/YarnScriptTemplate.txt"; - - /// - /// Menu Item "Tools > YarnSpinner > Create Yarn Script" - /// - /// - public static void CreateYarnScript(string scriptPath) + /// + /// Menu Item "Tools > YarnSpinner > Create Yarn Script" + /// + /// res:// path of the YarnProject resource to create + public static void CreateYarnProject(string projectPath) + { + var jsonProject = new Yarn.Compiler.Project(); + var absPath = ProjectSettings.GlobalizePath(projectPath); + jsonProject.SaveToFile(absPath); + } + /// + /// Menu Item "Tools > YarnSpinner > Create Markup Palette" + /// + /// res:// path to the markup palette to create + public static void CreateMarkupPalette(string palettePath) + { + var newPalette = new MarkupPalette(); + var absPath = ProjectSettings.GlobalizePath(palettePath); + newPalette.ResourceName = Path.GetFileNameWithoutExtension(absPath); + newPalette.ResourcePath = palettePath; + var saveErr = ResourceSaver.Save(newPalette, palettePath); + if (saveErr != Error.Ok) { - GD.Print($"Creating new yarn script at {scriptPath}"); - CreateYarnScriptAssetFromTemplate(scriptPath); + GD.Print($"Failed to save markup palette to {palettePath}"); } + } + + /// + /// Menu Item "Yarn Spinner/Create Yarn Script" + /// + /// + /// + public static void CreateYarnLocalization(string localizationPath) + { + var newLocalization = new Localization(); + var absPath = ProjectSettings.GlobalizePath(localizationPath); + newLocalization.ResourceName = Path.GetFileNameWithoutExtension(absPath); + newLocalization.ResourcePath = localizationPath; + ResourceSaver.Save( newLocalization , localizationPath); + GD.Print($"Saved new yarn localization to {localizationPath}"); + } - /// - /// Menu Item "Tools > YarnSpinner > Create Yarn Script" - /// - /// res:// path of the YarnProject resource to create - public static void CreateYarnProject(string projectPath) - { - var jsonProject = new Yarn.Compiler.Project(); - var absPath = ProjectSettings.GlobalizePath(projectPath); - jsonProject.SaveToFile(absPath); - } - /// - /// Menu Item "Tools > YarnSpinner > Create Markup Palette" - /// - /// res:// path to the markup palette to create - public static void CreateMarkupPalette(string palettePath) + private static void CreateYarnScriptAssetFromTemplate(string pathName) + { + // Read the contents of the template file + string templateContent; + try { - var newPalette = new MarkupPalette(); - var absPath = ProjectSettings.GlobalizePath(palettePath); - newPalette.ResourceName = Path.GetFileNameWithoutExtension(absPath); - newPalette.ResourcePath = palettePath; - var saveErr = ResourceSaver.Save(newPalette, palettePath); - if (saveErr != Error.Ok) - { - GD.Print($"Failed to save markup palette to {palettePath}"); - } + templateContent = File.ReadAllText(ProjectSettings.GlobalizePath(TemplateFilePath)); } - - /// - /// Menu Item "Yarn Spinner/Create Yarn Script" - /// - /// - /// - public static void CreateYarnLocalization(string localizationPath) + catch { - var newLocalization = new Localization(); - var absPath = ProjectSettings.GlobalizePath(localizationPath); - newLocalization.ResourceName = Path.GetFileNameWithoutExtension(absPath); - newLocalization.ResourcePath = localizationPath; - ResourceSaver.Save( newLocalization , localizationPath); - GD.Print($"Saved new yarn localization to {localizationPath}"); + GD.PrintErr("Failed to find the Yarn script template file. Creating an empty file instead."); + // the minimal valid Yarn script - no headers, no body + templateContent = "---\n===\n"; } - private static void CreateYarnScriptAssetFromTemplate(string pathName) - { - // Read the contents of the template file - string templateContent; - try - { - templateContent = File.ReadAllText(ProjectSettings.GlobalizePath(TemplateFilePath)); - } - catch - { - GD.PrintErr("Failed to find the Yarn script template file. Creating an empty file instead."); - // the minimal valid Yarn script - no headers, no body - templateContent = "---\n===\n"; - } - - // Figure out the 'file name' that the user entered - // The script name is the name of the file, sans extension. - string scriptName = Path.GetFileNameWithoutExtension(pathName); + // Figure out the 'file name' that the user entered + // The script name is the name of the file, sans extension. + string scriptName = Path.GetFileNameWithoutExtension(pathName); - // Replace any spaces with underscores - these aren't allowed - // in node names - scriptName = scriptName.Replace(" ", "_"); + // Replace any spaces with underscores - these aren't allowed + // in node names + scriptName = scriptName.Replace(" ", "_"); - // Replace the placeholder with the script name - templateContent = templateContent.Replace("#SCRIPTNAME#", scriptName); + // Replace the placeholder with the script name + templateContent = templateContent.Replace("#SCRIPTNAME#", scriptName); - string lineEndings = "\n"; + string lineEndings = "\n"; - // Replace every line ending in the template (this way we don't - // need to keep track of which line ending the asset was last - // saved in) - templateContent = System.Text.RegularExpressions.Regex.Replace(templateContent, @"\r\n?|\n", lineEndings); + // Replace every line ending in the template (this way we don't + // need to keep track of which line ending the asset was last + // saved in) + templateContent = System.Text.RegularExpressions.Regex.Replace(templateContent, @"\r\n?|\n", lineEndings); - // Write it all out to disk as UTF-8 - var fullPath = Path.GetFullPath(ProjectSettings.GlobalizePath(pathName)); - File.WriteAllText(fullPath, templateContent, System.Text.Encoding.UTF8); - GD.Print($"Wrote new file {pathName}"); - YarnSpinnerPlugin.editorInterface.GetResourceFilesystem().ScanSources(); - } - + // Write it all out to disk as UTF-8 + var fullPath = Path.GetFullPath(ProjectSettings.GlobalizePath(pathName)); + File.WriteAllText(fullPath, templateContent, System.Text.Encoding.UTF8); + GD.Print($"Wrote new file {pathName}"); + YarnSpinnerPlugin.editorInterface.GetResourceFilesystem().ScanSources(); } + } #endif \ No newline at end of file diff --git a/addons/YarnSpinner-Godot/Editor/YarnImporter.cs b/addons/YarnSpinner-Godot/Editor/YarnImporter.cs index a95a5e5..2d5bffe 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnImporter.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnImporter.cs @@ -5,126 +5,125 @@ using Godot; using Godot.Collections; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +/// +/// A for Yarn scripts (.yarn files) +/// +public partial class YarnImporter : EditorImportPlugin { - /// - /// A for Yarn scripts (.yarn files) - /// - public partial class YarnImporter : EditorImportPlugin - { - public override string[] _GetRecognizedExtensions() => - new[] - { - "yarn" - }; + public override string[] _GetRecognizedExtensions() => + new[] + { + "yarn" + }; + + public override string _GetImporterName() => "yarnscript"; - public override string _GetImporterName() => "yarnscript"; + public override string _GetVisibleName() => "Yarn Script"; - public override string _GetVisibleName() => "Yarn Script"; + public override string _GetSaveExtension() => "tres"; - public override string _GetSaveExtension() => "tres"; + public override string _GetResourceType() => "Resource"; - public override string _GetResourceType() => "Resource"; + public override int _GetPresetCount() => 0; - public override int _GetPresetCount() => 0; + public override float _GetPriority() => 1.0f; - public override float _GetPriority() => 1.0f; + public override int _GetImportOrder() => 0; - public override int _GetImportOrder() => 0; + public override Array _GetImportOptions(string path, int presetIndex) => new(); - public override Array _GetImportOptions(string path, int presetIndex) => new(); + public override Error _Import( + string assetPath, + string savePath, + Dictionary options, + Array platformVariants, + Array genFiles) + { + var extension = System.IO.Path.GetExtension(assetPath); - public override Error _Import( - string assetPath, - string savePath, - Dictionary options, - Array platformVariants, - Array genFiles) + if (extension == ".yarn") { - var extension = System.IO.Path.GetExtension(assetPath); + ImportYarn(assetPath); + } - if (extension == ".yarn") - { - ImportYarn(assetPath); - } + var importedMarkerResource = new Resource(); + importedMarkerResource.ResourceName = + System.IO.Path.GetFileNameWithoutExtension(ProjectSettings.GlobalizePath(assetPath)); - var importedMarkerResource = new Resource(); - importedMarkerResource.ResourceName = - System.IO.Path.GetFileNameWithoutExtension(ProjectSettings.GlobalizePath(assetPath)); + var saveErr = ResourceSaver.Save(importedMarkerResource, $"{savePath}.{_GetSaveExtension()}"); + if (saveErr != Error.Ok) + { + GD.PrintErr($"Error saving yarn file import: {saveErr.ToString()}"); + } - var saveErr = ResourceSaver.Save(importedMarkerResource, $"{savePath}.{_GetSaveExtension()}"); - if (saveErr != Error.Ok) - { - GD.PrintErr($"Error saving yarn file import: {saveErr.ToString()}"); - } + return (int) Error.Ok; + } - return (int) Error.Ok; + /// + /// Returns a byte array containing a SHA-256 hash of . + /// + /// The string to produce a hash value + /// for. + /// The hash of . + private static byte[] GetHash(string inputString) + { + using (HashAlgorithm algorithm = SHA256.Create()) + { + return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString)); } + } - /// - /// Returns a byte array containing a SHA-256 hash of . - /// - /// The string to produce a hash value - /// for. - /// The hash of . - private static byte[] GetHash(string inputString) + /// + /// Returns a string containing the hexadecimal representation of a + /// SHA-256 hash of . + /// + /// The string to produce a hash + /// for. + /// The length of the string to + /// return. The returned string will be at most characters long. If this is set to -1, + /// the entire string will be returned. + /// A string version of the hash. + public static string GetHashString(string inputString, int limitCharacters = -1) + { + var sb = new StringBuilder(); + foreach (byte b in GetHash(inputString)) { - using (HashAlgorithm algorithm = SHA256.Create()) - { - return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString)); - } + sb.Append(b.ToString("x2")); } - /// - /// Returns a string containing the hexadecimal representation of a - /// SHA-256 hash of . - /// - /// The string to produce a hash - /// for. - /// The length of the string to - /// return. The returned string will be at most characters long. If this is set to -1, - /// the entire string will be returned. - /// A string version of the hash. - public static string GetHashString(string inputString, int limitCharacters = -1) + if (limitCharacters == -1) { - var sb = new StringBuilder(); - foreach (byte b in GetHash(inputString)) - { - sb.Append(b.ToString("x2")); - } - - if (limitCharacters == -1) - { - // Return the entire string - return sb.ToString(); - } - else - { - // Return a substring (or the entire string, if - // limitCharacters is longer than the string) - return sb.ToString(0, Mathf.Min(sb.Length, limitCharacters)); - } + // Return the entire string + return sb.ToString(); } + else + { + // Return a substring (or the entire string, if + // limitCharacters is longer than the string) + return sb.ToString(0, Mathf.Min(sb.Length, limitCharacters)); + } + } - private void ImportYarn(string assetPath) + private void ImportYarn(string assetPath) + { + GD.Print($"Importing Yarn script {assetPath}"); + var projectPath = YarnProjectEditorUtility.GetDestinationProjectPath(assetPath); + if (projectPath == null) + { + GD.Print($"The yarn file {assetPath} is not currently associated with a Yarn Project." + + " Create a Yarn Project by selecting YarnProject from the create new resource menu and make sure this" + + " script matches one of the patterns defined for yarn source files."); + } + else { - GD.Print($"Importing Yarn script {assetPath}"); - var projectPath = YarnProjectEditorUtility.GetDestinationProjectPath(assetPath); - if (projectPath == null) - { - GD.Print($"The yarn file {assetPath} is not currently associated with a Yarn Project." + - " Create a Yarn Project by selecting YarnProject from the create new resource menu and make sure this" + - " script matches one of the patterns defined for yarn source files."); - } - else - { - // trigger update of the yarn project - var godotProject = ResourceLoader.Load(projectPath); - YarnProjectEditorUtility.UpdateYarnProject(godotProject); - } + // trigger update of the yarn project + var godotProject = ResourceLoader.Load(projectPath); + YarnProjectEditorUtility.UpdateYarnProject(godotProject); } } } diff --git a/addons/YarnSpinner-Godot/Editor/YarnMarkupPaletteInspectorPlugin.cs b/addons/YarnSpinner-Godot/Editor/YarnMarkupPaletteInspectorPlugin.cs index 7d04946..d30a20b 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnMarkupPaletteInspectorPlugin.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnMarkupPaletteInspectorPlugin.cs @@ -5,124 +5,123 @@ using YarnSpinnerGodot.Editor.UI; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +/// +/// Custom inspector for that allows the user +/// to add / remove markup tags and set their associated colors. +/// +[Tool] +public partial class YarnMarkupPaletteInspectorPlugin : EditorInspectorPlugin { - /// - /// Custom inspector for that allows the user - /// to add / remove markup tags and set their associated colors. - /// - [Tool] - public partial class YarnMarkupPaletteInspectorPlugin : EditorInspectorPlugin - { - public override bool _CanHandle(GodotObject obj) => obj is MarkupPalette; + public override bool _CanHandle(GodotObject obj) => obj is MarkupPalette; - public override bool _ParseProperty(GodotObject @object, Variant.Type type, - string path, - PropertyHint hint, string hintText, PropertyUsageFlags usage, bool wide) + public override bool _ParseProperty(GodotObject @object, Variant.Type type, + string path, + PropertyHint hint, string hintText, PropertyUsageFlags usage, bool wide) + { + if (@object is not MarkupPalette palette) { - if (@object is not MarkupPalette palette) - { - return false; - } + return false; + } - try + try + { + if (path == nameof(MarkupPalette.ColourMarkers)) { - if (path == nameof(MarkupPalette.ColourMarkers)) + AddCustomControl(new Label + {Text = "Map [markup] tag names to colors"}); + if (palette.ColourMarkers.Count == 0) { - AddCustomControl(new Label - {Text = "Map [markup] tag names to colors"}); - if (palette.ColourMarkers.Count == 0) - { - var noColorsLabel = new Label(); - noColorsLabel.Text = "No colors remapped"; - AddCustomControl(noColorsLabel); - } - else + var noColorsLabel = new Label(); + noColorsLabel.Text = "No colors remapped"; + AddCustomControl(noColorsLabel); + } + else + { + var colorRemapGrid = new GridContainer(); + colorRemapGrid.Columns = 3; + colorRemapGrid.SizeFlagsVertical = Control.SizeFlags.ExpandFill; + colorRemapGrid.SizeFlagsHorizontal = + Control.SizeFlags.ExpandFill; + + var originalHeader = new Label(); + originalHeader.Text = "Markup Tag"; + colorRemapGrid.AddChild(originalHeader); + + var replacementHeader = new Label(); + replacementHeader.Text = "Text Color"; + colorRemapGrid.AddChild(replacementHeader); + + var deleteHeader = new Label(); + deleteHeader.Text = "Delete"; + colorRemapGrid.AddChild(deleteHeader); + const int remapHeight = 4; + foreach (var tagName in palette.ColourMarkers.Keys) { - var colorRemapGrid = new GridContainer(); - colorRemapGrid.Columns = 3; - colorRemapGrid.SizeFlagsVertical = Control.SizeFlags.ExpandFill; - colorRemapGrid.SizeFlagsHorizontal = + colorRemapGrid.AddChild(new Label {Text = tagName}); + + var replacementColorButton = new MarkupPaletteColorButton + {palette = palette, tagName = tagName}; + replacementColorButton.Color = + palette.ColourMarkers[tagName]; + replacementColorButton.Size = new Vector2(0, remapHeight); + replacementColorButton.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + colorRemapGrid.AddChild(replacementColorButton); - var originalHeader = new Label(); - originalHeader.Text = "Markup Tag"; - colorRemapGrid.AddChild(originalHeader); + var deleteArea = new HBoxContainer(); + var deleteSpacer = new Label {Text = " "}; - var replacementHeader = new Label(); - replacementHeader.Text = "Text Color"; - colorRemapGrid.AddChild(replacementHeader); - - var deleteHeader = new Label(); - deleteHeader.Text = "Delete"; - colorRemapGrid.AddChild(deleteHeader); - const int remapHeight = 4; - foreach (var tagName in palette.ColourMarkers.Keys) + var deleteButton = new MarkupPaletteDeleteTagButton { - colorRemapGrid.AddChild(new Label {Text = tagName}); - - var replacementColorButton = new MarkupPaletteColorButton - {palette = palette, tagName = tagName}; - replacementColorButton.Color = - palette.ColourMarkers[tagName]; - replacementColorButton.Size = new Vector2(0, remapHeight); - replacementColorButton.SizeFlagsHorizontal = - Control.SizeFlags.ExpandFill; - colorRemapGrid.AddChild(replacementColorButton); - - var deleteArea = new HBoxContainer(); - var deleteSpacer = new Label {Text = " "}; - - var deleteButton = new MarkupPaletteDeleteTagButton - { - Text = "X", - tagName = tagName, - palette = palette - }; - deleteButton.Text = "x"; - deleteButton.AddThemeColorOverride("normal", Colors.Red); - deleteButton.Size = new Vector2(4, remapHeight); - deleteButton.SizeFlagsHorizontal = 0; - - deleteArea.AddChild(deleteSpacer); - deleteArea.AddChild(deleteButton); - colorRemapGrid.AddChild(deleteArea); - } - - AddCustomControl(colorRemapGrid); + Text = "X", + tagName = tagName, + palette = palette + }; + deleteButton.Text = "x"; + deleteButton.AddThemeColorOverride("normal", Colors.Red); + deleteButton.Size = new Vector2(4, remapHeight); + deleteButton.SizeFlagsHorizontal = 0; + + deleteArea.AddChild(deleteSpacer); + deleteArea.AddChild(deleteButton); + colorRemapGrid.AddChild(deleteArea); } - var newTagRow = new HBoxContainer(); - var addNewTagButton = new MarkupPaletteAddTagButton - {Text = "Add", palette = palette}; - - var newTagNameInput = new LineEditWithSubmit() - { - PlaceholderText = "tag name, without []", - CustomMinimumSize = new Vector2(80, 10), - SubmitButton = addNewTagButton - }; - addNewTagButton.newTagNameInput = newTagNameInput; - newTagNameInput.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - addNewTagButton.Disabled = true; - - newTagRow.AddChild(newTagNameInput); - newTagRow.AddChild(addNewTagButton); - newTagRow.SizeFlagsVertical = Control.SizeFlags.ExpandFill; - newTagRow.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - AddCustomControl(newTagRow); - - return true; + AddCustomControl(colorRemapGrid); } - return false; - } - catch (Exception e) - { - GD.PushError( - $"Error in {nameof(YarnMarkupPaletteInspectorPlugin)}: {e.Message}\n{e.StackTrace}"); - return false; + var newTagRow = new HBoxContainer(); + var addNewTagButton = new MarkupPaletteAddTagButton + {Text = "Add", palette = palette}; + + var newTagNameInput = new LineEditWithSubmit() + { + PlaceholderText = "tag name, without []", + CustomMinimumSize = new Vector2(80, 10), + SubmitButton = addNewTagButton + }; + addNewTagButton.newTagNameInput = newTagNameInput; + newTagNameInput.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + addNewTagButton.Disabled = true; + + newTagRow.AddChild(newTagNameInput); + newTagRow.AddChild(addNewTagButton); + newTagRow.SizeFlagsVertical = Control.SizeFlags.ExpandFill; + newTagRow.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + AddCustomControl(newTagRow); + + return true; } + + return false; + } + catch (Exception e) + { + GD.PushError( + $"Error in {nameof(YarnMarkupPaletteInspectorPlugin)}: {e.Message}\n{e.StackTrace}"); + return false; } } } diff --git a/addons/YarnSpinner-Godot/Editor/YarnProjectEditorUtility.cs b/addons/YarnSpinner-Godot/Editor/YarnProjectEditorUtility.cs index 0174615..448d871 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnProjectEditorUtility.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnProjectEditorUtility.cs @@ -21,886 +21,885 @@ using FileAccess = System.IO.FileAccess; using Path = System.IO.Path; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +[Tool] +public static class YarnProjectEditorUtility { - [Tool] - public static class YarnProjectEditorUtility + /// + /// The contents of a .csv.import file to avoid importing it as a Godot localization csv file + /// + public const string KEEP_IMPORT_TEXT = "[remap]\n\nimporter=\"keep\""; + + /// + /// Find an associated yarn project in the same or ancestor directory + /// + /// + /// + public static string GetDestinationProjectPath(string scriptPath) { - /// - /// The contents of a .csv.import file to avoid importing it as a Godot localization csv file - /// - public const string KEEP_IMPORT_TEXT = "[remap]\n\nimporter=\"keep\""; - - /// - /// Find an associated yarn project in the same or ancestor directory - /// - /// - /// - public static string GetDestinationProjectPath(string scriptPath) - { - string destinationProjectPath = null; - var globalScriptPath = Path.GetFullPath(ProjectSettings.GlobalizePath(scriptPath)); - var allProjects = FindAllYarnProjects(); - foreach (var project in allProjects) + string destinationProjectPath = null; + var globalScriptPath = Path.GetFullPath(ProjectSettings.GlobalizePath(scriptPath)); + var allProjects = FindAllYarnProjects(); + foreach (var project in allProjects) + { + var projectPath = ProjectSettings.GlobalizePath(project.ToString()) + .Replace("\\", "/"); + try { - var projectPath = ProjectSettings.GlobalizePath(project.ToString()) - .Replace("\\", "/"); - try - { - var loadedProject = Yarn.Compiler.Project.LoadFromFile(projectPath); - if (!loadedProject.SourceFiles.Contains(globalScriptPath)) - { - continue; - } - - destinationProjectPath = ProjectSettings.LocalizePath(projectPath); - break; - } - catch (Exception e) + var loadedProject = Yarn.Compiler.Project.LoadFromFile(projectPath); + if (!loadedProject.SourceFiles.Contains(globalScriptPath)) { - GD.PushError( - $"Error while searching for the project associated with {scriptPath}: {e.Message}\n{e.StackTrace}"); + continue; } - } - return destinationProjectPath; + destinationProjectPath = ProjectSettings.LocalizePath(projectPath); + break; + } + catch (Exception e) + { + GD.PushError( + $"Error while searching for the project associated with {scriptPath}: {e.Message}\n{e.StackTrace}"); + } } - private static IEnumerable FindAllYarnProjects() - { - var projectMatcher = new Matcher(); - projectMatcher.AddInclude($"**/*{YarnProject.YARN_PROJECT_EXTENSION}"); - return projectMatcher.GetResultsInFullPath(ProjectSettings.GlobalizePath("res://")) - .Select(ProjectSettings.LocalizePath); - } + return destinationProjectPath; + } + + private static IEnumerable FindAllYarnProjects() + { + var projectMatcher = new Matcher(); + projectMatcher.AddInclude($"**/*{YarnProject.YARN_PROJECT_EXTENSION}"); + return projectMatcher.GetResultsInFullPath(ProjectSettings.GlobalizePath("res://")) + .Select(ProjectSettings.LocalizePath); + } - private const int PROJECT_UPDATE_TIMEOUT = 80; // ms + private const int PROJECT_UPDATE_TIMEOUT = 80; // ms - private static ConcurrentDictionary _projectPathToLastUpdateTime = - new ConcurrentDictionary(); + private static ConcurrentDictionary _projectPathToLastUpdateTime = + new ConcurrentDictionary(); - private static Dictionary _projectPathToUpdateTask = new Dictionary(); - private static object _lastUpdateLock = new object(); + private static Dictionary _projectPathToUpdateTask = new Dictionary(); + private static object _lastUpdateLock = new object(); - /// - /// Queue up a re-compile of scripts in a yarn project, add all associated data to the project, - /// and save it back to disk in the same .tres file. This will wait for - /// before updating the project, resetting the timeout each time it is called for a given YarnProject. - /// Call this method when you want to avoid repeated updates of the same project. - /// - /// The yarn project to re-compile scripts for - public static void UpdateYarnProject(YarnProject project) + /// + /// Queue up a re-compile of scripts in a yarn project, add all associated data to the project, + /// and save it back to disk in the same .tres file. This will wait for + /// before updating the project, resetting the timeout each time it is called for a given YarnProject. + /// Call this method when you want to avoid repeated updates of the same project. + /// + /// The yarn project to re-compile scripts for + public static void UpdateYarnProject(YarnProject project) + { + if (project == null) { - if (project == null) - { - return; - } + return; + } - ; - if (string.IsNullOrEmpty(project.ResourcePath)) + ; + if (string.IsNullOrEmpty(project.ResourcePath)) + { + return; + } + + lock (_lastUpdateLock) + { + _projectPathToLastUpdateTime[project.ResourcePath] = DateTime.Now; + if (!_projectPathToUpdateTask.ContainsKey(project.ResourcePath)) { - return; + _projectPathToUpdateTask[project.ResourcePath] = UpdateYarnProjectTask(project); } + } + } + private static async Task UpdateYarnProjectTask(YarnProject project) + { + TimeSpan getTimeDiff() + { lock (_lastUpdateLock) { - _projectPathToLastUpdateTime[project.ResourcePath] = DateTime.Now; - if (!_projectPathToUpdateTask.ContainsKey(project.ResourcePath)) - { - _projectPathToUpdateTask[project.ResourcePath] = UpdateYarnProjectTask(project); - } + return DateTime.Now - _projectPathToLastUpdateTime[project.ResourcePath]; } } - private static async Task UpdateYarnProjectTask(YarnProject project) + while (getTimeDiff() < TimeSpan.FromMilliseconds(PROJECT_UPDATE_TIMEOUT)) { - TimeSpan getTimeDiff() - { - lock (_lastUpdateLock) - { - return DateTime.Now - _projectPathToLastUpdateTime[project.ResourcePath]; - } - } + // wait to update the yarn project until we haven't received another request in PROJECT_UPDATE_TIMEOUT ms + await Task.Delay(PROJECT_UPDATE_TIMEOUT); + } - while (getTimeDiff() < TimeSpan.FromMilliseconds(PROJECT_UPDATE_TIMEOUT)) + try + { + CompileAllScripts(project); + SaveYarnProject(project); + } + catch (Exception e) + { + GD.PushError( + $"Error updating {nameof(YarnProject)} '{project.ResourcePath}': {e.Message}{e.StackTrace}"); + } + finally + { + lock (_lastUpdateLock) { - // wait to update the yarn project until we haven't received another request in PROJECT_UPDATE_TIMEOUT ms - await Task.Delay(PROJECT_UPDATE_TIMEOUT); + _projectPathToUpdateTask.Remove(project.ResourcePath); } + } + } - try + public static void WriteBaseLanguageStringsCSV(YarnProject project, string path) + { + UpdateLocalizationFile(project.baseLocalization.GetStringTableEntries(), + project.JSONProject.BaseLanguage, path, false); + } + + public static void UpdateLocalizationCSVs(YarnProject project) + { + if (project.JSONProject.Localisation.Count > 0) + { + var modifiedFiles = new List(); + if (project.baseLocalization == null) { CompileAllScripts(project); - SaveYarnProject(project); } - catch (Exception e) - { - GD.PushError( - $"Error updating {nameof(YarnProject)} '{project.ResourcePath}': {e.Message}{e.StackTrace}"); - } - finally - { - lock (_lastUpdateLock) - { - _projectPathToUpdateTask.Remove(project.ResourcePath); - } - } - } - public static void WriteBaseLanguageStringsCSV(YarnProject project, string path) - { - UpdateLocalizationFile(project.baseLocalization.GetStringTableEntries(), - project.JSONProject.BaseLanguage, path, false); - } - - public static void UpdateLocalizationCSVs(YarnProject project) - { - if (project.JSONProject.Localisation.Count > 0) + foreach (var loc in project.JSONProject.Localisation) { - var modifiedFiles = new List(); - if (project.baseLocalization == null) + if (string.IsNullOrEmpty(loc.Value.Strings)) { - CompileAllScripts(project); + GD.PrintErr( + $"Can't update localization for {loc.Key} because it doesn't have a Strings file."); + continue; } - foreach (var loc in project.JSONProject.Localisation) - { - if (string.IsNullOrEmpty(loc.Value.Strings)) - { - GD.PrintErr( - $"Can't update localization for {loc.Key} because it doesn't have a Strings file."); - continue; - } + var fileWasChanged = UpdateLocalizationFile(project.baseLocalization.GetStringTableEntries(), + loc.Key, loc.Value.Strings); - var fileWasChanged = UpdateLocalizationFile(project.baseLocalization.GetStringTableEntries(), - loc.Key, loc.Value.Strings); - - if (fileWasChanged) - { - modifiedFiles.Add(loc.Value.Strings); - } - } - - if (modifiedFiles.Count > 0) + if (fileWasChanged) { - GD.Print($"Updated the following files: {string.Join(", ", modifiedFiles)}"); - } - else - { - GD.Print($"No Localization CSV files needed updating."); + modifiedFiles.Add(loc.Value.Strings); } } - } - - /// - /// Verifies the CSV file referred to by csvResourcePath and updates it if - /// necessary. - /// - /// A collection of - /// The language that provides strings for. - /// res:// path to the destination CSV to update - /// Whether the contents of was modified. - private static bool UpdateLocalizationFile(IEnumerable baseLocalizationStrings, - string language, string csvResourcePath, bool generateTranslation = true) - { - var absoluteCSVPath = ProjectSettings.GlobalizePath(csvResourcePath); - - // Tracks if the translated localisation needed modifications - // (either new lines added, old lines removed, or changed lines - // flagged) - var modificationsNeeded = false; - IEnumerable translatedStrings = new List(); - if (File.Exists(absoluteCSVPath)) + if (modifiedFiles.Count > 0) { - var existingCSVText = File.ReadAllText(absoluteCSVPath); - translatedStrings = StringTableEntry.ParseFromCSV(existingCSVText); + GD.Print($"Updated the following files: {string.Join(", ", modifiedFiles)}"); } else { - GD.Print( - $"CSV file {csvResourcePath} did not exist for locale {language}. A new file will be created at that location."); - modificationsNeeded = true; + GD.Print($"No Localization CSV files needed updating."); } + } + } - // Convert both enumerables to dictionaries, for easier lookup - var baseDictionary = baseLocalizationStrings.ToDictionary(entry => entry.ID); - var translatedDictionary = translatedStrings.ToDictionary(entry => entry.ID); + /// + /// Verifies the CSV file referred to by csvResourcePath and updates it if + /// necessary. + /// + /// A collection of + /// The language that provides strings for. + /// res:// path to the destination CSV to update + /// Whether the contents of was modified. + private static bool UpdateLocalizationFile(IEnumerable baseLocalizationStrings, + string language, string csvResourcePath, bool generateTranslation = true) + { + var absoluteCSVPath = ProjectSettings.GlobalizePath(csvResourcePath); - // The list of line IDs present in each localisation - var baseIDs = baseLocalizationStrings.Select(entry => entry.ID); - foreach (var str in translatedStrings) - { - if (baseDictionary.ContainsKey(str.ID)) - { - str.Original = baseDictionary[str.ID].Text; - } - } + // Tracks if the translated localisation needed modifications + // (either new lines added, old lines removed, or changed lines + // flagged) + var modificationsNeeded = false; - var translatedIDs = translatedStrings.Select(entry => entry.ID); + IEnumerable translatedStrings = new List(); + if (File.Exists(absoluteCSVPath)) + { + var existingCSVText = File.ReadAllText(absoluteCSVPath); + translatedStrings = StringTableEntry.ParseFromCSV(existingCSVText); + } + else + { + GD.Print( + $"CSV file {csvResourcePath} did not exist for locale {language}. A new file will be created at that location."); + modificationsNeeded = true; + } - // The list of line IDs that are ONLY present in each - // localisation - var onlyInBaseIDs = baseIDs.Except(translatedIDs); - var onlyInTranslatedIDs = translatedIDs.Except(baseIDs); + // Convert both enumerables to dictionaries, for easier lookup + var baseDictionary = baseLocalizationStrings.ToDictionary(entry => entry.ID); + var translatedDictionary = translatedStrings.ToDictionary(entry => entry.ID); - // Remove every entry whose ID is only present in the - // translated set. This entry has been removed from the base - // localization. - foreach (var id in onlyInTranslatedIDs.ToList()) - { - translatedDictionary.Remove(id); - modificationsNeeded = true; - } - - // Conversely, for every entry that is only present in the base - // localisation, we need to create a new entry for it. - foreach (var id in onlyInBaseIDs) + // The list of line IDs present in each localisation + var baseIDs = baseLocalizationStrings.Select(entry => entry.ID); + foreach (var str in translatedStrings) + { + if (baseDictionary.ContainsKey(str.ID)) { - StringTableEntry baseEntry = baseDictionary[id]; - baseEntry.File = ProjectSettings.LocalizePath(baseEntry.File); - var newEntry = new StringTableEntry(baseEntry) - { - // Empty this text, so that it's apparent that a - // translated version needs to be provided. - Text = string.Empty, - Original = baseEntry.Text, - Language = language, - }; - translatedDictionary.Add(id, newEntry); - modificationsNeeded = true; + str.Original = baseDictionary[str.ID].Text; } + } - // Finally, we need to check for any entries in the translated - // localisation that: - // 1. have the same line ID as one in the base, but - // 2. have a different Lock (the hash of the text), which - // indicates that the base text has changed. - - // First, get the list of IDs that are in both base and - // translated, and then filter this list to any where the lock - // values differ - var outOfDateLockIDs = baseDictionary.Keys - .Intersect(translatedDictionary.Keys) - .Where(id => baseDictionary[id].Lock != translatedDictionary[id].Lock); - - // Now loop over all of these, and update our translated - // dictionary to include a note that it needs attention - foreach (var id in outOfDateLockIDs) - { - // Get the translated entry as it currently exists - var entry = translatedDictionary[id]; - - // Include a note that this entry is out of date - entry.Text = $"(NEEDS UPDATE) {entry.Text}"; - - // update the base language text - entry.Original = baseDictionary[id].Text; - // Update the lock to match the new one - entry.Lock = baseDictionary[id].Lock; + var translatedIDs = translatedStrings.Select(entry => entry.ID); - // Put this modified entry back in the table - translatedDictionary[id] = entry; + // The list of line IDs that are ONLY present in each + // localisation + var onlyInBaseIDs = baseIDs.Except(translatedIDs); + var onlyInTranslatedIDs = translatedIDs.Except(baseIDs); - modificationsNeeded = true; - } + // Remove every entry whose ID is only present in the + // translated set. This entry has been removed from the base + // localization. + foreach (var id in onlyInTranslatedIDs.ToList()) + { + translatedDictionary.Remove(id); + modificationsNeeded = true; + } - // We're all done! + // Conversely, for every entry that is only present in the base + // localisation, we need to create a new entry for it. + foreach (var id in onlyInBaseIDs) + { + StringTableEntry baseEntry = baseDictionary[id]; + baseEntry.File = ProjectSettings.LocalizePath(baseEntry.File); + var newEntry = new StringTableEntry(baseEntry) + { + // Empty this text, so that it's apparent that a + // translated version needs to be provided. + Text = string.Empty, + Original = baseEntry.Text, + Language = language, + }; + translatedDictionary.Add(id, newEntry); + modificationsNeeded = true; + } - if (modificationsNeeded == false) - { - if (generateTranslation) - { - GenerateGodotTranslation(language, csvResourcePath); - } + // Finally, we need to check for any entries in the translated + // localisation that: + // 1. have the same line ID as one in the base, but + // 2. have a different Lock (the hash of the text), which + // indicates that the base text has changed. + + // First, get the list of IDs that are in both base and + // translated, and then filter this list to any where the lock + // values differ + var outOfDateLockIDs = baseDictionary.Keys + .Intersect(translatedDictionary.Keys) + .Where(id => baseDictionary[id].Lock != translatedDictionary[id].Lock); + + // Now loop over all of these, and update our translated + // dictionary to include a note that it needs attention + foreach (var id in outOfDateLockIDs) + { + // Get the translated entry as it currently exists + var entry = translatedDictionary[id]; - // No changes needed to be done to the translated string - // table entries. Stop here. - return false; - } + // Include a note that this entry is out of date + entry.Text = $"(NEEDS UPDATE) {entry.Text}"; - // We need to produce a replacement CSV file for the translated - // entries. + // update the base language text + entry.Original = baseDictionary[id].Text; + // Update the lock to match the new one + entry.Lock = baseDictionary[id].Lock; - var outputStringEntries = translatedDictionary.Values - .OrderBy(entry => entry.File) - .ThenBy(entry => int.Parse(entry.LineNumber)); + // Put this modified entry back in the table + translatedDictionary[id] = entry; - var outputCSV = StringTableEntry.CreateCSV(outputStringEntries); + modificationsNeeded = true; + } - // Write out the replacement text to this existing file, - // replacing its existing contents - File.WriteAllText(absoluteCSVPath, outputCSV, System.Text.Encoding.UTF8); - var csvImport = $"{absoluteCSVPath}.import"; - if (!File.Exists(csvImport)) - { - File.WriteAllText(csvImport, KEEP_IMPORT_TEXT); - } + // We're all done! + if (modificationsNeeded == false) + { if (generateTranslation) { GenerateGodotTranslation(language, csvResourcePath); } - // Signal that the file was changed - return true; + // No changes needed to be done to the translated string + // table entries. Stop here. + return false; } - private static void GenerateGodotTranslation(string language, string csvFilePath) - { - var absoluteCSVPath = ProjectSettings.GlobalizePath(csvFilePath); - var translation = new Translation(); - translation.Locale = language; + // We need to produce a replacement CSV file for the translated + // entries. - var csvText = File.ReadAllText(absoluteCSVPath); - var stringEntries = StringTableEntry.ParseFromCSV(csvText); - foreach (var entry in stringEntries) - { - translation.AddMessage(entry.ID, entry.Text); - } + var outputStringEntries = translatedDictionary.Values + .OrderBy(entry => entry.File) + .ThenBy(entry => int.Parse(entry.LineNumber)); + + var outputCSV = StringTableEntry.CreateCSV(outputStringEntries); - var extensionRegex = new Regex(@".csv$"); - var translationPath = extensionRegex.Replace(absoluteCSVPath, ".translation"); - var translationResPath = ProjectSettings.LocalizePath(translationPath); - ResourceSaver.Save(translation, translationResPath); - GD.Print($"Wrote translation file for {language} to {translationResPath}."); + // Write out the replacement text to this existing file, + // replacing its existing contents + File.WriteAllText(absoluteCSVPath, outputCSV, System.Text.Encoding.UTF8); + var csvImport = $"{absoluteCSVPath}.import"; + if (!File.Exists(csvImport)) + { + File.WriteAllText(csvImport, KEEP_IMPORT_TEXT); } - /// - /// Workaround for https://github.com/godotengine/godot/issues/78513 - /// - public static void ClearJSONCache() + if (generateTranslation) { - var assembly = typeof(JsonSerializerOptions).Assembly; - var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler"); - var clearCacheMethod = - updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public); - clearCacheMethod?.Invoke(null, new object[] {null}); + GenerateGodotTranslation(language, csvResourcePath); } - public static void SaveYarnProject(YarnProject project) + // Signal that the file was changed + return true; + } + + private static void GenerateGodotTranslation(string language, string csvFilePath) + { + var absoluteCSVPath = ProjectSettings.GlobalizePath(csvFilePath); + var translation = new Translation(); + translation.Locale = language; + + var csvText = File.ReadAllText(absoluteCSVPath); + var stringEntries = StringTableEntry.ParseFromCSV(csvText); + foreach (var entry in stringEntries) { - // force the JSON serialization to update before saving - if (GodotObject.IsInstanceValid(project.baseLocalization)) - { - project.baseLocalization.stringTable = project.baseLocalization.stringTable; - } + translation.AddMessage(entry.ID, entry.Text); + } - project.LineMetadata = project.LineMetadata; - project.ListOfFunctions = project.ListOfFunctions; - project.SerializedDeclarations = project.SerializedDeclarations; - if (string.IsNullOrEmpty(project.JSONProjectPath)) - { - project.JSONProjectPath = project.DefaultJSONProjectPath; - } + var extensionRegex = new Regex(@".csv$"); + var translationPath = extensionRegex.Replace(absoluteCSVPath, ".translation"); + var translationResPath = ProjectSettings.LocalizePath(translationPath); + ResourceSaver.Save(translation, translationResPath); + GD.Print($"Wrote translation file for {language} to {translationResPath}."); + } - // Prevent plugin failing to load when code is rebuilt - ClearJSONCache(); - var saveErr = ResourceSaver.Save(project, project.ImportPath); - if (saveErr != Error.Ok) - { - GD.PushError($"Error updating YarnProject {project.ResourceName} to {project.ResourcePath}: {saveErr}"); - } - else - { - GD.Print($"Wrote updated YarnProject {project.ResourceName} to {project.ResourcePath}"); - } + /// + /// Workaround for https://github.com/godotengine/godot/issues/78513 + /// + public static void ClearJSONCache() + { + var assembly = typeof(JsonSerializerOptions).Assembly; + var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler"); + var clearCacheMethod = + updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public); + clearCacheMethod?.Invoke(null, new object[] {null}); + } + + public static void SaveYarnProject(YarnProject project) + { + // force the JSON serialization to update before saving + if (GodotObject.IsInstanceValid(project.baseLocalization)) + { + project.baseLocalization.stringTable = project.baseLocalization.stringTable; } - public static void CompileAllScripts(YarnProject project) + project.LineMetadata = project.LineMetadata; + project.ListOfFunctions = project.ListOfFunctions; + project.SerializedDeclarations = project.SerializedDeclarations; + if (string.IsNullOrEmpty(project.JSONProjectPath)) { - lock (project) - { - List newFunctionList = new List(); - var assetPath = project.ResourcePath; - GD.Print($"Compiling all scripts in {assetPath}"); + project.JSONProjectPath = project.DefaultJSONProjectPath; + } - project.ResourceName = Path.GetFileNameWithoutExtension(assetPath); - var sourceScripts = project.JSONProject.SourceFiles.ToList(); - if (!sourceScripts.Any()) - { - GD.Print( - $"No .yarn files found matching the {nameof(project.JSONProject.SourceFilePatterns)} in {project.JSONProjectPath}"); - return; - } + // Prevent plugin failing to load when code is rebuilt + ClearJSONCache(); + var saveErr = ResourceSaver.Save(project, project.ImportPath); + if (saveErr != Error.Ok) + { + GD.PushError($"Error updating YarnProject {project.ResourceName} to {project.ResourcePath}: {saveErr}"); + } + else + { + GD.Print($"Wrote updated YarnProject {project.ResourceName} to {project.ResourcePath}"); + } + } - var library = new Library(); + public static void CompileAllScripts(YarnProject project) + { + lock (project) + { + List newFunctionList = new List(); + var assetPath = project.ResourcePath; + GD.Print($"Compiling all scripts in {assetPath}"); - IEnumerable errors; - project.ProjectErrors = Array.Empty(); + project.ResourceName = Path.GetFileNameWithoutExtension(assetPath); + var sourceScripts = project.JSONProject.SourceFiles.ToList(); + if (!sourceScripts.Any()) + { + GD.Print( + $"No .yarn files found matching the {nameof(project.JSONProject.SourceFilePatterns)} in {project.JSONProjectPath}"); + return; + } - // We now now compile! - var scriptAbsolutePaths = sourceScripts.ToList().Where(s => s != null) - .Select(ProjectSettings.GlobalizePath).ToList(); - // Store the compiled program - byte[] compiledBytes = null; - CompilationResult? compilationResult = new CompilationResult?(); - if (scriptAbsolutePaths.Count > 0) - { - var job = CompilationJob.CreateFromFiles(scriptAbsolutePaths); - // job.VariableDeclarations = localDeclarations; - job.CompilationType = CompilationJob.Type.FullCompilation; - job.Library = library; - compilationResult = Yarn.Compiler.Compiler.Compile(job); + var library = new Library(); - errors = compilationResult.Value.Diagnostics.Where(d => - d.Severity == Diagnostic.DiagnosticSeverity.Error); + IEnumerable errors; + project.ProjectErrors = Array.Empty(); - if (errors.Count() > 0) + // We now now compile! + var scriptAbsolutePaths = sourceScripts.ToList().Where(s => s != null) + .Select(ProjectSettings.GlobalizePath).ToList(); + // Store the compiled program + byte[] compiledBytes = null; + CompilationResult? compilationResult = new CompilationResult?(); + if (scriptAbsolutePaths.Count > 0) + { + var job = CompilationJob.CreateFromFiles(scriptAbsolutePaths); + // job.VariableDeclarations = localDeclarations; + job.CompilationType = CompilationJob.Type.FullCompilation; + job.Library = library; + compilationResult = Yarn.Compiler.Compiler.Compile(job); + + errors = compilationResult.Value.Diagnostics.Where(d => + d.Severity == Diagnostic.DiagnosticSeverity.Error); + + if (errors.Count() > 0) + { + var errorGroups = errors.GroupBy(e => e.FileName); + foreach (var errorGroup in errorGroups) { - var errorGroups = errors.GroupBy(e => e.FileName); - foreach (var errorGroup in errorGroups) - { - var errorMessages = errorGroup.Select(e => e.ToString()); + var errorMessages = errorGroup.Select(e => e.ToString()); - foreach (var message in errorMessages) - { - GD.PushError($"Error compiling: {message}"); - } + foreach (var message in errorMessages) + { + GD.PushError($"Error compiling: {message}"); } - - var projectErrors = errors.ToList().ConvertAll(e => - new YarnProjectError - { - Context = e.Context, - Message = e.Message, - FileName = ProjectSettings.LocalizePath(e.FileName) - }); - project.ProjectErrors = projectErrors.ToArray(); - return; } - if (compilationResult.Value.Program == null) - { - GD.PushError( - "public error: Failed to compile: resulting program was null, but compiler did not report errors."); - return; - } + var projectErrors = errors.ToList().ConvertAll(e => + new YarnProjectError + { + Context = e.Context, + Message = e.Message, + FileName = ProjectSettings.LocalizePath(e.FileName) + }); + project.ProjectErrors = projectErrors.ToArray(); + return; + } + + if (compilationResult.Value.Program == null) + { + GD.PushError( + "public error: Failed to compile: resulting program was null, but compiler did not report errors."); + return; + } - // Store _all_ declarations - both the ones in this - // .yarnproject file, and the ones inside the .yarn files. + // Store _all_ declarations - both the ones in this + // .yarnproject file, and the ones inside the .yarn files. - // While we're here, filter out any declarations that begin with our - // Yarn public prefix. These are synthesized variables that are - // generated as a result of the compilation, and are not declared by - // the user. + // While we're here, filter out any declarations that begin with our + // Yarn public prefix. These are synthesized variables that are + // generated as a result of the compilation, and are not declared by + // the user. - var newDeclarations = new List() //localDeclarations - .Concat(compilationResult.Value.Declarations) - .Where(decl => !decl.Name.StartsWith("$Yarn.Internal.")) - .Where(decl => !(decl.Type is FunctionType)) - .Select(decl => + var newDeclarations = new List() //localDeclarations + .Concat(compilationResult.Value.Declarations) + .Where(decl => !decl.Name.StartsWith("$Yarn.Internal.")) + .Where(decl => !(decl.Type is FunctionType)) + .Select(decl => + { + SerializedDeclaration existingDeclaration = null; + // try to re-use a declaration if one exists to avoid changing the .tres file so much + foreach (var existing in project.SerializedDeclarations) { - SerializedDeclaration existingDeclaration = null; - // try to re-use a declaration if one exists to avoid changing the .tres file so much - foreach (var existing in project.SerializedDeclarations) + if (existing.name == decl.Name) { - if (existing.name == decl.Name) - { - existingDeclaration = existing; - break; - } + existingDeclaration = existing; + break; } + } - var serialized = existingDeclaration ?? new SerializedDeclaration(); - serialized.SetDeclaration(decl); - return serialized; - }).ToArray(); - project.SerializedDeclarations = newDeclarations; - // Clear error messages from all scripts - they've all passed - // compilation - project.ProjectErrors = Array.Empty(); - - CreateYarnInternalLocalizationAssets(project, compilationResult.Value); + var serialized = existingDeclaration ?? new SerializedDeclaration(); + serialized.SetDeclaration(decl); + return serialized; + }).ToArray(); + project.SerializedDeclarations = newDeclarations; + // Clear error messages from all scripts - they've all passed + // compilation + project.ProjectErrors = Array.Empty(); - using (var memoryStream = new MemoryStream()) - using (var outputStream = new CodedOutputStream(memoryStream)) - { - // Serialize the compiled program to memory - compilationResult.Value.Program.WriteTo(outputStream); - outputStream.Flush(); + CreateYarnInternalLocalizationAssets(project, compilationResult.Value); - compiledBytes = memoryStream.ToArray(); - } - } - - project.ListOfFunctions = newFunctionList.ToArray(); - project.CompiledYarnProgramBase64 = compiledBytes == null ? "" : Convert.ToBase64String(compiledBytes); - var saveErr = ResourceSaver.Save(project, project.ImportPath, - ResourceSaver.SaverFlags.ReplaceSubresourcePaths); - if (saveErr != Error.Ok) + using (var memoryStream = new MemoryStream()) + using (var outputStream = new CodedOutputStream(memoryStream)) { - GD.PushError($"Failed to save updated {nameof(YarnProject)}: {saveErr}"); + // Serialize the compiled program to memory + compilationResult.Value.Program.WriteTo(outputStream); + outputStream.Flush(); + + compiledBytes = memoryStream.ToArray(); } } - } - - private static void LogDiagnostic(Diagnostic diagnostic) - { - var messagePrefix = string.IsNullOrEmpty(diagnostic.FileName) - ? string.Empty - : $"{diagnostic.FileName}: {diagnostic.Range.Start}:{diagnostic.Range.Start.Character} "; - - var message = messagePrefix + diagnostic.Message; - switch (diagnostic.Severity) + project.ListOfFunctions = newFunctionList.ToArray(); + project.CompiledYarnProgramBase64 = compiledBytes == null ? "" : Convert.ToBase64String(compiledBytes); + var saveErr = ResourceSaver.Save(project, project.ImportPath, + ResourceSaver.SaverFlags.ReplaceSubresourcePaths); + if (saveErr != Error.Ok) { - case Diagnostic.DiagnosticSeverity.Error: - GD.PrintErr(message); - break; - case Diagnostic.DiagnosticSeverity.Warning: - GD.Print(message); - break; - case Diagnostic.DiagnosticSeverity.Info: - GD.Print(message); - break; + GD.PushError($"Failed to save updated {nameof(YarnProject)}: {saveErr}"); } } + } - public static CompilationResult CompileProgram(FileInfo[] inputs) - { - // The list of all files and their associated compiled results - var results = new List<(FileInfo file, Program program, IDictionary stringTable)>(); + private static void LogDiagnostic(Diagnostic diagnostic) + { + var messagePrefix = string.IsNullOrEmpty(diagnostic.FileName) + ? string.Empty + : $"{diagnostic.FileName}: {diagnostic.Range.Start}:{diagnostic.Range.Start.Character} "; - var compilationJob = CompilationJob.CreateFromFiles(inputs.Select(fileInfo => fileInfo.FullName)); + var message = messagePrefix + diagnostic.Message; - CompilationResult compilationResult; + switch (diagnostic.Severity) + { + case Diagnostic.DiagnosticSeverity.Error: + GD.PrintErr(message); + break; + case Diagnostic.DiagnosticSeverity.Warning: + GD.Print(message); + break; + case Diagnostic.DiagnosticSeverity.Info: + GD.Print(message); + break; + } + } - try - { - compilationResult = Yarn.Compiler.Compiler.Compile(compilationJob); - } - catch (Exception e) - { - var errorBuilder = new StringBuilder(); + public static CompilationResult CompileProgram(FileInfo[] inputs) + { + // The list of all files and their associated compiled results + var results = new List<(FileInfo file, Program program, IDictionary stringTable)>(); - errorBuilder.AppendLine("Failed to compile because of the following error:"); - errorBuilder.AppendLine(e.ToString()); + var compilationJob = CompilationJob.CreateFromFiles(inputs.Select(fileInfo => fileInfo.FullName)); - GD.PrintErr(errorBuilder.ToString()); - throw new Exception(); - } + CompilationResult compilationResult; - return compilationResult; + try + { + compilationResult = Yarn.Compiler.Compiler.Compile(compilationJob); } - - public static FunctionInfo CreateFunctionInfoFromMethodGroup(MethodInfo method) + catch (Exception e) { - var returnType = $"-> {method.ReturnType.Name}"; + var errorBuilder = new StringBuilder(); - var parameters = method.GetParameters(); - var p = new string[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) - { - var q = parameters[i].ParameterType; - p[i] = parameters[i].Name; - } + errorBuilder.AppendLine("Failed to compile because of the following error:"); + errorBuilder.AppendLine(e.ToString()); - var info = new FunctionInfo(); - info.Name = method.Name; - info.ReturnType = returnType; - info.Parameters = p; - return info; - } - - /// - /// If , will search - /// all assemblies that have been defined using an for commands and actions, when this - /// project is loaded into a . Otherwise, - /// will be used. - /// - /// - public static bool searchAllAssembliesForActions = true; - - - private static void CreateYarnInternalLocalizationAssets(YarnProject project, - CompilationResult compilationResult) - { - // Will we need to create a default localization? This variable - // will be set to false if any of the languages we've - // configured in languagesToSourceAssets is the default - // language. - var shouldAddDefaultLocalization = true; - if (project.JSONProject.Localisation == null) - { - project.JSONProject.Localisation = - new System.Collections.Generic.Dictionary(); - } - - if (shouldAddDefaultLocalization) - { - // We didn't add a localization for the default language. - // Create one for it now. - var stringTableEntries = GetStringTableEntries(project, compilationResult); - - var developmentLocalization = project.baseLocalization ?? new Localization(); - developmentLocalization.Clear(); - developmentLocalization.ResourceName = $"Default ({project.defaultLanguage})"; - developmentLocalization.LocaleCode = project.defaultLanguage; - - // Add these new lines to the development localisation's asset - foreach (var entry in stringTableEntries) - { - developmentLocalization.AddLocalisedStringToAsset(entry.ID, entry); - } + GD.PrintErr(errorBuilder.ToString()); + throw new Exception(); + } - project.baseLocalization = developmentLocalization; + return compilationResult; + } - // Since this is the default language, also populate the line metadata. - project.LineMetadata ??= new LineMetadata(); - project.LineMetadata.Clear(); - project.LineMetadata.AddMetadata(LineMetadataTableEntriesFromCompilationResult(compilationResult)); - } - } + public static FunctionInfo CreateFunctionInfoFromMethodGroup(MethodInfo method) + { + var returnType = $"-> {method.ReturnType.Name}"; - /// - /// Generates a collection of - /// objects, one for each line in this Yarn Project's scripts. - /// - /// An IEnumerable containing a for each of the lines in the Yarn - /// Project, or if the Yarn Project contains - /// errors. - public static IEnumerable GenerateStringsTable(YarnProject project) + var parameters = method.GetParameters(); + var p = new string[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) { - CompilationResult? compilationResult = CompileStringsOnly(project); + var q = parameters[i].ParameterType; + p[i] = parameters[i].Name; + } - if (!compilationResult.HasValue) - { - // We only get no value if we have no scripts to work with. - // In this case, return an empty collection - there's no - // error, but there's no content either. - return new List(); - } + var info = new FunctionInfo(); + info.Name = method.Name; + info.ReturnType = returnType; + info.Parameters = p; + return info; + } - var errors = - compilationResult.Value.Diagnostics.Where(d => d.Severity == Diagnostic.DiagnosticSeverity.Error); + /// + /// If , will search + /// all assemblies that have been defined using an for commands and actions, when this + /// project is loaded into a . Otherwise, + /// will be used. + /// + /// + public static bool searchAllAssembliesForActions = true; - if (errors.Count() > 0) - { - GD.PrintErr("Can't generate a strings table from a Yarn Project that contains compile errors", null); - return null; - } - return GetStringTableEntries(project, compilationResult.Value); + private static void CreateYarnInternalLocalizationAssets(YarnProject project, + CompilationResult compilationResult) + { + // Will we need to create a default localization? This variable + // will be set to false if any of the languages we've + // configured in languagesToSourceAssets is the default + // language. + var shouldAddDefaultLocalization = true; + if (project.JSONProject.Localisation == null) + { + project.JSONProject.Localisation = + new System.Collections.Generic.Dictionary(); } - private static CompilationResult? CompileStringsOnly(YarnProject project) + if (shouldAddDefaultLocalization) { - var scriptPaths = project.JSONProject.SourceFiles.Where(s => s != null) - .Select(s => ProjectSettings.GlobalizePath(s)).ToList(); + // We didn't add a localization for the default language. + // Create one for it now. + var stringTableEntries = GetStringTableEntries(project, compilationResult); + + var developmentLocalization = project.baseLocalization ?? new Localization(); + developmentLocalization.Clear(); + developmentLocalization.ResourceName = $"Default ({project.defaultLanguage})"; + developmentLocalization.LocaleCode = project.defaultLanguage; - if (!scriptPaths.Any()) + // Add these new lines to the development localisation's asset + foreach (var entry in stringTableEntries) { - // We have no scripts to work with. - return null; + developmentLocalization.AddLocalisedStringToAsset(entry.ID, entry); } - // We now now compile! - var job = CompilationJob.CreateFromFiles(scriptPaths); - job.CompilationType = CompilationJob.Type.StringsOnly; + project.baseLocalization = developmentLocalization; - return Yarn.Compiler.Compiler.Compile(job); + // Since this is the default language, also populate the line metadata. + project.LineMetadata ??= new LineMetadata(); + project.LineMetadata.Clear(); + project.LineMetadata.AddMetadata(LineMetadataTableEntriesFromCompilationResult(compilationResult)); } + } - private static IEnumerable LineMetadataTableEntriesFromCompilationResult( - CompilationResult result) + /// + /// Generates a collection of + /// objects, one for each line in this Yarn Project's scripts. + /// + /// An IEnumerable containing a for each of the lines in the Yarn + /// Project, or if the Yarn Project contains + /// errors. + public static IEnumerable GenerateStringsTable(YarnProject project) + { + CompilationResult? compilationResult = CompileStringsOnly(project); + + if (!compilationResult.HasValue) { - return result.StringTable.Select(x => - { - var meta = new LineMetadataTableEntry(); - meta.ID = x.Key; - meta.File = ProjectSettings.LocalizePath(x.Value.fileName); - meta.Node = x.Value.nodeName; - meta.LineNumber = x.Value.lineNumber.ToString(); - meta.Metadata = RemoveLineIDFromMetadata(x.Value.metadata).ToArray(); - return meta; - }).Where(x => x.Metadata.Length > 0); + // We only get no value if we have no scripts to work with. + // In this case, return an empty collection - there's no + // error, but there's no content either. + return new List(); } - private static IEnumerable GetStringTableEntries(YarnProject project, - CompilationResult result) + var errors = + compilationResult.Value.Diagnostics.Where(d => d.Severity == Diagnostic.DiagnosticSeverity.Error); + + if (errors.Count() > 0) { - return result.StringTable.Select(x => - { - var entry = new StringTableEntry(); - - entry.ID = x.Key; - entry.Language = project.defaultLanguage; - entry.Text = x.Value.text; - entry.File = ProjectSettings.LocalizePath(x.Value.fileName); - entry.Node = x.Value.nodeName; - entry.LineNumber = x.Value.lineNumber.ToString(); - entry.Lock = YarnImporter.GetHashString(x.Value.text, 8); - entry.Comment = GenerateCommentWithLineMetadata(x.Value.metadata); - return entry; - } - ); + GD.PrintErr("Can't generate a strings table from a Yarn Project that contains compile errors", null); + return null; } - /// - /// Generates a string with the line metadata. This string is intended - /// to be used in the "comment" column of a strings table CSV. Because - /// of this, it will ignore the line ID if it exists (which is also - /// part of the line metadata). - /// - /// The metadata from a given line. - /// A string prefixed with "Line metadata: ", followed by each - /// piece of metadata separated by whitespace. If no metadata exists or - /// only the line ID is part of the metadata, returns an empty string - /// instead. - private static string GenerateCommentWithLineMetadata(string[] metadata) + return GetStringTableEntries(project, compilationResult.Value); + } + + private static CompilationResult? CompileStringsOnly(YarnProject project) + { + var scriptPaths = project.JSONProject.SourceFiles.Where(s => s != null) + .Select(s => ProjectSettings.GlobalizePath(s)).ToList(); + + if (!scriptPaths.Any()) { - var cleanedMetadata = RemoveLineIDFromMetadata(metadata); + // We have no scripts to work with. + return null; + } + + // We now now compile! + var job = CompilationJob.CreateFromFiles(scriptPaths); + job.CompilationType = CompilationJob.Type.StringsOnly; - if (cleanedMetadata.Count() == 0) + return Yarn.Compiler.Compiler.Compile(job); + } + + private static IEnumerable LineMetadataTableEntriesFromCompilationResult( + CompilationResult result) + { + return result.StringTable.Select(x => + { + var meta = new LineMetadataTableEntry(); + meta.ID = x.Key; + meta.File = ProjectSettings.LocalizePath(x.Value.fileName); + meta.Node = x.Value.nodeName; + meta.LineNumber = x.Value.lineNumber.ToString(); + meta.Metadata = RemoveLineIDFromMetadata(x.Value.metadata).ToArray(); + return meta; + }).Where(x => x.Metadata.Length > 0); + } + + private static IEnumerable GetStringTableEntries(YarnProject project, + CompilationResult result) + { + return result.StringTable.Select(x => { - return string.Empty; - } + var entry = new StringTableEntry(); - return $"Line metadata: {string.Join(" ", cleanedMetadata)}"; - } + entry.ID = x.Key; + entry.Language = project.defaultLanguage; + entry.Text = x.Value.text; + entry.File = ProjectSettings.LocalizePath(x.Value.fileName); + entry.Node = x.Value.nodeName; + entry.LineNumber = x.Value.lineNumber.ToString(); + entry.Lock = YarnImporter.GetHashString(x.Value.text, 8); + entry.Comment = GenerateCommentWithLineMetadata(x.Value.metadata); + return entry; + } + ); + } + /// + /// Generates a string with the line metadata. This string is intended + /// to be used in the "comment" column of a strings table CSV. Because + /// of this, it will ignore the line ID if it exists (which is also + /// part of the line metadata). + /// + /// The metadata from a given line. + /// A string prefixed with "Line metadata: ", followed by each + /// piece of metadata separated by whitespace. If no metadata exists or + /// only the line ID is part of the metadata, returns an empty string + /// instead. + private static string GenerateCommentWithLineMetadata(string[] metadata) + { + var cleanedMetadata = RemoveLineIDFromMetadata(metadata); - /// - /// Removes any line ID entry from an array of line metadata. - /// Line metadata will always contain a line ID entry if it's set. For - /// example, if a line contains "#line:1eaf1e55", its line metadata - /// will always have an entry with "line:1eaf1e55". - /// - /// The array with line metadata. - /// An IEnumerable with any line ID entries removed. - private static IEnumerable RemoveLineIDFromMetadata(string[] metadata) + if (cleanedMetadata.Count() == 0) { - return metadata.Where(x => !x.StartsWith("line:")); + return string.Empty; } - /// - /// Update any .yarn scripts in the project to add #line: tags with - /// unique IDs. - /// - /// The YarnProject to update the scripts for - /// reference to the EditorInterface - public static void AddLineTagsToFilesInYarnProject(YarnProject project) - { - // First, gather all existing line tags across ALL yarn - // projects, so that we don't accidentally overwrite an - // existing one. Do this by finding all yarn scripts in all - // yarn projects, and get the string tags inside them. + return $"Line metadata: {string.Join(" ", cleanedMetadata)}"; + } - var allYarnFiles = - // Get the path for each script - // remove any nulls, in case any are found - // get all yarn projects across the entire project - LoadAllYarnProjects() - // Get all of their source scripts, as a single sequence - .SelectMany(proj => proj.JSONProject.SourceFiles).ToList() - .Where(path => path != null); + + /// + /// Removes any line ID entry from an array of line metadata. + /// Line metadata will always contain a line ID entry if it's set. For + /// example, if a line contains "#line:1eaf1e55", its line metadata + /// will always have an entry with "line:1eaf1e55". + /// + /// The array with line metadata. + /// An IEnumerable with any line ID entries removed. + private static IEnumerable RemoveLineIDFromMetadata(string[] metadata) + { + return metadata.Where(x => !x.StartsWith("line:")); + } + + /// + /// Update any .yarn scripts in the project to add #line: tags with + /// unique IDs. + /// + /// The YarnProject to update the scripts for + /// reference to the EditorInterface + public static void AddLineTagsToFilesInYarnProject(YarnProject project) + { + // First, gather all existing line tags across ALL yarn + // projects, so that we don't accidentally overwrite an + // existing one. Do this by finding all yarn scripts in all + // yarn projects, and get the string tags inside them. + + var allYarnFiles = + // Get the path for each script + // remove any nulls, in case any are found + // get all yarn projects across the entire project + LoadAllYarnProjects() + // Get all of their source scripts, as a single sequence + .SelectMany(proj => proj.JSONProject.SourceFiles).ToList() + .Where(path => path != null); #if YARNSPINNER_DEBUG - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); #endif - var library = new Library(); + var library = new Library(); - // Compile all of these, and get whatever existing string tags - // they had. Do each in isolation so that we can continue even - // if a file contains a parse error. - var allExistingTags = allYarnFiles.SelectMany(path => - { - // Compile this script in strings-only mode to get - // string entries - var compilationJob = CompilationJob.CreateFromFiles(ProjectSettings.GlobalizePath(path)); - compilationJob.CompilationType = CompilationJob.Type.StringsOnly; - compilationJob.Library = library; - var result = Yarn.Compiler.Compiler.Compile(compilationJob); + // Compile all of these, and get whatever existing string tags + // they had. Do each in isolation so that we can continue even + // if a file contains a parse error. + var allExistingTags = allYarnFiles.SelectMany(path => + { + // Compile this script in strings-only mode to get + // string entries + var compilationJob = CompilationJob.CreateFromFiles(ProjectSettings.GlobalizePath(path)); + compilationJob.CompilationType = CompilationJob.Type.StringsOnly; + compilationJob.Library = library; + var result = Yarn.Compiler.Compiler.Compile(compilationJob); - bool containsErrors = result.Diagnostics - .Any(d => d.Severity == Diagnostic.DiagnosticSeverity.Error); + bool containsErrors = result.Diagnostics + .Any(d => d.Severity == Diagnostic.DiagnosticSeverity.Error); - if (containsErrors) + if (containsErrors) + { + GD.PrintErr($"Can't check for existing line tags in {path} because it contains errors."); + return new string[] { - GD.PrintErr($"Can't check for existing line tags in {path} because it contains errors."); - return new string[] - { - }; - } + }; + } - return result.StringTable.Where(i => i.Value.isImplicitTag == false).Select(i => i.Key); - }).ToList(); // immediately execute this query so we can determine timing information + return result.StringTable.Where(i => i.Value.isImplicitTag == false).Select(i => i.Key); + }).ToList(); // immediately execute this query so we can determine timing information #if YARNSPINNER_DEBUG - stopwatch.Stop(); - GD.Print($"Checked {allYarnFiles.Count()} yarn files for line tags in {stopwatch.ElapsedMilliseconds}ms"); + stopwatch.Stop(); + GD.Print($"Checked {allYarnFiles.Count()} yarn files for line tags in {stopwatch.ElapsedMilliseconds}ms"); #endif - var modifiedFiles = new List(); + var modifiedFiles = new List(); - try + try + { + foreach (var script in project.JSONProject.SourceFiles) { - foreach (var script in project.JSONProject.SourceFiles) - { - var assetPath = ProjectSettings.GlobalizePath(script); - var contents = File.ReadAllText(assetPath); - - // Produce a version of this file that contains line - // tags added where they're needed. - var tagged = Yarn.Compiler.Utility.TagLines(contents, allExistingTags); + var assetPath = ProjectSettings.GlobalizePath(script); + var contents = File.ReadAllText(assetPath); - var taggedVersion = tagged.Item1; + // Produce a version of this file that contains line + // tags added where they're needed. + var tagged = Yarn.Compiler.Utility.TagLines(contents, allExistingTags); - // if the file has an error it returns null - // we want to bail out then otherwise we'd wipe the yarn file - if (taggedVersion == null) - { - continue; - } + var taggedVersion = tagged.Item1; - // If this produced a modified version of the file, - // write it out and re-import it. - if (contents != taggedVersion) - { - modifiedFiles.Add(Path.GetFileNameWithoutExtension(assetPath)); - File.WriteAllText(assetPath, taggedVersion, Encoding.UTF8); - } + // if the file has an error it returns null + // we want to bail out then otherwise we'd wipe the yarn file + if (taggedVersion == null) + { + continue; } - } - catch (Exception e) - { - GD.PrintErr($"Encountered an error when updating scripts: {e}"); - return; - } - // Report on the work we did. - if (modifiedFiles.Count > 0) - { - GD.Print($"Updated the following files: {string.Join(", ", modifiedFiles)}"); - // trigger reimport - YarnSpinnerPlugin.editorInterface.GetResourceFilesystem().ScanSources(); - } - else - { - GD.Print("No files needed updating."); + // If this produced a modified version of the file, + // write it out and re-import it. + if (contents != taggedVersion) + { + modifiedFiles.Add(Path.GetFileNameWithoutExtension(assetPath)); + File.WriteAllText(assetPath, taggedVersion, Encoding.UTF8); + } } } + catch (Exception e) + { + GD.PrintErr($"Encountered an error when updating scripts: {e}"); + return; + } - /// - /// Load all known YarnProject resources in the project - /// - /// a list of all YarnProject resources - public static List LoadAllYarnProjects() + // Report on the work we did. + if (modifiedFiles.Count > 0) { - var projects = new List(); - var allProjects = FindAllYarnProjects(); - foreach (var path in allProjects) - { - projects.Add(ResourceLoader.Load(path.ToString())); - } + GD.Print($"Updated the following files: {string.Join(", ", modifiedFiles)}"); + // trigger reimport + YarnSpinnerPlugin.editorInterface.GetResourceFilesystem().ScanSources(); + } + else + { + GD.Print("No files needed updating."); + } + } - return projects; + /// + /// Load all known YarnProject resources in the project + /// + /// a list of all YarnProject resources + public static List LoadAllYarnProjects() + { + var projects = new List(); + var allProjects = FindAllYarnProjects(); + foreach (var path in allProjects) + { + projects.Add(ResourceLoader.Load(path.ToString())); } + + return projects; } } #endif \ No newline at end of file diff --git a/addons/YarnSpinner-Godot/Editor/YarnProjectImporter.cs b/addons/YarnSpinner-Godot/Editor/YarnProjectImporter.cs index 04243ea..962917a 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnProjectImporter.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnProjectImporter.cs @@ -5,90 +5,89 @@ using Godot; using Godot.Collections; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +/// +/// A for YarnSpinner JSON project files (.yarnproject files) +/// +public partial class YarnProjectImporter : EditorImportPlugin { - /// - /// A for YarnSpinner JSON project files (.yarnproject files) - /// - public partial class YarnProjectImporter : EditorImportPlugin + public override string[] _GetRecognizedExtensions() => + new[] + { + "yarnproject" + }; + + public override string _GetImporterName() { - public override string[] _GetRecognizedExtensions() => - new[] - { - "yarnproject" - }; + return "yarnproject"; + } - public override string _GetImporterName() - { - return "yarnproject"; - } + public override string _GetVisibleName() + { + return "Yarn Project"; + } - public override string _GetVisibleName() - { - return "Yarn Project"; - } + public override string _GetSaveExtension() => "tres"; - public override string _GetSaveExtension() => "tres"; + public override string _GetResourceType() + { + return "Resource"; + } - public override string _GetResourceType() - { - return "Resource"; - } + public override int _GetPresetCount() + { + return 0; + } - public override int _GetPresetCount() - { - return 0; - } + public override float _GetPriority() + { + return 1.0f; + } - public override float _GetPriority() - { - return 1.0f; - } + public override int _GetImportOrder() + { + return 0; + } - public override int _GetImportOrder() + public override Array _GetImportOptions(string path, int presetIndex) + { + return new Array(); + } + + public override Error _Import( + string assetPath, + string savePath, + Dictionary options, + Array platformVariants, + Array genFiles) + { + var stopwatch = new System.Diagnostics.Stopwatch(); + stopwatch.Start(); + YarnProject godotProject = null; + var fullSavePath = $"{savePath}.{_GetSaveExtension()}"; + try { - return 0; + godotProject = ResourceLoader.Load(assetPath); } - - public override Array _GetImportOptions(string path, int presetIndex) + catch (Exception e) { - return new Array(); + GD.PushError( + $"Error loading existing {nameof(YarnProject)}: {e.Message}\n{e.StackTrace}. Creating new resource."); } - public override Error _Import( - string assetPath, - string savePath, - Dictionary options, - Array platformVariants, - Array genFiles) + godotProject ??= new YarnProject(); + godotProject.JSONProjectPath = assetPath; + godotProject.ImportPath = fullSavePath; + godotProject.ResourceName = Path.GetFileName(assetPath); + var saveErr = ResourceSaver.Save(godotProject, godotProject.ImportPath); + if (saveErr != Error.Ok) { - var stopwatch = new System.Diagnostics.Stopwatch(); - stopwatch.Start(); - YarnProject godotProject = null; - var fullSavePath = $"{savePath}.{_GetSaveExtension()}"; - try - { - godotProject = ResourceLoader.Load(assetPath); - } - catch (Exception e) - { - GD.PushError( - $"Error loading existing {nameof(YarnProject)}: {e.Message}\n{e.StackTrace}. Creating new resource."); - } - - godotProject ??= new YarnProject(); - godotProject.JSONProjectPath = assetPath; - godotProject.ImportPath = fullSavePath; - godotProject.ResourceName = Path.GetFileName(assetPath); - var saveErr = ResourceSaver.Save(godotProject, godotProject.ImportPath); - if (saveErr != Error.Ok) - { - GD.PrintErr($"Error saving .yarnproject file import: {saveErr.ToString()}"); - } - - YarnProjectEditorUtility.UpdateYarnProject(godotProject); - return (int) Error.Ok; + GD.PrintErr($"Error saving .yarnproject file import: {saveErr.ToString()}"); } + + YarnProjectEditorUtility.UpdateYarnProject(godotProject); + return (int) Error.Ok; } } #endif \ No newline at end of file diff --git a/addons/YarnSpinner-Godot/Editor/YarnProjectInspectorPlugin.cs b/addons/YarnSpinner-Godot/Editor/YarnProjectInspectorPlugin.cs index ea2fbb8..f8f71d4 100644 --- a/addons/YarnSpinner-Godot/Editor/YarnProjectInspectorPlugin.cs +++ b/addons/YarnSpinner-Godot/Editor/YarnProjectInspectorPlugin.cs @@ -11,646 +11,645 @@ using YarnSpinnerGodot.Editor.UI; -namespace YarnSpinnerGodot.Editor +namespace YarnSpinnerGodot.Editor; + +[Tool] +public partial class YarnProjectInspectorPlugin : EditorInspectorPlugin { - [Tool] - public partial class YarnProjectInspectorPlugin : EditorInspectorPlugin + private YarnCompileErrorsPropertyEditor _compileErrorsPropertyEditor; + private ScrollContainer _parseErrorControl; + private YarnProject _project; + + private readonly PackedScene _fileNameLabelScene = + ResourceLoader.Load( + "res://addons/YarnSpinner-Godot/Editor/UI/FilenameLabel.tscn"); + + private readonly PackedScene _errorTextLabelScene = + ResourceLoader.Load( + "res://addons/YarnSpinner-Godot/Editor/UI/ErrorTextLabel.tscn"); + + private readonly PackedScene _contextLabelScene = + ResourceLoader.Load( + "res://addons/YarnSpinner-Godot/Editor/UI/ContextLabel.tscn"); + + private VBoxContainer _errorContainer; + private RichTextLabel _sourceScriptsListLabel; + private LineEditWithSubmit _localeTextEntry; + private bool _addLocaleConnected; + private string _pendingCSVFileLocaleCode; + private LineEdit _baseLocaleInput; + + public override bool _CanHandle(GodotObject obj) + { + return obj is YarnProject; + } + + public override bool _ParseProperty(GodotObject @object, Variant.Type type, + string path, + PropertyHint hint, string hintText, PropertyUsageFlags usage, bool wide) { - private YarnCompileErrorsPropertyEditor _compileErrorsPropertyEditor; - private ScrollContainer _parseErrorControl; - private YarnProject _project; - - private readonly PackedScene _fileNameLabelScene = - ResourceLoader.Load( - "res://addons/YarnSpinner-Godot/Editor/UI/FilenameLabel.tscn"); - - private readonly PackedScene _errorTextLabelScene = - ResourceLoader.Load( - "res://addons/YarnSpinner-Godot/Editor/UI/ErrorTextLabel.tscn"); - - private readonly PackedScene _contextLabelScene = - ResourceLoader.Load( - "res://addons/YarnSpinner-Godot/Editor/UI/ContextLabel.tscn"); - - private VBoxContainer _errorContainer; - private RichTextLabel _sourceScriptsListLabel; - private LineEditWithSubmit _localeTextEntry; - private bool _addLocaleConnected; - private string _pendingCSVFileLocaleCode; - private LineEdit _baseLocaleInput; - - public override bool _CanHandle(GodotObject obj) + if (@object is not YarnProject project) { - return obj is YarnProject; + return false; } - public override bool _ParseProperty(GodotObject @object, Variant.Type type, - string path, - PropertyHint hint, string hintText, PropertyUsageFlags usage, bool wide) + if (IsTresYarnProject(project)) { - if (@object is not YarnProject project) - { - return false; - } + return true; + } - if (IsTresYarnProject(project)) + try + { + _project = project; + // hide some properties that are not editable by the user + string[] hideProperties = + { + nameof(YarnProject.LastImportHadAnyStrings), + nameof(YarnProject.LastImportHadImplicitStringIDs), + nameof(YarnProject.IsSuccessfullyParsed), + nameof(YarnProject.CompiledYarnProgramBase64), + nameof(YarnProject.baseLocalization), + nameof(YarnProject.ImportPath), + nameof(YarnProject.JSONProjectPath), + // can't use nameof for private fields here + "_baseLocalizationJSON", + "_lineMetadataJSON", + "_listOfFunctionsJSON", + }; + if (hideProperties.Contains(path)) { + // hide these properties from inspector return true; } - try + if (path == nameof(YarnProject.ProjectErrors)) { - _project = project; - // hide some properties that are not editable by the user - string[] hideProperties = + _compileErrorsPropertyEditor = + new YarnCompileErrorsPropertyEditor(); + AddPropertyEditor(path, _compileErrorsPropertyEditor); + _parseErrorControl = new ScrollContainer { - nameof(YarnProject.LastImportHadAnyStrings), - nameof(YarnProject.LastImportHadImplicitStringIDs), - nameof(YarnProject.IsSuccessfullyParsed), - nameof(YarnProject.CompiledYarnProgramBase64), - nameof(YarnProject.baseLocalization), - nameof(YarnProject.ImportPath), - nameof(YarnProject.JSONProjectPath), - // can't use nameof for private fields here - "_baseLocalizationJSON", - "_lineMetadataJSON", - "_listOfFunctionsJSON", + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill, }; - if (hideProperties.Contains(path)) + var errorAreaHeight = 40; + if (_project.ProjectErrors != null && + _project.ProjectErrors.Length > 0) { - // hide these properties from inspector - return true; + errorAreaHeight = 200; } - if (path == nameof(YarnProject.ProjectErrors)) - { - _compileErrorsPropertyEditor = - new YarnCompileErrorsPropertyEditor(); - AddPropertyEditor(path, _compileErrorsPropertyEditor); - _parseErrorControl = new ScrollContainer - { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill, - }; - var errorAreaHeight = 40; - if (_project.ProjectErrors != null && - _project.ProjectErrors.Length > 0) - { - errorAreaHeight = 200; - } + _parseErrorControl.CustomMinimumSize = + new Vector2(0, errorAreaHeight); - _parseErrorControl.CustomMinimumSize = - new Vector2(0, errorAreaHeight); + _errorContainer = new VBoxContainer + { + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill, + }; + _parseErrorControl.AddChild(_errorContainer); + _compileErrorsPropertyEditor.Connect( + YarnCompileErrorsPropertyEditor.SignalName.OnErrorsUpdate, + Callable.From(RenderCompilationErrors)); + RenderCompilationErrors(); + AddCustomControl(_parseErrorControl); + return true; + } - _errorContainer = new VBoxContainer + if (path == "_serializedDeclarationsJSON") + { + var header = new HBoxContainer(); + header.AddChild(new Label + { + Text = " Story Variables", + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill, + }); + header.AddChild(new Label + { + Text = _project.SerializedDeclarations == null || _project.SerializedDeclarations.Length == 0 + ? "None" + : _project.SerializedDeclarations.Length.ToString(CultureInfo.InvariantCulture), + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill, + }); + AddCustomControl(header); + if (_project.SerializedDeclarations is {Length: >= 1}) + { + var scrollContainer = new ScrollContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsVertical = Control.SizeFlags.ExpandFill, }; - _parseErrorControl.AddChild(_errorContainer); - _compileErrorsPropertyEditor.Connect( - YarnCompileErrorsPropertyEditor.SignalName.OnErrorsUpdate, - Callable.From(RenderCompilationErrors)); - RenderCompilationErrors(); - AddCustomControl(_parseErrorControl); - return true; - } - if (path == "_serializedDeclarationsJSON") - { - var header = new HBoxContainer(); - header.AddChild(new Label + var vbox = new VBoxContainer { - Text = " Story Variables", SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill, - }); - header.AddChild(new Label - { - Text = _project.SerializedDeclarations == null || _project.SerializedDeclarations.Length == 0 - ? "None" - : _project.SerializedDeclarations.Length.ToString(CultureInfo.InvariantCulture), - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill, - }); - AddCustomControl(header); - if (_project.SerializedDeclarations is {Length: >= 1}) + SizeFlagsVertical = Control.SizeFlags.ShrinkBegin, + }; + scrollContainer.AddChild(vbox); + foreach (var declaration in _project.SerializedDeclarations) { - var scrollContainer = new ScrollContainer + var labelText = $"{declaration.name} ({declaration.typeName})\n"; + if (declaration.isImplicit) { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ExpandFill, - }; + labelText += "Implicitly declared."; + } + else + { + labelText += $"Declared in {declaration.sourceYarnAssetPath}\n"; + } - var vbox = new VBoxContainer + var typeName = declaration.typeName; + var defaultValue = ""; + if (typeName == BuiltinTypes.String.Name) { - SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, - SizeFlagsVertical = Control.SizeFlags.ShrinkBegin, - }; - scrollContainer.AddChild(vbox); - foreach (var declaration in _project.SerializedDeclarations) + defaultValue = declaration.defaultValueString; + } + else if (typeName == BuiltinTypes.Boolean.Name) { - var labelText = $"{declaration.name} ({declaration.typeName})\n"; - if (declaration.isImplicit) - { - labelText += "Implicitly declared."; - } - else - { - labelText += $"Declared in {declaration.sourceYarnAssetPath}\n"; - } - - var typeName = declaration.typeName; - var defaultValue = ""; - if (typeName == BuiltinTypes.String.Name) - { - defaultValue = declaration.defaultValueString; - } - else if (typeName == BuiltinTypes.Boolean.Name) - { - defaultValue = declaration.defaultValueBool.ToString(); - } - else if (typeName == BuiltinTypes.Number.Name) - { - defaultValue = declaration.defaultValueNumber.ToString(CultureInfo.InvariantCulture); - } - - labelText += $"Default value: {defaultValue}\n"; - var label = _fileNameLabelScene.Instantiate