update dialogue manager
							parent
							
								
									e3d6fbb714
								
							
						
					
					
						commit
						f07ce2f5df
					
				|  | @ -1,144 +1,394 @@ | |||
| using Godot; | ||||
| using Godot.Collections; | ||||
| using System; | ||||
| using System.Reflection; | ||||
| using System.Threading.Tasks; | ||||
| 
 | ||||
| #nullable enable | ||||
| 
 | ||||
| namespace DialogueManagerRuntime | ||||
| { | ||||
|   public partial class DialogueManager : Node | ||||
|   { | ||||
|     public static async Task<DialogueLine> GetNextDialogueLine(Resource dialogueResource, string key = "0", Array<Variant> extraGameStates = null) | ||||
|     public enum TranslationSource | ||||
|     { | ||||
|       var dialogueManager = Engine.GetSingleton("DialogueManager"); | ||||
|       dialogueManager.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|       var result = await dialogueManager.ToSignal(dialogueManager, "bridge_get_next_dialogue_line_completed"); | ||||
|         None, | ||||
|         Guess, | ||||
|         CSV, | ||||
|         PO | ||||
|     } | ||||
| 
 | ||||
|       if ((RefCounted)result[0] == null) return null; | ||||
|     public partial class DialogueManager : Node | ||||
|     { | ||||
|         public delegate void PassedTitleEventHandler(string title); | ||||
|         public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); | ||||
|         public delegate void MutatedEventHandler(Dictionary mutation); | ||||
|         public delegate void DialogueEndedEventHandler(Resource dialogueResource); | ||||
| 
 | ||||
|       return new DialogueLine((RefCounted)result[0]); | ||||
|         public static PassedTitleEventHandler? PassedTitle; | ||||
|         public static GotDialogueEventHandler? GotDialogue; | ||||
|         public static MutatedEventHandler? Mutated; | ||||
|         public static DialogueEndedEventHandler? DialogueEnded; | ||||
| 
 | ||||
|         [Signal] public delegate void ResolvedEventHandler(Variant value); | ||||
| 
 | ||||
|         private static GodotObject? instance; | ||||
|         public static GodotObject Instance | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (instance == null) | ||||
|                 { | ||||
|                     instance = Engine.GetSingleton("DialogueManager"); | ||||
|                 } | ||||
|                 return instance; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static Godot.Collections.Array GameStates | ||||
|         { | ||||
|             get => (Godot.Collections.Array)Instance.Get("game_states"); | ||||
|             set => Instance.Set("game_states", value); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static bool IncludeSingletons | ||||
|         { | ||||
|             get => (bool)Instance.Get("include_singletons"); | ||||
|             set => Instance.Set("include_singletons", value); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static bool IncludeClasses | ||||
|         { | ||||
|             get => (bool)Instance.Get("include_classes"); | ||||
|             set => Instance.Set("include_classes", value); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static TranslationSource TranslationSource | ||||
|         { | ||||
|             get => (TranslationSource)(int)Instance.Get("translation_source"); | ||||
|             set => Instance.Set("translation_source", (int)value); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static Func<Node> GetCurrentScene | ||||
|         { | ||||
|             set => Instance.Set("get_current_scene", Callable.From(value)); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public void Prepare() | ||||
|         { | ||||
|             Instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title))); | ||||
|             Instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line)))); | ||||
|             Instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation))); | ||||
|             Instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource))); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static async Task<GodotObject> GetSingleton() | ||||
|         { | ||||
|             if (instance != null) return instance; | ||||
| 
 | ||||
|             var tree = Engine.GetMainLoop(); | ||||
|             int x = 0; | ||||
| 
 | ||||
|             // Try and find the singleton for a few seconds | ||||
|             while (!Engine.HasSingleton("DialogueManager") && x < 300) | ||||
|             { | ||||
|                 await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame); | ||||
|                 x++; | ||||
|             } | ||||
| 
 | ||||
|             // If it times out something is wrong | ||||
|             if (x >= 300) | ||||
|             { | ||||
|                 throw new Exception("The DialogueManager singleton is missing."); | ||||
|             } | ||||
| 
 | ||||
|             instance = Engine.GetSingleton("DialogueManager"); | ||||
|             return instance; | ||||
|         } | ||||
| 
 | ||||
|         public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) | ||||
|         { | ||||
|             Instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|             var result = await Instance.ToSignal(Instance, "bridge_get_next_dialogue_line_completed"); | ||||
| 
 | ||||
|             if ((RefCounted)result[0] == null) return null; | ||||
| 
 | ||||
|             return new DialogueLine((RefCounted)result[0]); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) | ||||
|         { | ||||
|             return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) | ||||
|         { | ||||
|             return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|         } | ||||
| 
 | ||||
|         public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) | ||||
|         { | ||||
|             return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|         } | ||||
| 
 | ||||
|         public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) | ||||
|         { | ||||
|             return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) | ||||
|         { | ||||
|             return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false) | ||||
|         { | ||||
|             Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation); | ||||
|             await Instance.ToSignal(Instance, "bridge_mutated"); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public bool ThingHasMethod(GodotObject thing, string method) | ||||
|         { | ||||
|             MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); | ||||
|             return info != null; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args) | ||||
|         { | ||||
|             MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); | ||||
| 
 | ||||
|             if (info == null) return; | ||||
| 
 | ||||
| #nullable disable | ||||
|             // Convert the method args to something reflection can handle | ||||
|             ParameterInfo[] argTypes = info.GetParameters(); | ||||
|             object[] _args = new object[argTypes.Length]; | ||||
|             for (int i = 0; i < argTypes.Length; i++) | ||||
|             { | ||||
|                 if (i < args.Count && args[i].Obj != null) | ||||
|                 { | ||||
|                     _args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType); | ||||
|                 } | ||||
|                 else if (argTypes[i].DefaultValue != null) | ||||
|                 { | ||||
|                     _args[i] = argTypes[i].DefaultValue; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Add a single frame wait in case the method returns before signals can listen | ||||
|             await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame); | ||||
| 
 | ||||
|             if (info.ReturnType == typeof(Task)) | ||||
|             { | ||||
|                 await (Task)info.Invoke(thing, _args); | ||||
|                 EmitSignal(SignalName.Resolved, null); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var value = (Variant)info.Invoke(thing, _args); | ||||
|                 EmitSignal(SignalName.Resolved, value); | ||||
|             } | ||||
|         } | ||||
| #nullable enable | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static void ShowExampleDialogueBalloon(Resource dialogueResource, string key = "0", Array<Variant> extraGameStates = null) | ||||
|     public partial class DialogueLine : RefCounted | ||||
|     { | ||||
|       Engine.GetSingleton("DialogueManager").Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>()); | ||||
|     } | ||||
|   } | ||||
|         private string type = "dialogue"; | ||||
|         public string Type | ||||
|         { | ||||
|             get => type; | ||||
|             set => type = value; | ||||
|         } | ||||
| 
 | ||||
|         private string next_id = ""; | ||||
|         public string NextId | ||||
|         { | ||||
|             get => next_id; | ||||
|             set => next_id = value; | ||||
|         } | ||||
| 
 | ||||
|         private string character = ""; | ||||
|         public string Character | ||||
|         { | ||||
|             get => character; | ||||
|             set => character = value; | ||||
|         } | ||||
| 
 | ||||
|         private string text = ""; | ||||
|         public string Text | ||||
|         { | ||||
|             get => text; | ||||
|             set => text = value; | ||||
|         } | ||||
| 
 | ||||
|         private string translation_key = ""; | ||||
|         public string TranslationKey | ||||
|         { | ||||
|             get => translation_key; | ||||
|             set => translation_key = value; | ||||
|         } | ||||
| 
 | ||||
|         private Array<DialogueResponse> responses = new Array<DialogueResponse>(); | ||||
|         public Array<DialogueResponse> Responses | ||||
|         { | ||||
|             get => responses; | ||||
|         } | ||||
| 
 | ||||
|         private string? time = null; | ||||
|         public string? Time | ||||
|         { | ||||
|             get => time; | ||||
|         } | ||||
| 
 | ||||
|         private Dictionary pauses = new Dictionary(); | ||||
|         public Dictionary Pauses | ||||
|         { | ||||
|             get => pauses; | ||||
|         } | ||||
| 
 | ||||
|         private Dictionary speeds = new Dictionary(); | ||||
|         public Dictionary Speeds | ||||
|         { | ||||
|             get => speeds; | ||||
|         } | ||||
| 
 | ||||
|         private Array<Godot.Collections.Array> inline_mutations = new Array<Godot.Collections.Array>(); | ||||
|         public Array<Godot.Collections.Array> InlineMutations | ||||
|         { | ||||
|             get => inline_mutations; | ||||
|         } | ||||
| 
 | ||||
|         private Array<Variant> extra_game_states = new Array<Variant>(); | ||||
| 
 | ||||
|         private Array<string> tags = new Array<string>(); | ||||
|         public Array<string> Tags | ||||
|         { | ||||
|             get => tags; | ||||
|         } | ||||
| 
 | ||||
|         public DialogueLine(RefCounted data) | ||||
|         { | ||||
|             type = (string)data.Get("type"); | ||||
|             next_id = (string)data.Get("next_id"); | ||||
|             character = (string)data.Get("character"); | ||||
|             text = (string)data.Get("text"); | ||||
|             translation_key = (string)data.Get("translation_key"); | ||||
|             pauses = (Dictionary)data.Get("pauses"); | ||||
|             speeds = (Dictionary)data.Get("speeds"); | ||||
|             inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations"); | ||||
|             time = (string)data.Get("time"); | ||||
|             tags = (Array<string>)data.Get("tags"); | ||||
| 
 | ||||
|             foreach (var response in (Array<RefCounted>)data.Get("responses")) | ||||
|             { | ||||
|                 responses.Add(new DialogueResponse(response)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|   public partial class DialogueLine : RefCounted | ||||
|   { | ||||
|     private string type = "dialogue"; | ||||
|     public string Type | ||||
|     { | ||||
|       get => type; | ||||
|       set => type = value; | ||||
|     } | ||||
|         public string GetTagValue(string tagName) | ||||
|         { | ||||
|             string wrapped = $"{tagName}="; | ||||
|             foreach (var tag in tags) | ||||
|             { | ||||
|                 if (tag.StartsWith(wrapped)) | ||||
|                 { | ||||
|                     return tag.Substring(wrapped.Length); | ||||
|                 } | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
| 
 | ||||
|     private string next_id = ""; | ||||
|     public string NextId | ||||
|     { | ||||
|       get => next_id; | ||||
|       set => next_id = value; | ||||
|     } | ||||
| 
 | ||||
|     private string character = ""; | ||||
|     public string Character | ||||
|     { | ||||
|       get => character; | ||||
|       set => character = value; | ||||
|     } | ||||
| 
 | ||||
|     private string text = ""; | ||||
|     public string Text | ||||
|     { | ||||
|       get => text; | ||||
|       set => text = value; | ||||
|     } | ||||
| 
 | ||||
|     private string translation_key = ""; | ||||
|     public string TranslationKey | ||||
|     { | ||||
|       get => translation_key; | ||||
|       set => translation_key = value; | ||||
|     } | ||||
| 
 | ||||
|     private Array<DialogueResponse> responses = new Array<DialogueResponse>(); | ||||
|     public Array<DialogueResponse> Responses | ||||
|     { | ||||
|       get => responses; | ||||
|     } | ||||
| 
 | ||||
|     private string time = null; | ||||
|     public string Time | ||||
|     { | ||||
|       get => time; | ||||
|     } | ||||
| 
 | ||||
|     private Dictionary pauses = new Dictionary(); | ||||
|     private Dictionary speeds = new Dictionary(); | ||||
| 
 | ||||
|     private Array<Array> inline_mutations = new Array<Array>(); | ||||
| 
 | ||||
|     private Array<Variant> extra_game_states = new Array<Variant>(); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     public DialogueLine(RefCounted data) | ||||
|     { | ||||
|       type = (string)data.Get("type"); | ||||
|       next_id = (string)data.Get("next_id"); | ||||
|       character = (string)data.Get("character"); | ||||
|       text = (string)data.Get("text"); | ||||
|       translation_key = (string)data.Get("translation_key"); | ||||
|       pauses = (Dictionary)data.Get("pauses"); | ||||
|       speeds = (Dictionary)data.Get("speeds"); | ||||
|       inline_mutations = (Array<Array>)data.Get("inline_mutations"); | ||||
| 
 | ||||
|       foreach (var response in (Array<RefCounted>)data.Get("responses")) | ||||
|       { | ||||
|         responses.Add(new DialogueResponse(response)); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   public partial class DialogueResponse : RefCounted | ||||
|   { | ||||
|     private string next_id = ""; | ||||
|     public string NextId | ||||
|     { | ||||
|       get => next_id; | ||||
|       set => next_id = value; | ||||
|     } | ||||
| 
 | ||||
|     private bool is_allowed = true; | ||||
|     public bool IsAllowed | ||||
|     { | ||||
|       get => is_allowed; | ||||
|       set => is_allowed = value; | ||||
|     } | ||||
| 
 | ||||
|     private string text = ""; | ||||
|     public string Text | ||||
|     { | ||||
|       get => text; | ||||
|       set => text = value; | ||||
|     } | ||||
| 
 | ||||
|     private string translation_key = ""; | ||||
|     public string TranslationKey | ||||
|     { | ||||
|       get => translation_key; | ||||
|       set => translation_key = value; | ||||
|         public override string ToString() | ||||
|         { | ||||
|             switch (type) | ||||
|             { | ||||
|                 case "dialogue": | ||||
|                     return $"<DialogueLine character=\"{character}\" text=\"{text}\">"; | ||||
|                 case "mutation": | ||||
|                     return "<DialogueLine mutation>"; | ||||
|                 default: | ||||
|                     return ""; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public DialogueResponse(RefCounted data) | ||||
|     public partial class DialogueResponse : RefCounted | ||||
|     { | ||||
|       next_id = (string)data.Get("next_id"); | ||||
|       is_allowed = (bool)data.Get("is_allowed"); | ||||
|       text = (string)data.Get("text"); | ||||
|       translation_key = (string)data.Get("translation_key"); | ||||
|         private string next_id = ""; | ||||
|         public string NextId | ||||
|         { | ||||
|             get => next_id; | ||||
|             set => next_id = value; | ||||
|         } | ||||
| 
 | ||||
|         private bool is_allowed = true; | ||||
|         public bool IsAllowed | ||||
|         { | ||||
|             get => is_allowed; | ||||
|             set => is_allowed = value; | ||||
|         } | ||||
| 
 | ||||
|         private string text = ""; | ||||
|         public string Text | ||||
|         { | ||||
|             get => text; | ||||
|             set => text = value; | ||||
|         } | ||||
| 
 | ||||
|         private string translation_key = ""; | ||||
|         public string TranslationKey | ||||
|         { | ||||
|             get => translation_key; | ||||
|             set => translation_key = value; | ||||
|         } | ||||
| 
 | ||||
|         private Array<string> tags = new Array<string>(); | ||||
|         public Array<string> Tags | ||||
|         { | ||||
|             get => tags; | ||||
|         } | ||||
| 
 | ||||
|         public DialogueResponse(RefCounted data) | ||||
|         { | ||||
|             next_id = (string)data.Get("next_id"); | ||||
|             is_allowed = (bool)data.Get("is_allowed"); | ||||
|             text = (string)data.Get("text"); | ||||
|             translation_key = (string)data.Get("translation_key"); | ||||
|             tags = (Array<string>)data.Get("tags"); | ||||
|         } | ||||
| 
 | ||||
|         public string GetTagValue(string tagName) | ||||
|         { | ||||
|             string wrapped = $"{tagName}="; | ||||
|             foreach (var tag in tags) | ||||
|             { | ||||
|                 if (tag.StartsWith(wrapped)) | ||||
|                 { | ||||
|                     return tag.Substring(wrapped.Length); | ||||
|                 } | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
| 
 | ||||
|         public override string ToString() | ||||
|         { | ||||
|             return $"<DialogueResponse text=\"{text}\""; | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| MIT License | ||||
| 
 | ||||
| Copyright (c) 2022-present Nathan Hoad | ||||
| Copyright (c) 2022-present Nathan Hoad and Dialogue Manager contributors. | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
|  |  | |||
|  | @ -0,0 +1,52 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
| 
 | ||||
| <svg | ||||
|    width="16" | ||||
|    height="16" | ||||
|    viewBox="0 0 4.2333333 4.2333335" | ||||
|    version="1.1" | ||||
|    id="svg291" | ||||
|    inkscape:version="1.3 (0e150ed6c4, 2023-07-21)" | ||||
|    sodipodi:docname="responses_menu.svg" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <sodipodi:namedview | ||||
|      id="namedview293" | ||||
|      pagecolor="#ffffff" | ||||
|      bordercolor="#666666" | ||||
|      borderopacity="1.0" | ||||
|      inkscape:pageshadow="2" | ||||
|      inkscape:pageopacity="0.0" | ||||
|      inkscape:pagecheckerboard="0" | ||||
|      inkscape:document-units="px" | ||||
|      showgrid="false" | ||||
|      width="1920px" | ||||
|      units="px" | ||||
|      borderlayer="true" | ||||
|      inkscape:showpageshadow="false" | ||||
|      inkscape:deskcolor="#d1d1d1" | ||||
|      inkscape:zoom="45.254834" | ||||
|      inkscape:cx="7.8334173" | ||||
|      inkscape:cy="6.5959804" | ||||
|      inkscape:window-width="2560" | ||||
|      inkscape:window-height="1377" | ||||
|      inkscape:window-x="-8" | ||||
|      inkscape:window-y="-8" | ||||
|      inkscape:window-maximized="1" | ||||
|      inkscape:current-layer="layer1" /> | ||||
|   <defs | ||||
|      id="defs288" /> | ||||
|   <g | ||||
|      inkscape:label="Layer 1" | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer1"> | ||||
|     <path | ||||
|        id="rect181" | ||||
|        style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.77487;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill" | ||||
|        d="M 1.5875 0.26458334 L 1.5875 0.79375001 L 4.2333334 0.79375001 L 4.2333334 0.26458334 L 1.5875 0.26458334 z M 0 0.83147381 L 0 2.4189738 L 1.3229167 1.6252238 L 0 0.83147381 z M 1.5875 1.3229167 L 1.5875 1.8520834 L 4.2333334 1.8520834 L 4.2333334 1.3229167 L 1.5875 1.3229167 z M 1.5875 2.38125 L 1.5875 2.9104167 L 4.2333334 2.9104167 L 4.2333334 2.38125 L 1.5875 2.38125 z M 1.5875 3.4395834 L 1.5875 3.9687501 L 4.2333334 3.9687501 L 4.2333334 3.4395834 L 1.5875 3.4395834 z " | ||||
|        fill="#E0E0E0" /> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.0 KiB | 
|  | @ -0,0 +1,38 @@ | |||
| [remap] | ||||
| 
 | ||||
| importer="texture" | ||||
| type="CompressedTexture2D" | ||||
| uid="uid://drjfciwitjm83" | ||||
| path="res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex" | ||||
| metadata={ | ||||
| "has_editor_variant": true, | ||||
| "vram_texture": false | ||||
| } | ||||
| 
 | ||||
| [deps] | ||||
| 
 | ||||
| source_file="res://addons/dialogue_manager/assets/responses_menu.svg" | ||||
| dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"] | ||||
| 
 | ||||
| [params] | ||||
| 
 | ||||
| compress/mode=0 | ||||
| compress/high_quality=false | ||||
| compress/lossy_quality=0.7 | ||||
| compress/hdr_compression=1 | ||||
| compress/normal_map=0 | ||||
| compress/channel_pack=0 | ||||
| mipmaps/generate=false | ||||
| mipmaps/limit=-1 | ||||
| roughness/mode=0 | ||||
| roughness/src_normal="" | ||||
| process/fix_alpha_border=true | ||||
| process/premult_alpha=false | ||||
| process/normal_map_invert_y=false | ||||
| process/hdr_as_srgb=false | ||||
| process/hdr_clamp_exposure=false | ||||
| process/size_limit=0 | ||||
| detect_3d/compress_to=1 | ||||
| svg/scale=1.0 | ||||
| editor/scale_with_editor_scale=true | ||||
| editor/convert_colors_with_editor_theme=true | ||||
|  | @ -7,6 +7,9 @@ signal error_clicked(line_number: int) | |||
| signal external_file_requested(path: String, title: String) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueSyntaxHighlighter = preload("./code_edit_syntax_highlighter.gd") | ||||
| 
 | ||||
| 
 | ||||
| # A link back to the owner MainView | ||||
| var main_view | ||||
| 
 | ||||
|  | @ -15,50 +18,9 @@ var theme_overrides: Dictionary: | |||
| 	set(value): | ||||
| 		theme_overrides = value | ||||
| 
 | ||||
| 		syntax_highlighter.clear_color_regions() | ||||
| 		syntax_highlighter.clear_keyword_colors() | ||||
| 
 | ||||
| 		# Imports | ||||
| 		syntax_highlighter.add_keyword_color("import", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("as", theme_overrides.conditions_color) | ||||
| 
 | ||||
| 		# Titles | ||||
| 		syntax_highlighter.add_color_region("~", "~", theme_overrides.titles_color, true) | ||||
| 
 | ||||
| 		# Comments | ||||
| 		syntax_highlighter.add_color_region("#", "##", theme_overrides.comments_color, true) | ||||
| 
 | ||||
| 		# Conditions | ||||
| 		syntax_highlighter.add_keyword_color("if", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("elif", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("else", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("while", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("endif", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("in", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("and", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("or", theme_overrides.conditions_color) | ||||
| 		syntax_highlighter.add_keyword_color("not", theme_overrides.conditions_color) | ||||
| 
 | ||||
| 		# Values | ||||
| 		syntax_highlighter.add_keyword_color("true", theme_overrides.numbers_color) | ||||
| 		syntax_highlighter.add_keyword_color("false", theme_overrides.numbers_color) | ||||
| 		syntax_highlighter.number_color = theme_overrides.numbers_color | ||||
| 		syntax_highlighter.add_color_region("\"", "\"", theme_overrides.strings_color) | ||||
| 
 | ||||
| 		# Mutations | ||||
| 		syntax_highlighter.add_keyword_color("do", theme_overrides.mutations_color) | ||||
| 		syntax_highlighter.add_keyword_color("set", theme_overrides.mutations_color) | ||||
| 		syntax_highlighter.function_color = theme_overrides.mutations_color | ||||
| 		syntax_highlighter.member_variable_color = theme_overrides.members_color | ||||
| 
 | ||||
| 		# Jumps | ||||
| 		syntax_highlighter.add_color_region("=>", "<=", theme_overrides.jumps_color, true) | ||||
| 
 | ||||
| 		# Dialogue | ||||
| 		syntax_highlighter.add_color_region(": ", "::", theme_overrides.text_color, true) | ||||
| 		syntax_highlighter = DialogueSyntaxHighlighter.new() | ||||
| 
 | ||||
| 		# General UI | ||||
| 		syntax_highlighter.symbol_color = theme_overrides.symbols_color | ||||
| 		add_theme_color_override("font_color", theme_overrides.text_color) | ||||
| 		add_theme_color_override("background_color", theme_overrides.background_color) | ||||
| 		add_theme_color_override("current_line_color", theme_overrides.current_line_color) | ||||
|  | @ -92,6 +54,8 @@ var font_size: int: | |||
| 	get: | ||||
| 		return font_size | ||||
| 
 | ||||
| var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s") | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
| 	# Add error gutter | ||||
|  | @ -102,6 +66,8 @@ func _ready() -> void: | |||
| 	if not has_comment_delimiter("#"): | ||||
| 		add_comment_delimiter("#", "", true) | ||||
| 
 | ||||
| 	syntax_highlighter = DialogueSyntaxHighlighter.new() | ||||
| 
 | ||||
| 
 | ||||
| func _gui_input(event: InputEvent) -> void: | ||||
| 	if event is InputEventKey and event.is_pressed(): | ||||
|  | @ -192,8 +158,7 @@ func _request_code_completion(force: bool) -> void: | |||
| 		parser.free() | ||||
| 		return | ||||
| 
 | ||||
| #	var last_character: String = current_line.substr(cursor.x - 1, 1) | ||||
| 	var name_so_far: String = current_line.strip_edges() | ||||
| 	var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "") | ||||
| 	if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]: | ||||
| 		# Only show names starting with that character | ||||
| 		var names: PackedStringArray = get_character_names(name_so_far) | ||||
|  | @ -260,10 +225,10 @@ func check_active_title() -> void: | |||
| 	# Look at each line above this one to find the next title line | ||||
| 	for i in range(line_number, -1, -1): | ||||
| 		if lines[i].begins_with("~ "): | ||||
| 			emit_signal("active_title_change", lines[i].replace("~ ", "")) | ||||
| 			active_title_change.emit(lines[i].replace("~ ", "")) | ||||
| 			return | ||||
| 
 | ||||
| 	emit_signal("active_title_change", "0") | ||||
| 	active_title_change.emit("") | ||||
| 
 | ||||
| 
 | ||||
| # Move the caret line to match a given title | ||||
|  | @ -280,7 +245,7 @@ func get_character_names(beginning_with: String) -> PackedStringArray: | |||
| 	var lines = text.split("\n") | ||||
| 	for line in lines: | ||||
| 		if ": " in line: | ||||
| 			var name: String = line.split(": ")[0].strip_edges() | ||||
| 			var name: String = WEIGHTED_RANDOM_PREFIX.sub(line.split(": ")[0].strip_edges(), "") | ||||
| 			if not name in names and matches_prompt(beginning_with, name): | ||||
| 				names.append(name) | ||||
| 	return names | ||||
|  | @ -320,47 +285,68 @@ func insert_text(text: String) -> void: | |||
| 
 | ||||
| # Toggle the selected lines as comments | ||||
| func toggle_comment() -> void: | ||||
| 	# Starting complex operation so that the entire toggle comment can be undone in a single step | ||||
| 	begin_complex_operation() | ||||
| 
 | ||||
| 	var caret_count: int = get_caret_count() | ||||
| 	var caret_offsets: Dictionary = {} | ||||
| 	var comment_delimiter: String = delimiter_comments[0] | ||||
| 	var is_first_line: bool = true | ||||
| 	var will_comment: bool = true | ||||
| 	var selections: Array = [] | ||||
| 	var line_offsets: Dictionary = {} | ||||
| 
 | ||||
| 	for caret_index in caret_count: | ||||
| 		var caret_line: int = get_caret_line(caret_index) | ||||
| 		var from: int = caret_line | ||||
| 		var to: int = caret_line | ||||
| 	for caret_index in range(0, get_caret_count()): | ||||
| 		var from_line: int = get_caret_line(caret_index) | ||||
| 		var from_column: int = get_caret_column(caret_index) | ||||
| 		var to_line: int = get_caret_line(caret_index) | ||||
| 		var to_column: int = get_caret_column(caret_index) | ||||
| 
 | ||||
| 		if has_selection(caret_index): | ||||
| 			from = get_selection_from_line(caret_index) | ||||
| 			to = get_selection_to_line(caret_index) | ||||
| 			from_line = get_selection_from_line(caret_index) | ||||
| 			to_line = get_selection_to_line(caret_index) | ||||
| 			from_column = get_selection_from_column(caret_index) | ||||
| 			to_column = get_selection_to_column(caret_index) | ||||
| 
 | ||||
| 		for line in range(from, to + 1): | ||||
| 			if line not in caret_offsets: | ||||
| 				caret_offsets[line] = 0 | ||||
| 		selections.append({ | ||||
| 			from_line = from_line, | ||||
| 			from_column = from_column, | ||||
| 			to_line = to_line, | ||||
| 			to_column = to_column | ||||
| 		}) | ||||
| 
 | ||||
| 			var line_text: String = get_line(line) | ||||
| 			var comment_delimiter: String = delimiter_comments[0] | ||||
| 			var is_line_commented: bool = line_text.begins_with(comment_delimiter) | ||||
| 			set_line(line, line_text.substr(comment_delimiter.length()) if is_line_commented else comment_delimiter + line_text) | ||||
| 			caret_offsets[line] += (-1 if is_line_commented else 1) * comment_delimiter.length() | ||||
| 		for line_number in range(from_line, to_line + 1): | ||||
| 			if line_offsets.has(line_number): continue | ||||
| 
 | ||||
| 		emit_signal("lines_edited_from", from, to) | ||||
| 			var line_text: String = get_line(line_number) | ||||
| 
 | ||||
| 	# Readjust carets and selection positions after all carets effect have been calculated | ||||
| 	# Tried making it in the above loop, but that causes a weird behaviour if two carets are on the same line (first caret will move, but not the second one) | ||||
| 	for caret_index in caret_count: | ||||
| 		if has_selection(caret_index): | ||||
| 			var from: int = get_selection_from_line(caret_index) | ||||
| 			var to: int = get_selection_to_line(caret_index) | ||||
| 			select(from, get_selection_from_column(caret_index) + caret_offsets[from], to, get_selection_to_column(caret_index) + caret_offsets[to], caret_index) | ||||
| 			# The first line determines if we are commenting or uncommentingg | ||||
| 			if is_first_line: | ||||
| 				is_first_line = false | ||||
| 				will_comment = not line_text.strip_edges().begins_with(comment_delimiter) | ||||
| 
 | ||||
| 		set_caret_column(get_caret_column(caret_index) + caret_offsets[get_caret_line(caret_index)], true, caret_index) | ||||
| 			# Only comment/uncomment if the current line needs to | ||||
| 			if will_comment: | ||||
| 				set_line(line_number, comment_delimiter + line_text) | ||||
| 				line_offsets[line_number] = 1 | ||||
| 			elif line_text.begins_with(comment_delimiter): | ||||
| 				set_line(line_number, line_text.substr(comment_delimiter.length())) | ||||
| 				line_offsets[line_number] = -1 | ||||
| 			else: | ||||
| 				line_offsets[line_number] = 0 | ||||
| 
 | ||||
| 	for caret_index in range(0, get_caret_count()): | ||||
| 		var selection: Dictionary = selections[caret_index] | ||||
| 		select( | ||||
| 			selection.from_line, | ||||
| 			selection.from_column + line_offsets[selection.from_line], | ||||
| 			selection.to_line, | ||||
| 			selection.to_column + line_offsets[selection.to_line], | ||||
| 			caret_index | ||||
| 		) | ||||
| 		set_caret_column(selection.from_column + line_offsets[selection.from_line], false, caret_index) | ||||
| 
 | ||||
| 	end_complex_operation() | ||||
| 
 | ||||
| 	emit_signal("text_set") | ||||
| 	emit_signal("text_changed") | ||||
| 	text_set.emit() | ||||
| 	text_changed.emit() | ||||
| 
 | ||||
| 
 | ||||
| # Move the selected lines up or down | ||||
|  | @ -395,7 +381,7 @@ func move_line(offset: int) -> void: | |||
| 	if reselect: | ||||
| 		select(from, 0, to, get_line_width(to)) | ||||
| 	set_cursor(cursor) | ||||
| 	emit_signal("text_changed") | ||||
| 	text_changed.emit() | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
|  | @ -415,7 +401,7 @@ func _on_code_edit_symbol_validate(symbol: String) -> void: | |||
| 
 | ||||
| func _on_code_edit_symbol_lookup(symbol: String, line: int, column: int) -> void: | ||||
| 	if symbol.begins_with("res://") and symbol.ends_with(".dialogue"): | ||||
| 		emit_signal("external_file_requested", symbol, "") | ||||
| 		external_file_requested.emit(symbol, "") | ||||
| 	else: | ||||
| 		go_to_title(symbol) | ||||
| 
 | ||||
|  | @ -436,4 +422,4 @@ func _on_code_edit_caret_changed() -> void: | |||
| func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void: | ||||
| 	var line_errors = errors.filter(func(error): return error.line_number == line) | ||||
| 	if line_errors.size() > 0: | ||||
| 		emit_signal("error_clicked", line) | ||||
| 		error_clicked.emit(line) | ||||
|  |  | |||
											
												
													File diff suppressed because one or more lines are too long
												
											
										
									
								|  | @ -0,0 +1,382 @@ | |||
| @tool | ||||
| extends SyntaxHighlighter | ||||
| 
 | ||||
| 
 | ||||
| enum ExpressionType {DO, SET, IF} | ||||
| 
 | ||||
| 
 | ||||
| var dialogue_manager_parser: DialogueManagerParser = DialogueManagerParser.new() | ||||
| 
 | ||||
| var regex_titles: RegEx = RegEx.create_from_string("^\\s*(?<title>~\\s+[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)") | ||||
| var regex_comments: RegEx = RegEx.create_from_string("(?:(?>\"(?:\\\\\"|[^\"\\n])*\")[^\"\\n]*?\\s*(?<comment>#[^\\n]*)$|^[^\"#\\n]*?\\s*(?<comment2>#[^\\n]*))") | ||||
| var regex_mutation: RegEx = RegEx.create_from_string("^\\s*(do|do!|set) (?<mutation>.*)") | ||||
| var regex_condition: RegEx = RegEx.create_from_string("^\\s*(if|elif|while|else if) (?<condition>.*)") | ||||
| var regex_wcondition: RegEx = RegEx.create_from_string("\\[if (?<condition>((?:[^\\[\\]]*)|(?:\\[(?1)\\]))*?)\\]") | ||||
| var regex_wendif: RegEx = RegEx.create_from_string("\\[(\\/if|else)\\]") | ||||
| var regex_rgroup: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]") | ||||
| var regex_endconditions: RegEx = RegEx.create_from_string("^\\s*(endif|else):?\\s*$") | ||||
| var regex_tags: RegEx = RegEx.create_from_string("\\[(?<tag>(?!(?:ID:.*)|if)[a-zA-Z_][a-zA-Z0-9_]*!?)(?:[= ](?<val>[^\\[\\]]+))?\\](?:(?<text>(?!\\[\\/\\k<tag>\\]).*?)?(?<end>\\[\\/\\k<tag>\\]))?") | ||||
| var regex_dialogue: RegEx = RegEx.create_from_string("^\\s*(?:(?<random>\\%[\\d.]* )|(?<response>- ))?(?:(?<character>[^#:]*): )?(?<dialogue>.*)$") | ||||
| var regex_goto: RegEx = RegEx.create_from_string("=><? (?:(?<file>[^\\/]+)\\/)?(?<title>[^\\/]*)") | ||||
| var regex_string: RegEx = RegEx.create_from_string("^(?<delimiter>[\"'])(?<content>(?:\\\\{2})*|(?:.*?[^\\\\](?:\\\\{2})*))\\1$") | ||||
| var regex_escape: RegEx = RegEx.create_from_string("\\\\.") | ||||
| var regex_number: RegEx = RegEx.create_from_string("^-?(?:(?:0x(?:[0-9A-Fa-f]{2})+)|(?:0b[01]+)|(?:\\d+(?:(?:[\\.]\\d*)?(?:e\\d+)?)|(?:_\\d+)+)?)$") | ||||
| var regex_array: RegEx = RegEx.create_from_string("\\[((?>[^\\[\\]]+|(?R))*)\\]") | ||||
| var regex_dict: RegEx = RegEx.create_from_string("^\\{((?>[^\\{\\}]+|(?R))*)\\}$") | ||||
| var regex_kvdict: RegEx = RegEx.create_from_string("^\\s*(?<left>.*?)\\s*(?<colon>:|=)\\s*(?<right>[^\\/]+)$") | ||||
| var regex_commas: RegEx = RegEx.create_from_string("([^,]+)(?:\\s*,\\s*)?") | ||||
| var regex_assignment: RegEx = RegEx.create_from_string("^\\s*(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*(?<op>(?:\\/|\\*|-|\\+)?=)\\s*(?<val>.*)$") | ||||
| var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|not|in|null)(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*$") | ||||
| var regex_keyword: RegEx = RegEx.create_from_string("^\\s*(true|false|null)\\s*$") | ||||
| var regex_function: RegEx = RegEx.create_from_string("^\\s*([a-zA-Z_][a-zA-Z_0-9]*\\s*)\\(") | ||||
| var regex_comparison: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s*(?<op>==|>=|<=|<|>|!=)\\s*(?<right>.*)$") | ||||
| var regex_blogical: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s+(?<op>and|or|in)\\s+(?<right>.*)$") | ||||
| var regex_ulogical: RegEx = RegEx.create_from_string("^\\s*(?<op>not)\\s+(?<right>.*)$") | ||||
| var regex_paren: RegEx = RegEx.create_from_string("\\((?<paren>((?:[^\\(\\)]*)|(?:\\((?1)\\)))*?)\\)") | ||||
| 
 | ||||
| var cache: Dictionary = {} | ||||
| 
 | ||||
| 
 | ||||
| func _notification(what: int) -> void: | ||||
| 	if what == NOTIFICATION_PREDELETE: | ||||
| 		dialogue_manager_parser.free() | ||||
| 
 | ||||
| 
 | ||||
| func _clear_highlighting_cache() -> void: | ||||
| 	cache = {} | ||||
| 
 | ||||
| 
 | ||||
| ## Returns the syntax coloring for a dialogue file line | ||||
| func _get_line_syntax_highlighting(line: int) -> Dictionary: | ||||
| 	var colors: Dictionary = {} | ||||
| 	var text_edit: TextEdit = get_text_edit() | ||||
| 	var text: String = text_edit.get_line(line) | ||||
| 
 | ||||
| 	# Prevents an error from popping up while developing | ||||
| 	if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty(): | ||||
| 		return colors | ||||
| 
 | ||||
| 	# Disable this, as well as the line at the bottom of this function to remove the cache. | ||||
| 	if text in cache: | ||||
| 		return cache[text] | ||||
| 
 | ||||
| 	# Comments, we have to remove them at this point so the rest of the processing is easier | ||||
| 	# Counts both end-of-line and single-line comments | ||||
| 	# Comments are not allowed within dialogue lines or response lines, so we ask the parser what it thinks the current line is | ||||
| 	if not (dialogue_manager_parser.is_dialogue_line(text) or dialogue_manager_parser.is_response_line(text)) or dialogue_manager_parser.is_line_empty(text) or dialogue_manager_parser.is_import_line(text): | ||||
| 		var comment_matches: Array[RegExMatch] = regex_comments.search_all(text) | ||||
| 		for comment_match in comment_matches: | ||||
| 			for i in ["comment", "comment2"]: | ||||
| 				if i in comment_match.names: | ||||
| 					colors[comment_match.get_start(i)] = {"color": text_edit.theme_overrides.comments_color} | ||||
| 					text = text.substr(0, comment_match.get_start(i)) | ||||
| 
 | ||||
| 	# Dialogues. | ||||
| 	var dialogue_matches: Array[RegExMatch] = regex_dialogue.search_all(text) | ||||
| 	for dialogue_match in dialogue_matches: | ||||
| 		if "random" in dialogue_match.names: | ||||
| 			colors[dialogue_match.get_start("random")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 			colors[dialogue_match.get_end("random")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		if "response" in dialogue_match.names: | ||||
| 			colors[dialogue_match.get_start("response")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 			colors[dialogue_match.get_end("response")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		if "character" in dialogue_match.names: | ||||
| 			colors[dialogue_match.get_start("character")] = {"color": text_edit.theme_overrides.members_color} | ||||
| 			colors[dialogue_match.get_end("character")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_dialogue_syntax_highlighting(dialogue_match.get_start("dialogue"), dialogue_match.get_string("dialogue")), true) | ||||
| 
 | ||||
| 	# Title lines. | ||||
| 	if dialogue_manager_parser.is_title_line(text): | ||||
| 		var title_matches: Array[RegExMatch] = regex_titles.search_all(text) | ||||
| 		for title_match in title_matches: | ||||
| 			colors[title_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color} | ||||
| 
 | ||||
| 	# Import lines. | ||||
| 	var import_matches: Array[RegExMatch] = dialogue_manager_parser.IMPORT_REGEX.search_all(text) | ||||
| 	for import_match in import_matches: | ||||
| 		colors[import_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[import_match.get_start("path") - 1] = {"color": text_edit.theme_overrides.strings_color} | ||||
| 		colors[import_match.get_end("path") + 1] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[import_match.get_start("prefix")] = {"color": text_edit.theme_overrides.members_color} | ||||
| 		colors[import_match.get_end("prefix")] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 
 | ||||
| 	# Using clauses | ||||
| 	var using_matches: Array[RegExMatch] = dialogue_manager_parser.USING_REGEX.search_all(text) | ||||
| 	for using_match in using_matches: | ||||
| 		colors[using_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[using_match.get_start("state") - 1] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	# Condition keywords and expressions. | ||||
| 	var condition_matches: Array[RegExMatch] = regex_condition.search_all(text) | ||||
| 	for condition_match in condition_matches: | ||||
| 		colors[condition_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[condition_match.get_end(1)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_expression_syntax_highlighting(condition_match.get_start("condition"), ExpressionType.IF, condition_match.get_string("condition")), true) | ||||
| 	# endif/else | ||||
| 	var endcondition_matches: Array[RegExMatch] = regex_endconditions.search_all(text) | ||||
| 	for endcondition_match in endcondition_matches: | ||||
| 		colors[endcondition_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[endcondition_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 
 | ||||
| 	# Mutations. | ||||
| 	var mutation_matches: Array[RegExMatch] = regex_mutation.search_all(text) | ||||
| 	for mutation_match in mutation_matches: | ||||
| 		colors[mutation_match.get_start(0)] = {"color": text_edit.theme_overrides.mutations_color} | ||||
| 		colors.merge(_get_expression_syntax_highlighting(mutation_match.get_start("mutation"), ExpressionType.DO if mutation_match.strings[1] == "do" else ExpressionType.SET, mutation_match.get_string("mutation")), true) | ||||
| 
 | ||||
| 	# CodeEdit seems to have issues if the Dictionary keys weren't added in order? | ||||
| 	var new_colors: Dictionary = {} | ||||
| 	var ordered_keys: Array = colors.keys() | ||||
| 	ordered_keys.sort() | ||||
| 	for index in ordered_keys: | ||||
| 		new_colors[index] = colors[index] | ||||
| 
 | ||||
| 	cache[text] = new_colors | ||||
| 	return new_colors | ||||
| 
 | ||||
| 
 | ||||
| ## Returns the syntax highlighting for a dialogue line | ||||
| func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictionary: | ||||
| 	var text_edit: TextEdit = get_text_edit() | ||||
| 	var colors: Dictionary = {} | ||||
| 
 | ||||
| 	# #tag style tags | ||||
| 	var hashtag_matches: Array[RegExMatch] = dialogue_manager_parser.TAGS_REGEX.search_all(text) | ||||
| 	for hashtag_match in hashtag_matches: | ||||
| 		colors[start_index + hashtag_match.get_start(0)] = { "color": text_edit.theme_overrides.comments_color } | ||||
| 		colors[start_index + hashtag_match.get_end(0)] = { "color": text_edit.theme_overrides.text_color } | ||||
| 
 | ||||
| 	# Global tags, like bbcode. | ||||
| 	var tag_matches: Array[RegExMatch] = regex_tags.search_all(text) | ||||
| 	for tag_match in tag_matches: | ||||
| 		colors[start_index + tag_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		if "val" in tag_match.names: | ||||
| 			colors.merge(_get_literal_syntax_highlighting(start_index + tag_match.get_start("val"), tag_match.get_string("val")), true) | ||||
| 			colors[start_index + tag_match.get_end("val")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		# Showing the text color straight in the editor for better ease-of-use | ||||
| 		if tag_match.get_string("tag") == "color": | ||||
| 			colors[start_index + tag_match.get_start("val")] = {"color": Color.from_string(tag_match.get_string("val"), text_edit.theme_overrides.text_color)} | ||||
| 		if "text" in tag_match.names: | ||||
| 			colors[start_index + tag_match.get_start("text")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 			# Text can still contain tags if several effects are applied ([center][b]Something[/b][/center], so recursing | ||||
| 			colors.merge(_get_dialogue_syntax_highlighting(start_index + tag_match.get_start("text"), tag_match.get_string("text")), true) | ||||
| 			colors[start_index + tag_match.get_end("text")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		if "end" in tag_match.names: | ||||
| 			colors[start_index + tag_match.get_start("end")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 			colors[start_index + tag_match.get_end("end")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors[start_index + tag_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	# ID tag. | ||||
| 	var translation_matches: Array[RegExMatch] = dialogue_manager_parser.TRANSLATION_REGEX.search_all(text) | ||||
| 	for translation_match in translation_matches: | ||||
| 		colors[start_index + translation_match.get_start(0)] = {"color": text_edit.theme_overrides.comments_color} | ||||
| 		colors[start_index + translation_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	# Replacements. | ||||
| 	var replacement_matches: Array[RegExMatch] = dialogue_manager_parser.REPLACEMENTS_REGEX.search_all(text) | ||||
| 	for replacement_match in replacement_matches: | ||||
| 		colors[start_index + replacement_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + replacement_match.get_start(1)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + replacement_match.get_start(1), replacement_match.strings[1]), true) | ||||
| 		colors[start_index + replacement_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + replacement_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	# Jump at the end of a response. | ||||
| 	var goto_matches: Array[RegExMatch] = regex_goto.search_all(text) | ||||
| 	for goto_match in goto_matches: | ||||
| 		colors[start_index + goto_match.get_start(0)] = {"color": text_edit.theme_overrides.jumps_color} | ||||
| 		if "file" in goto_match.names: | ||||
| 			colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.members_color} | ||||
| 			colors[start_index + goto_match.get_end("file")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color} | ||||
| 		colors[start_index + goto_match.get_end("title")] = {"color": text_edit.theme_overrides.jumps_color} | ||||
| 		colors[start_index + goto_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	# Wrapped condition. | ||||
| 	var wcondition_matches: Array[RegExMatch] = regex_wcondition.search_all(text) | ||||
| 	for wcondition_match in wcondition_matches: | ||||
| 		colors[start_index + wcondition_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + wcondition_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[start_index + wcondition_match.get_start(0) + 3] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + wcondition_match.get_start("condition"), wcondition_match.get_string("condition")), true) | ||||
| 		colors[start_index + wcondition_match.get_end("condition")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + wcondition_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 	# [/if] tag for color matching with the opening tag | ||||
| 	var wendif_matches: Array[RegExMatch] = regex_wendif.search_all(text) | ||||
| 	for wendif_match in wendif_matches: | ||||
| 		colors[start_index + wendif_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + wendif_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[start_index + wendif_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + wendif_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	# Random groups | ||||
| 	var rgroup_matches: Array[RegExMatch] = regex_rgroup.search_all(text) | ||||
| 	for rgroup_match in rgroup_matches: | ||||
| 		colors[start_index + rgroup_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + rgroup_match.get_start("options")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		var separator_matches: Array[RegExMatch] = RegEx.create_from_string("\\|").search_all(rgroup_match.get_string("options")) | ||||
| 		for separator_match in separator_matches: | ||||
| 			colors[start_index + rgroup_match.get_start("options") + separator_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 			colors[start_index + rgroup_match.get_start("options") + separator_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors[start_index + rgroup_match.get_end("options")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + rgroup_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 	return colors | ||||
| 
 | ||||
| 
 | ||||
| ## Returns the syntax highlighting for an expression (mutation set/do, or condition) | ||||
| func _get_expression_syntax_highlighting(start_index: int, type: ExpressionType, text: String) -> Dictionary: | ||||
| 	var text_edit: TextEdit = get_text_edit() | ||||
| 	var colors: Dictionary = {} | ||||
| 
 | ||||
| 	if type == ExpressionType.SET: | ||||
| 		var assignment_matches: Array[RegExMatch] = regex_assignment.search_all(text) | ||||
| 		for assignment_match in assignment_matches: | ||||
| 			colors[start_index + assignment_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 			if "attr" in assignment_match.names: | ||||
| 				colors[start_index + assignment_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color} | ||||
| 				colors[start_index + assignment_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 			if "key" in assignment_match.names: | ||||
| 				# Braces are outside of the key, so coloring them symbols_color | ||||
| 				colors[start_index + assignment_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 				colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("key"), assignment_match.get_string("key")), true) | ||||
| 				colors[start_index + assignment_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 				colors[start_index + assignment_match.get_end("key") + 1] = {"color": text_edit.theme_overrides.text_color} | ||||
| 
 | ||||
| 			colors[start_index + assignment_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 			colors[start_index + assignment_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 			colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("val"), assignment_match.get_string("val")), true) | ||||
| 	else: | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index, text), true) | ||||
| 
 | ||||
| 	return colors | ||||
| 
 | ||||
| 
 | ||||
| ## Returns the syntax highlighting for a literal. | ||||
| ## For this purpose, "literal" refers to a regular code line that could be used to get a value out of: | ||||
| ## - function calls | ||||
| ## - real literals (bool, string, int, float, etc.) | ||||
| ## - logical operators (>, <, >=, or, and, not, etc.) | ||||
| func _get_literal_syntax_highlighting(start_index: int, text: String) -> Dictionary: | ||||
| 	var text_edit: TextEdit = get_text_edit() | ||||
| 	var colors: Dictionary = {} | ||||
| 
 | ||||
| 	# Removing spaces at start/end of the literal | ||||
| 	var text_length: int = text.length() | ||||
| 	text = text.lstrip(" ") | ||||
| 	start_index += text_length - text.length() | ||||
| 	text = text.rstrip(" ") | ||||
| 
 | ||||
| 	# Parenthesis expression. | ||||
| 	var paren_matches: Array[RegExMatch] = regex_paren.search_all(text) | ||||
| 	for paren_match in paren_matches: | ||||
| 		colors[start_index + paren_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + paren_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + paren_match.get_start("paren"), paren_match.get_string("paren")), true) | ||||
| 		colors[start_index + paren_match.get_end(0) - 1] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 
 | ||||
| 	# Strings. | ||||
| 	var string_matches: Array[RegExMatch] = regex_string.search_all(text) | ||||
| 	for string_match in string_matches: | ||||
| 		colors[start_index + string_match.get_start(0)] = {"color": text_edit.theme_overrides.strings_color} | ||||
| 		if "content" in string_match.names: | ||||
| 			var escape_matches: Array[RegExMatch] = regex_escape.search_all(string_match.get_string("content")) | ||||
| 			for escape_match in escape_matches: | ||||
| 				colors[start_index + string_match.get_start("content") + escape_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 				colors[start_index + string_match.get_start("content") + escape_match.get_end(0)] = {"color": text_edit.theme_overrides.strings_color} | ||||
| 
 | ||||
| 	# Numbers. | ||||
| 	var number_matches: Array[RegExMatch] = regex_number.search_all(text) | ||||
| 	for number_match in number_matches: | ||||
| 		colors[start_index + number_match.get_start(0)] = {"color": text_edit.theme_overrides.numbers_color} | ||||
| 
 | ||||
| 	# Arrays. | ||||
| 	var array_matches: Array[RegExMatch] = regex_array.search_all(text) | ||||
| 	for array_match in array_matches: | ||||
| 		colors[start_index + array_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors.merge(_get_list_syntax_highlighting(start_index + array_match.get_start(1), array_match.strings[1]), true) | ||||
| 		colors[start_index + array_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 
 | ||||
| 	# Dictionaries. | ||||
| 	var dict_matches: Array[RegExMatch] = regex_dict.search_all(text) | ||||
| 	for dict_match in dict_matches: | ||||
| 		colors[start_index + dict_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors.merge(_get_list_syntax_highlighting(start_index + dict_match.get_start(1), dict_match.strings[1]), true) | ||||
| 		colors[start_index + dict_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 
 | ||||
| 	# Dictionary key: value pairs | ||||
| 	var kvdict_matches: Array[RegExMatch] = regex_kvdict.search_all(text) | ||||
| 	for kvdict_match in kvdict_matches: | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("left"), kvdict_match.get_string("left")), true) | ||||
| 		colors[start_index + kvdict_match.get_start("colon")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + kvdict_match.get_end("colon")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("right"), kvdict_match.get_string("right")), true) | ||||
| 
 | ||||
| 	# Booleans. | ||||
| 	var bool_matches: Array[RegExMatch] = regex_keyword.search_all(text) | ||||
| 	for bool_match in bool_matches: | ||||
| 		colors[start_index + bool_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 
 | ||||
| 	# Functions. | ||||
| 	var function_matches: Array[RegExMatch] = regex_function.search_all(text) | ||||
| 	for function_match in function_matches: | ||||
| 		var last_brace_index: int = text.rfind(")") | ||||
| 		colors[start_index + function_match.get_start(1)] = {"color": text_edit.theme_overrides.mutations_color} | ||||
| 		colors[start_index + function_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors.merge(_get_list_syntax_highlighting(start_index + function_match.get_end(0), text.substr(function_match.get_end(0), last_brace_index - function_match.get_end(0))), true) | ||||
| 		colors[start_index + last_brace_index] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 
 | ||||
| 	# Variables. | ||||
| 	var varname_matches: Array[RegExMatch] = regex_varname.search_all(text) | ||||
| 	for varname_match in varname_matches: | ||||
| 		colors[start_index + varname_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		if "attr" in varname_match.names: | ||||
| 			colors[start_index + varname_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color} | ||||
| 			colors[start_index + varname_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		if "key" in varname_match.names: | ||||
| 			# Braces are outside of the key, so coloring them symbols_color | ||||
| 			colors[start_index + varname_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 			colors.merge(_get_literal_syntax_highlighting(start_index + varname_match.get_start("key"), varname_match.get_string("key")), true) | ||||
| 			colors[start_index + varname_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 
 | ||||
| 	# Comparison operators. | ||||
| 	var comparison_matches: Array[RegExMatch] = regex_comparison.search_all(text) | ||||
| 	for comparison_match in comparison_matches: | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("left"), comparison_match.get_string("left")), true) | ||||
| 		colors[start_index + comparison_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color} | ||||
| 		colors[start_index + comparison_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		var right = comparison_match.get_string("right") | ||||
| 		if right.ends_with(":"): | ||||
| 			right = right.substr(0, right.length() - 1) | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("right"), right), true) | ||||
| 		colors[start_index + comparison_match.get_start("right") + right.length()] = { "color": text_edit.theme_overrides.symbols_color } | ||||
| 
 | ||||
| 	# Logical binary operators. | ||||
| 	var blogical_matches: Array[RegExMatch] = regex_blogical.search_all(text) | ||||
| 	for blogical_match in blogical_matches: | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("left"), blogical_match.get_string("left")), true) | ||||
| 		colors[start_index + blogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[start_index + blogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("right"), blogical_match.get_string("right")), true) | ||||
| 
 | ||||
| 	# Logical unary operators. | ||||
| 	var ulogical_matches: Array[RegExMatch] = regex_ulogical.search_all(text) | ||||
| 	for ulogical_match in ulogical_matches: | ||||
| 		colors[start_index + ulogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color} | ||||
| 		colors[start_index + ulogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color} | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + ulogical_match.get_start("right"), ulogical_match.get_string("right")), true) | ||||
| 
 | ||||
| 	return colors | ||||
| 
 | ||||
| 
 | ||||
| ## Returns the syntax coloring for a list of literals separated by commas | ||||
| func _get_list_syntax_highlighting(start_index: int, text: String) -> Dictionary: | ||||
| 	var text_edit: TextEdit = get_text_edit() | ||||
| 	var colors: Dictionary = {} | ||||
| 
 | ||||
| 	# Comma-separated list of literals (for arrays and function arguments) | ||||
| 	var element_matches: Array[RegExMatch] = regex_commas.search_all(text) | ||||
| 	for element_match in element_matches: | ||||
| 		colors.merge(_get_literal_syntax_highlighting(start_index + element_match.get_start(1), element_match.strings[1]), true) | ||||
| 
 | ||||
| 	return colors | ||||
|  | @ -0,0 +1,160 @@ | |||
| extends Node | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| const DialogueSettings = preload("../settings.gd") | ||||
| const DialogueManagerParseResult = preload("./parse_result.gd") | ||||
| 
 | ||||
| 
 | ||||
| # Keeps track of errors and dependencies. | ||||
| # { | ||||
| # 	<dialogue file path> = { | ||||
| # 		path = <dialogue file path>, | ||||
| # 		dependencies = [<dialogue file path>, <dialogue file path>], | ||||
| # 		errors = [<error>, <error>] | ||||
| # 	} | ||||
| # } | ||||
| var _cache: Dictionary = {} | ||||
| 
 | ||||
| var _update_dependency_timer: Timer = Timer.new() | ||||
| var _update_dependency_paths: PackedStringArray = [] | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
| 	add_child(_update_dependency_timer) | ||||
| 	_update_dependency_timer.timeout.connect(_on_update_dependency_timeout) | ||||
| 
 | ||||
| 	_build_cache() | ||||
| 
 | ||||
| 
 | ||||
| func reimport_files(files: PackedStringArray = []) -> void: | ||||
| 	if files.is_empty(): files = get_files() | ||||
| 
 | ||||
| 	var file_system: EditorFileSystem = Engine.get_meta("DialogueManagerPlugin") \ | ||||
| 		.get_editor_interface() \ | ||||
| 		.get_resource_filesystem() | ||||
| 
 | ||||
| 	# NOTE: Godot 4.2rc1 has an issue with reimporting more than one | ||||
| 	# file at a time so we do them one by one | ||||
| 	for file in files: | ||||
| 		file_system.reimport_files([file]) | ||||
| 		await get_tree().create_timer(0.2) | ||||
| 
 | ||||
| 
 | ||||
| ## Add a dialogue file to the cache. | ||||
| func add_file(path: String, parse_results: DialogueManagerParseResult = null) -> void: | ||||
| 	_cache[path] = { | ||||
| 		path = path, | ||||
| 		dependencies = [], | ||||
| 		errors = [] | ||||
| 	} | ||||
| 
 | ||||
| 	if parse_results != null: | ||||
| 		_cache[path].dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path) | ||||
| 		_cache[path].parsed_at = Time.get_ticks_msec() | ||||
| 
 | ||||
| 	# If this is a fresh cache entry then we need to check for dependencies | ||||
| 	if parse_results == null and not _update_dependency_paths.has(path): | ||||
| 		queue_updating_dependencies(path) | ||||
| 
 | ||||
| 
 | ||||
| ## Get the file paths in the cache. | ||||
| func get_files() -> PackedStringArray: | ||||
| 	return _cache.keys() | ||||
| 
 | ||||
| 
 | ||||
| ## Remember any errors in a dialogue file. | ||||
| func add_errors_to_file(path: String, errors: Array[Dictionary]) -> void: | ||||
| 	if _cache.has(path): | ||||
| 		_cache[path].errors = errors | ||||
| 	else: | ||||
| 		_cache[path] = { | ||||
| 			path = path, | ||||
| 			resource_path = "", | ||||
| 			dependencies = [], | ||||
| 			errors = errors | ||||
| 		} | ||||
| 
 | ||||
| 
 | ||||
| ## Get a list of files that have errors in them. | ||||
| func get_files_with_errors() -> Array[Dictionary]: | ||||
| 	var files_with_errors: Array[Dictionary] = [] | ||||
| 	for dialogue_file in _cache.values(): | ||||
| 		if dialogue_file and dialogue_file.errors.size() > 0: | ||||
| 			files_with_errors.append(dialogue_file) | ||||
| 	return files_with_errors | ||||
| 
 | ||||
| 
 | ||||
| ## Queue a file to have it's dependencies checked | ||||
| func queue_updating_dependencies(of_path: String) -> void: | ||||
| 	_update_dependency_timer.stop() | ||||
| 	if not _update_dependency_paths.has(of_path): | ||||
| 		_update_dependency_paths.append(of_path) | ||||
| 	_update_dependency_timer.start(0.5) | ||||
| 
 | ||||
| 
 | ||||
| ## Update any references to a file path that has moved | ||||
| func move_file_path(from_path: String, to_path: String) -> void: | ||||
| 	if not _cache.has(from_path): return | ||||
| 
 | ||||
| 	if to_path != "": | ||||
| 		_cache[to_path] = _cache[from_path].duplicate() | ||||
| 	_cache.erase(from_path) | ||||
| 
 | ||||
| 
 | ||||
| ## Get any dialogue files that import a given path. | ||||
| func get_files_with_dependency(imported_path: String) -> Array: | ||||
| 	return _cache.values().filter(func(d): return d.dependencies.has(imported_path)) | ||||
| 
 | ||||
| 
 | ||||
| ## Get any paths that are dependent on a given path | ||||
| func get_dependent_paths_for_reimport(on_path: String) -> PackedStringArray: | ||||
| 	return get_files_with_dependency(on_path) \ | ||||
| 		.filter(func(d): return Time.get_ticks_msec() - d.get("parsed_at", 0) > 3000) \ | ||||
| 		.map(func(d): return d.path) | ||||
| 
 | ||||
| 
 | ||||
| # Build the initial cache for dialogue files. | ||||
| func _build_cache() -> void: | ||||
| 	var current_files: PackedStringArray = _get_dialogue_files_in_filesystem() | ||||
| 	for file in current_files: | ||||
| 		add_file(file) | ||||
| 
 | ||||
| 
 | ||||
| # Recursively find any dialogue files in a directory | ||||
| func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray: | ||||
| 	var files: PackedStringArray = [] | ||||
| 
 | ||||
| 	if DirAccess.dir_exists_absolute(path): | ||||
| 		var dir = DirAccess.open(path) | ||||
| 		dir.list_dir_begin() | ||||
| 		var file_name = dir.get_next() | ||||
| 		while file_name != "": | ||||
| 			var file_path: String = (path + "/" + file_name).simplify_path() | ||||
| 			if dir.current_is_dir(): | ||||
| 				if not file_name in [".godot", ".tmp"]: | ||||
| 					files.append_array(_get_dialogue_files_in_filesystem(file_path)) | ||||
| 			elif file_name.get_extension() == "dialogue": | ||||
| 				files.append(file_path) | ||||
| 			file_name = dir.get_next() | ||||
| 
 | ||||
| 	return files | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
| 
 | ||||
| 
 | ||||
| func _on_update_dependency_timeout() -> void: | ||||
| 	_update_dependency_timer.stop() | ||||
| 	var import_regex: RegEx = RegEx.create_from_string("import \"(?<path>.*?)\"") | ||||
| 	var file: FileAccess | ||||
| 	var found_imports: Array[RegExMatch] | ||||
| 	for path in _update_dependency_paths: | ||||
| 		# Open the file and check for any "import" lines | ||||
| 		file = FileAccess.open(path, FileAccess.READ) | ||||
| 		found_imports = import_regex.search_all(file.get_as_text()) | ||||
| 		var dependencies: PackedStringArray = [] | ||||
| 		for found in found_imports: | ||||
| 			dependencies.append(found.strings[found.names.path]) | ||||
| 		_cache[path].dependencies = dependencies | ||||
| 	_update_dependency_paths.clear() | ||||
|  | @ -6,7 +6,7 @@ signal failed() | |||
| signal updated(updated_to_version: String) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| const TEMP_FILE_NAME = "user://temp.zip" | ||||
| 
 | ||||
|  | @ -19,14 +19,14 @@ const TEMP_FILE_NAME = "user://temp.zip" | |||
| var next_version_release: Dictionary: | ||||
| 	set(value): | ||||
| 		next_version_release = value | ||||
| 		label.text = DialogueConstants.translate("update.is_available_for_download") % value.tag_name.substr(1) | ||||
| 		label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1) | ||||
| 	get: | ||||
| 		return next_version_release | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
| 	$VBox/Center/DownloadButton.text = DialogueConstants.translate("update.download_update") | ||||
| 	$VBox/Center2/NotesButton.text = DialogueConstants.translate("update.release_notes") | ||||
| 	$VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update") | ||||
| 	$VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes") | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
|  | @ -34,28 +34,27 @@ func _ready() -> void: | |||
| 
 | ||||
| func _on_download_button_pressed() -> void: | ||||
| 	# Safeguard the actual dialogue manager repo from accidentally updating itself | ||||
| 	if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"):  | ||||
| 	if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"): | ||||
| 		prints("You can't update the addon from within itself.") | ||||
| 		failed.emit() | ||||
| 		return | ||||
| 	 | ||||
| 
 | ||||
| 	http_request.request(next_version_release.zipball_url) | ||||
| 	download_button.disabled = true | ||||
| 	download_button.text = DialogueConstants.translate("update.downloading") | ||||
| 	download_button.text = DialogueConstants.translate(&"update.downloading") | ||||
| 
 | ||||
| 
 | ||||
| func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: | ||||
| 	if result != HTTPRequest.RESULT_SUCCESS:  | ||||
| 	if result != HTTPRequest.RESULT_SUCCESS: | ||||
| 		failed.emit() | ||||
| 		return | ||||
| 	 | ||||
| 
 | ||||
| 	# Save the downloaded zip | ||||
| 	var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE) | ||||
| 	zip_file.store_buffer(body) | ||||
| 	zip_file.close() | ||||
| 	 | ||||
| 	if DirAccess.dir_exists_absolute("res://addons/dialogue_manager"): | ||||
| 		DirAccess.remove_absolute("res://addons/dialogue_manager") | ||||
| 
 | ||||
| 	OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager")) | ||||
| 
 | ||||
| 	var zip_reader: ZIPReader = ZIPReader.new() | ||||
| 	zip_reader.open(TEMP_FILE_NAME) | ||||
|  | @ -77,7 +76,7 @@ func _on_http_request_request_completed(result: int, response_code: int, headers | |||
| 
 | ||||
| 	zip_reader.close() | ||||
| 	DirAccess.remove_absolute(TEMP_FILE_NAME) | ||||
| 	 | ||||
| 
 | ||||
| 	updated.emit(next_version_release.tag_name.substr(1)) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,14 +46,14 @@ layout_mode = 2 | |||
| [node name="DownloadButton" type="Button" parent="VBox/Center"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Download and install update" | ||||
| text = "Download update" | ||||
| 
 | ||||
| [node name="Center2" type="CenterContainer" parent="VBox"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="NotesButton" type="LinkButton" parent="VBox/Center2"] | ||||
| layout_mode = 2 | ||||
| text = "Read release notes..." | ||||
| text = "Read release notes" | ||||
| 
 | ||||
| [connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"] | ||||
| [connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"] | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ extends HBoxContainer | |||
| signal error_pressed(line_number) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| @onready var error_button: Button = $ErrorButton | ||||
|  | @ -33,7 +33,7 @@ var errors: Array = []: | |||
| func _ready() -> void: | ||||
| 	apply_theme() | ||||
| 	hide() | ||||
| 	 | ||||
| 
 | ||||
| 
 | ||||
| ## Set up colors and icons | ||||
| func apply_theme() -> void: | ||||
|  | @ -57,9 +57,11 @@ func show_error() -> void: | |||
| 		hide() | ||||
| 	else: | ||||
| 		show() | ||||
| 		count_label.text = DialogueConstants.translate("n_of_n").format({ index = error_index + 1, total = errors.size() }) | ||||
| 		count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() }) | ||||
| 		var error = errors[error_index] | ||||
| 		error_button.text = DialogueConstants.translate("errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) | ||||
| 		error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) | ||||
| 		if error.has("external_error"): | ||||
| 			error_button.text += " " + DialogueConstants.get_error_message(error.external_error) | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
|  |  | |||
|  | @ -5,13 +5,16 @@ extends VBoxContainer | |||
| signal file_selected(file_path: String) | ||||
| signal file_popup_menu_requested(at_position: Vector2) | ||||
| signal file_double_clicked(file_path: String) | ||||
| signal file_middle_clicked(file_path: String) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| const MODIFIED_SUFFIX = "(*)" | ||||
| 
 | ||||
| 
 | ||||
| @export var icon: Texture2D | ||||
| 
 | ||||
| @onready var filter_edit: LineEdit = $FilterEdit | ||||
| @onready var list: ItemList = $List | ||||
| 
 | ||||
|  | @ -41,7 +44,7 @@ var filter: String: | |||
| func _ready() -> void: | ||||
| 	apply_theme() | ||||
| 
 | ||||
| 	filter_edit.placeholder_text = DialogueConstants.translate("files_list.filter") | ||||
| 	filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter") | ||||
| 
 | ||||
| 
 | ||||
| func select_file(file: String) -> void: | ||||
|  | @ -96,7 +99,8 @@ func apply_filter() -> void: | |||
| 			var nice_file = file_map[file] | ||||
| 			if file in unsaved_files: | ||||
| 				nice_file += MODIFIED_SUFFIX | ||||
| 			list.add_item(nice_file) | ||||
| 			var new_id := list.add_item(nice_file) | ||||
| 			list.set_item_icon(new_id, icon) | ||||
| 
 | ||||
| 	select_file(current_file_path) | ||||
| 
 | ||||
|  | @ -127,6 +131,11 @@ func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: | |||
| 	if mouse_button_index == MOUSE_BUTTON_RIGHT: | ||||
| 		file_popup_menu_requested.emit(at_position) | ||||
| 
 | ||||
| 	if mouse_button_index == MOUSE_BUTTON_MIDDLE: | ||||
| 		var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "") | ||||
| 		var file = file_map.find_key(item_text) | ||||
| 		file_middle_clicked.emit(file) | ||||
| 
 | ||||
| 
 | ||||
| func _on_list_item_activated(index: int) -> void: | ||||
| 	var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "") | ||||
|  |  | |||
|  | @ -1,18 +1,19 @@ | |||
| [gd_scene load_steps=4 format=3 uid="uid://dnufpcdrreva3"] | ||||
| [gd_scene load_steps=5 format=3 uid="uid://dnufpcdrreva3"] | ||||
| 
 | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] | ||||
| [ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"] | ||||
| 
 | ||||
| [sub_resource type="Image" id="Image_mirhx"] | ||||
| [sub_resource type="Image" id="Image_h3jns"] | ||||
| data = { | ||||
| "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), | ||||
| "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), | ||||
| "format": "RGBA8", | ||||
| "height": 16, | ||||
| "mipmaps": false, | ||||
| "width": 16 | ||||
| } | ||||
| 
 | ||||
| [sub_resource type="ImageTexture" id="ImageTexture_wy68i"] | ||||
| image = SubResource("Image_mirhx") | ||||
| [sub_resource type="ImageTexture" id="ImageTexture_44sbr"] | ||||
| image = SubResource("Image_h3jns") | ||||
| 
 | ||||
| [node name="FilesList" type="VBoxContainer"] | ||||
| anchors_preset = 15 | ||||
|  | @ -21,12 +22,13 @@ anchor_bottom = 1.0 | |||
| grow_horizontal = 2 | ||||
| grow_vertical = 2 | ||||
| script = ExtResource("1_cytii") | ||||
| icon = ExtResource("2_3ijx1") | ||||
| 
 | ||||
| [node name="FilterEdit" type="LineEdit" parent="."] | ||||
| layout_mode = 2 | ||||
| placeholder_text = "Filter files" | ||||
| clear_button_enabled = true | ||||
| right_icon = SubResource("ImageTexture_wy68i") | ||||
| right_icon = SubResource("ImageTexture_44sbr") | ||||
| 
 | ||||
| [node name="List" type="ItemList" parent="."] | ||||
| layout_mode = 2 | ||||
|  |  | |||
|  | @ -0,0 +1,229 @@ | |||
| @tool | ||||
| extends Control | ||||
| 
 | ||||
| signal result_selected(path: String, cursor: Vector2, length: int) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| @export var main_view: Control | ||||
| @export var code_edit: CodeEdit | ||||
| 
 | ||||
| @onready var input: LineEdit = %Input | ||||
| @onready var search_button: Button = %SearchButton | ||||
| @onready var match_case_button: CheckBox = %MatchCaseButton | ||||
| @onready var replace_toggle: CheckButton = %ReplaceToggle | ||||
| @onready var replace_container: VBoxContainer = %ReplaceContainer | ||||
| @onready var replace_input: LineEdit = %ReplaceInput | ||||
| @onready var replace_selected_button: Button = %ReplaceSelectedButton | ||||
| @onready var replace_all_button: Button = %ReplaceAllButton | ||||
| @onready var results_container: VBoxContainer = %ResultsContainer | ||||
| @onready var result_template: HBoxContainer = %ResultTemplate | ||||
| 
 | ||||
| var current_results: Dictionary = {}: | ||||
| 	set(value): | ||||
| 		current_results = value | ||||
| 		update_results_view() | ||||
| 		if current_results.size() == 0: | ||||
| 			replace_selected_button.disabled = true | ||||
| 			replace_all_button.disabled = true | ||||
| 		else: | ||||
| 			replace_selected_button.disabled = false | ||||
| 			replace_all_button.disabled = false | ||||
| 	get: | ||||
| 		return current_results | ||||
| 
 | ||||
| var selections: PackedStringArray = [] | ||||
| 
 | ||||
| 
 | ||||
| func prepare() -> void: | ||||
| 	input.grab_focus() | ||||
| 
 | ||||
| 	var template_label = result_template.get_node("Label") | ||||
| 	template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color | ||||
| 	template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font")) | ||||
| 
 | ||||
| 	replace_toggle.set_pressed_no_signal(false) | ||||
| 	replace_container.hide() | ||||
| 
 | ||||
| 	$VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find") | ||||
| 	input.placeholder_text = DialogueConstants.translate(&"search.placeholder") | ||||
| 	input.text = "" | ||||
| 	search_button.text = DialogueConstants.translate(&"search.find_all") | ||||
| 	match_case_button.text = DialogueConstants.translate(&"search.match_case") | ||||
| 	replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace") | ||||
| 	$VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with") | ||||
| 	replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder") | ||||
| 	replace_input.text = "" | ||||
| 	replace_all_button.text = DialogueConstants.translate(&"search.replace_all") | ||||
| 	replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected") | ||||
| 
 | ||||
| 	selections.clear() | ||||
| 	self.current_results = {} | ||||
| 
 | ||||
| #region helpers | ||||
| 
 | ||||
| 
 | ||||
| func update_results_view() -> void: | ||||
| 	for child in results_container.get_children(): | ||||
| 		child.queue_free() | ||||
| 
 | ||||
| 	for path in current_results.keys(): | ||||
| 		var path_label: Label = Label.new() | ||||
| 		path_label.text = path | ||||
| 		# Show open files | ||||
| 		if main_view.open_buffers.has(path): | ||||
| 			path_label.text += "(*)" | ||||
| 		results_container.add_child(path_label) | ||||
| 		for path_result in current_results.get(path): | ||||
| 			var result_item: HBoxContainer = result_template.duplicate() | ||||
| 
 | ||||
| 			var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox | ||||
| 			var key: String = get_selection_key(path, path_result) | ||||
| 			checkbox.toggled.connect(func(is_pressed): | ||||
| 				if is_pressed: | ||||
| 					if not selections.has(key): | ||||
| 						selections.append(key) | ||||
| 				else: | ||||
| 					if selections.has(key): | ||||
| 						selections.remove_at(selections.find(key)) | ||||
| 			) | ||||
| 			checkbox.set_pressed_no_signal(selections.has(key)) | ||||
| 			checkbox.visible = replace_toggle.button_pressed | ||||
| 
 | ||||
| 			var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel | ||||
| 			var colors: Dictionary = code_edit.theme_overrides | ||||
| 			var highlight: String = "" | ||||
| 			if replace_toggle.button_pressed: | ||||
| 				var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" | ||||
| 				highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]" | ||||
| 			else: | ||||
| 				highlight = "[bgcolor=" + colors.symbols_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" | ||||
| 			var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length()) | ||||
| 			result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text] | ||||
| 			result_label.gui_input.connect(func(event): | ||||
| 				if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click: | ||||
| 					result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length()) | ||||
| 			) | ||||
| 
 | ||||
| 			results_container.add_child(result_item) | ||||
| 
 | ||||
| 
 | ||||
| func find_in_files() -> Dictionary: | ||||
| 	var results: Dictionary = {} | ||||
| 
 | ||||
| 	var q: String = input.text | ||||
| 	var cache = Engine.get_meta("DialogueCache") | ||||
| 	var file: FileAccess | ||||
| 	for path in cache.get_files(): | ||||
| 		var path_results: Array = [] | ||||
| 		var lines: PackedStringArray = [] | ||||
| 
 | ||||
| 		if main_view.open_buffers.has(path): | ||||
| 			lines = main_view.open_buffers.get(path).text.split("\n") | ||||
| 		else: | ||||
| 			file = FileAccess.open(path, FileAccess.READ) | ||||
| 			lines = file.get_as_text().split("\n") | ||||
| 
 | ||||
| 		for i in range(0, lines.size()): | ||||
| 			var index: int = find_in_line(lines[i], q) | ||||
| 			while index > -1: | ||||
| 				path_results.append({ | ||||
| 					line = i, | ||||
| 					index = index, | ||||
| 					text = lines[i], | ||||
| 					matched_text = lines[i].substr(index, q.length()), | ||||
| 					query = q | ||||
| 				}) | ||||
| 				index = find_in_line(lines[i], q, index + q.length()) | ||||
| 
 | ||||
| 		if file != null and file.is_open(): | ||||
| 			file.close() | ||||
| 
 | ||||
| 		if path_results.size() > 0: | ||||
| 			results[path] = path_results | ||||
| 
 | ||||
| 	return results | ||||
| 
 | ||||
| 
 | ||||
| func get_selection_key(path: String, path_result: Dictionary) -> String: | ||||
| 	return "%s-%d-%d" % [path, path_result.line, path_result.index] | ||||
| 
 | ||||
| 
 | ||||
| func find_in_line(line: String, query: String, from_index: int = 0) -> int: | ||||
| 	if match_case_button.button_pressed: | ||||
| 		return line.find(query, from_index) | ||||
| 	else: | ||||
| 		return line.findn(query, from_index) | ||||
| 
 | ||||
| 
 | ||||
| func replace_results(only_selected: bool) -> void: | ||||
| 	var file: FileAccess | ||||
| 	var lines: PackedStringArray = [] | ||||
| 	for path in current_results: | ||||
| 		if main_view.open_buffers.has(path): | ||||
| 			lines = main_view.open_buffers.get(path).text.split("\n") | ||||
| 		else: | ||||
| 			file = FileAccess.open(path, FileAccess.READ_WRITE) | ||||
| 			lines = file.get_as_text().split("\n") | ||||
| 
 | ||||
| 		# Read the results in reverse because we're going to be modifying them as we go | ||||
| 		var path_results: Array = current_results.get(path).duplicate() | ||||
| 		path_results.reverse() | ||||
| 		for path_result in path_results: | ||||
| 			var key: String = get_selection_key(path, path_result) | ||||
| 			if not only_selected or (only_selected and selections.has(key)): | ||||
| 				lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length()) | ||||
| 
 | ||||
| 		var replaced_text: String = "\n".join(lines) | ||||
| 		if file != null and file.is_open(): | ||||
| 			file.seek(0) | ||||
| 			file.store_string(replaced_text) | ||||
| 			file.close() | ||||
| 		else: | ||||
| 			main_view.open_buffers.get(path).text = replaced_text | ||||
| 			if main_view.current_file_path == path: | ||||
| 				code_edit.text = replaced_text | ||||
| 
 | ||||
| 	current_results = find_in_files() | ||||
| 
 | ||||
| 
 | ||||
| #endregion | ||||
| 
 | ||||
| #region signals | ||||
| 
 | ||||
| 
 | ||||
| func _on_search_button_pressed() -> void: | ||||
| 	selections.clear() | ||||
| 	self.current_results = find_in_files() | ||||
| 
 | ||||
| 
 | ||||
| func _on_input_text_submitted(new_text: String) -> void: | ||||
| 	_on_search_button_pressed() | ||||
| 
 | ||||
| 
 | ||||
| func _on_replace_toggle_toggled(toggled_on: bool) -> void: | ||||
| 	replace_container.visible = toggled_on | ||||
| 	if toggled_on: | ||||
| 		replace_input.grab_focus() | ||||
| 	update_results_view() | ||||
| 
 | ||||
| 
 | ||||
| func _on_replace_input_text_changed(new_text: String) -> void: | ||||
| 	update_results_view() | ||||
| 
 | ||||
| 
 | ||||
| func _on_replace_selected_button_pressed() -> void: | ||||
| 	replace_results(true) | ||||
| 
 | ||||
| 
 | ||||
| func _on_replace_all_button_pressed() -> void: | ||||
| 	replace_results(false) | ||||
| 
 | ||||
| 
 | ||||
| func _on_match_case_button_toggled(toggled_on: bool) -> void: | ||||
| 	_on_search_button_pressed() | ||||
| 
 | ||||
| 
 | ||||
| #endregion | ||||
|  | @ -0,0 +1,139 @@ | |||
| [gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"] | ||||
| 
 | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"] | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"] | ||||
| bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137) | ||||
| corner_detail = 1 | ||||
| 
 | ||||
| [node name="FindInFiles" type="Control"] | ||||
| layout_mode = 3 | ||||
| anchors_preset = 15 | ||||
| anchor_right = 1.0 | ||||
| anchor_bottom = 1.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 2 | ||||
| size_flags_horizontal = 3 | ||||
| size_flags_vertical = 3 | ||||
| script = ExtResource("1_3xicy") | ||||
| 
 | ||||
| [node name="VBoxContainer" type="VBoxContainer" parent="."] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 15 | ||||
| anchor_right = 1.0 | ||||
| anchor_bottom = 1.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 2 | ||||
| 
 | ||||
| [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="FindContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"] | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| 
 | ||||
| [node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/FindContainer"] | ||||
| layout_mode = 2 | ||||
| text = "Find:" | ||||
| 
 | ||||
| [node name="Input" type="LineEdit" parent="VBoxContainer/HBoxContainer/FindContainer"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| clear_button_enabled = true | ||||
| 
 | ||||
| [node name="FindToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/FindContainer"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="SearchButton" type="Button" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Find all..." | ||||
| 
 | ||||
| [node name="MatchCaseButton" type="CheckBox" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Match case" | ||||
| 
 | ||||
| [node name="Control" type="Control" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| 
 | ||||
| [node name="ReplaceToggle" type="CheckButton" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Replace" | ||||
| 
 | ||||
| [node name="ReplaceContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| 
 | ||||
| [node name="ReplaceLabel" type="Label" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] | ||||
| layout_mode = 2 | ||||
| text = "Replace with:" | ||||
| 
 | ||||
| [node name="ReplaceInput" type="LineEdit" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| clear_button_enabled = true | ||||
| 
 | ||||
| [node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="ReplaceSelectedButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Replace selected" | ||||
| 
 | ||||
| [node name="ReplaceAllButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Replace all" | ||||
| 
 | ||||
| [node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/VBoxContainer"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] | ||||
| layout_mode = 2 | ||||
| size_flags_vertical = 3 | ||||
| follow_focus = true | ||||
| 
 | ||||
| [node name="ResultsContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| size_flags_vertical = 3 | ||||
| theme_override_constants/separation = 0 | ||||
| 
 | ||||
| [node name="ResultTemplate" type="HBoxContainer" parent="."] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 0 | ||||
| offset_left = 155.0 | ||||
| offset_top = -74.0 | ||||
| offset_right = 838.0 | ||||
| offset_bottom = -51.0 | ||||
| 
 | ||||
| [node name="CheckBox" type="CheckBox" parent="ResultTemplate"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="Label" type="RichTextLabel" parent="ResultTemplate"] | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| focus_mode = 2 | ||||
| theme_override_styles/focus = SubResource("StyleBoxFlat_owohg") | ||||
| bbcode_enabled = true | ||||
| text = "Result" | ||||
| fit_content = true | ||||
| scroll_active = false | ||||
| 
 | ||||
| [connection signal="text_submitted" from="VBoxContainer/HBoxContainer/FindContainer/Input" to="." method="_on_input_text_submitted"] | ||||
| [connection signal="pressed" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/SearchButton" to="." method="_on_search_button_pressed"] | ||||
| [connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/MatchCaseButton" to="." method="_on_match_case_button_toggled"] | ||||
| [connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/ReplaceToggle" to="." method="_on_replace_toggle_toggled"] | ||||
| [connection signal="text_changed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceInput" to="." method="_on_replace_input_text_changed"] | ||||
| [connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceSelectedButton" to="." method="_on_replace_selected_button_pressed"] | ||||
| [connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"] | ||||
|  | @ -1,7 +1,10 @@ | |||
| class_name DialogueManagerParseResult extends RefCounted | ||||
| 
 | ||||
| var imported_paths: PackedStringArray = [] | ||||
| var using_states: PackedStringArray = [] | ||||
| var titles: Dictionary = {} | ||||
| var character_names: PackedStringArray = [] | ||||
| var first_title: String = "" | ||||
| var lines: Dictionary = {} | ||||
| var errors: Array[Dictionary] = [] | ||||
| var raw_text: String = "" | ||||
|  |  | |||
|  | @ -3,19 +3,27 @@ | |||
| class_name DialogueManagerParser extends Object | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| const DialogueSettings = preload("../settings.gd") | ||||
| const ResolvedLineData = preload("./resolved_line_data.gd") | ||||
| const ResolvedTagData = preload("./resolved_tag_data.gd") | ||||
| const DialogueManagerParseResult = preload("./parse_result.gd") | ||||
| 
 | ||||
| 
 | ||||
| var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)") | ||||
| var USING_REGEX: RegEx = RegEx.create_from_string("using (?<state>.*)") | ||||
| var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+$") | ||||
| var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d") | ||||
| var TRANSLATION_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<tr>.*?)\\]") | ||||
| var MUTATION_REGEX: RegEx = RegEx.create_from_string("(do|set) (?<mutation>.*)") | ||||
| var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]") | ||||
| var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>do|do!|set) (?<mutation>.*)") | ||||
| var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if) (?<condition>.*)") | ||||
| var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.*)\\]") | ||||
| var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}") | ||||
| var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<jump_to_title>.*)") | ||||
| var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+") | ||||
| var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]") | ||||
| var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]") | ||||
| 
 | ||||
| var TOKEN_DEFINITIONS: Dictionary = { | ||||
| 	DialogueConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*\\("), | ||||
|  | @ -33,28 +41,30 @@ var TOKEN_DEFINITIONS: Dictionary = { | |||
| 	DialogueConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), | ||||
| 	DialogueConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), | ||||
| 	DialogueConstants.TOKEN_DOT: RegEx.create_from_string("^\\."), | ||||
| 	DialogueConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"), | ||||
| 	DialogueConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)"), | ||||
| 	DialogueConstants.TOKEN_STRING: RegEx.create_from_string("^(\".*?\"|\'.*?\')"), | ||||
| 	DialogueConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), | ||||
| 	DialogueConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or)( |$)"), | ||||
| 	DialogueConstants.TOKEN_STRING: RegEx.create_from_string("^(\".*?\"|\'.*?\')"), | ||||
| 	DialogueConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_][a-zA-Z_0-9]*"), | ||||
| 	DialogueConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*") | ||||
| 	DialogueConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"), | ||||
| 	DialogueConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"), | ||||
| 	DialogueConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)") | ||||
| } | ||||
| 
 | ||||
| var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>\\d+)? ") | ||||
| var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)? ") | ||||
| 
 | ||||
| var raw_lines: PackedStringArray = [] | ||||
| var parent_stack: Array[String] = [] | ||||
| 
 | ||||
| var parsed_lines: Dictionary = {} | ||||
| var imported_paths: PackedStringArray = [] | ||||
| var using_states: PackedStringArray = [] | ||||
| var titles: Dictionary = {} | ||||
| var character_names: PackedStringArray = [] | ||||
| var first_title: String = "" | ||||
| var errors: Array[Dictionary] = [] | ||||
| var raw_text: String = "" | ||||
| 
 | ||||
| var _imported_line_map: Array[Dictionary] = [] | ||||
| var _imported_line_map: Dictionary = {} | ||||
| var _imported_line_count: int = 0 | ||||
| 
 | ||||
| var while_loopbacks: Array[String] = [] | ||||
|  | @ -74,9 +84,9 @@ static func parse_string(string: String, path: String) -> DialogueManagerParseRe | |||
| 
 | ||||
| 
 | ||||
| ## Extract bbcode and other markers from a string | ||||
| static func extract_markers_from_string(string: String) -> Dictionary: | ||||
| static func extract_markers_from_string(string: String) -> ResolvedLineData: | ||||
| 	var parser: DialogueManagerParser = DialogueManagerParser.new() | ||||
| 	var markers: Dictionary = parser.extract_markers(string) | ||||
| 	var markers: ResolvedLineData = parser.extract_markers(string) | ||||
| 	parser.free() | ||||
| 
 | ||||
| 	return markers | ||||
|  | @ -85,25 +95,30 @@ static func extract_markers_from_string(string: String) -> Dictionary: | |||
| ## Parse some raw dialogue text. Returns a dictionary containing parse results | ||||
| func parse(text: String, path: String) -> Error: | ||||
| 	prepare(text, path) | ||||
| 	raw_text = text | ||||
| 
 | ||||
| 	# Parse all of the content | ||||
| 	var known_translations = {} | ||||
| 
 | ||||
| 	# Get list of known autoloads | ||||
| 	var autoload_names: PackedStringArray = get_autoload_names() | ||||
| 
 | ||||
| 	# Keep track of the last doc comment | ||||
| 	var doc_comments: Array[String] = [] | ||||
| 
 | ||||
| 	# Then parse all lines | ||||
| 	for id in range(0, raw_lines.size()): | ||||
| 		var raw_line: String = raw_lines[id] | ||||
| 
 | ||||
| 		var line: Dictionary = { | ||||
| 			id = str(id), | ||||
| 			next_id = DialogueConstants.ID_NULL | ||||
| 		} | ||||
| 
 | ||||
| 		# Ignore empty lines and comments | ||||
| 		if is_line_empty(raw_line): continue | ||||
| 
 | ||||
| 		# Work out if we are inside a conditional or option or if we just | ||||
| 		# indented back out of one | ||||
| 		var indent_size: int = get_indent(raw_line) | ||||
| 		if indent_size < parent_stack.size(): | ||||
| 		if indent_size < parent_stack.size() and not is_line_empty(raw_line): | ||||
| 			for _tab in range(0, parent_stack.size() - indent_size): | ||||
| 				parent_stack.pop_back() | ||||
| 
 | ||||
|  | @ -121,12 +136,33 @@ func parse(text: String, path: String) -> Error: | |||
| 			line["translation_key"] = translation_key | ||||
| 			raw_line = raw_line.replace("[ID:%s]" % translation_key, "") | ||||
| 
 | ||||
| 		## Check for each kind of line | ||||
| 		# Check for each kind of line | ||||
| 
 | ||||
| 		# Start shortcuts | ||||
| 		if raw_line.begins_with("using "): | ||||
| 			var using_match: RegExMatch = USING_REGEX.search(raw_line) | ||||
| 			if "state" in using_match.names: | ||||
| 				var using_state: String = using_match.strings[using_match.names.state].strip_edges() | ||||
| 				if not using_state in autoload_names: | ||||
| 					add_error(id, 0, DialogueConstants.ERR_UNKNOWN_USING) | ||||
| 				elif not using_state in using_states: | ||||
| 					using_states.append(using_state) | ||||
| 			continue | ||||
| 
 | ||||
| 		# Response | ||||
| 		if is_response_line(raw_line): | ||||
| 		elif is_response_line(raw_line): | ||||
| 			# Add any doc notes | ||||
| 			line["notes"] = "\n".join(doc_comments) | ||||
| 			doc_comments = [] | ||||
| 
 | ||||
| 			parent_stack.append(str(id)) | ||||
| 			line["type"] = DialogueConstants.TYPE_RESPONSE | ||||
| 
 | ||||
| 			# Extract any #tags | ||||
| 			var tag_data: ResolvedTagData = extract_tags(raw_line) | ||||
| 			line["tags"] = tag_data.tags | ||||
| 			raw_line = tag_data.line_without_tags | ||||
| 
 | ||||
| 			if " [if " in raw_line: | ||||
| 				line["condition"] = extract_condition(raw_line, true, indent_size) | ||||
| 			if " =>" in raw_line: | ||||
|  | @ -147,6 +183,8 @@ func parse(text: String, path: String) -> Error: | |||
| 				if goto_line.next_id in [DialogueConstants.ID_ERROR, DialogueConstants.ID_ERROR_INVALID_TITLE, DialogueConstants.ID_ERROR_TITLE_HAS_NO_BODY]: | ||||
| 					line["next_id"] = goto_line.next_id | ||||
| 
 | ||||
| 			line["character"] = "" | ||||
| 			line["character_replacements"] = [] as Array[Dictionary] | ||||
| 			line["text"] = extract_response_prompt(raw_line) | ||||
| 
 | ||||
| 			var previous_response_id = find_previous_response_id(id) | ||||
|  | @ -177,44 +215,35 @@ func parse(text: String, path: String) -> Error: | |||
| 
 | ||||
| 			# If this response has a character name in it then it will automatically be | ||||
| 			# injected as a line of dialogue if the player selects it | ||||
| 			var l = line.text.replace("\\:", "!ESCAPED_COLON!") | ||||
| 			if ": " in l: | ||||
| 				var first_child: Dictionary = { | ||||
| 					type = DialogueConstants.TYPE_DIALOGUE, | ||||
| 					next_id = line.next_id, | ||||
| 					next_id_after = line.next_id_after, | ||||
| 					text_replacements = line.text_replacements, | ||||
| 					translation_key = line.get("translation_key") | ||||
| 				} | ||||
| 
 | ||||
| 				var bits = Array(l.strip_edges().split(": ")) | ||||
| 				first_child["character"] = bits.pop_front() | ||||
| 				# You can use variables in the character's name | ||||
| 				first_child["character_replacements"] = extract_dialogue_replacements(first_child.character, first_child.character.length() + 2 + indent_size) | ||||
| 				for replacement in first_child.character_replacements: | ||||
| 					if replacement.has("error"): | ||||
| 						add_error(id, replacement.index, replacement.error) | ||||
| 				first_child["text"] = ": ".join(bits).replace("!ESCAPED_COLON!", ":") | ||||
| 
 | ||||
| 				line["character"] = first_child.character.strip_edges() | ||||
| 				if not line["character"] in character_names: | ||||
| 					character_names.append(line["character"]) | ||||
| 				line["text"] = first_child.text.strip_edges() | ||||
| 
 | ||||
| 				if first_child.translation_key == null: | ||||
| 					first_child["translation_key"] = first_child.text | ||||
| 
 | ||||
| 				parsed_lines[str(id) + ".2"] = first_child | ||||
| 				line["next_id"] = str(id) + ".2" | ||||
| 			var response_text: String = line.text.replace("\\:", "!ESCAPED_COLON!") | ||||
| 			if ": " in response_text: | ||||
| 				if DialogueSettings.get_setting("create_lines_for_responses_with_characters", true): | ||||
| 					var first_child: Dictionary = { | ||||
| 						type = DialogueConstants.TYPE_DIALOGUE, | ||||
| 						next_id = line.next_id, | ||||
| 						next_id_after = line.next_id_after, | ||||
| 						text_replacements = line.text_replacements, | ||||
| 						tags = line.tags, | ||||
| 						translation_key = line.get("translation_key") | ||||
| 					} | ||||
| 					parse_response_character_and_text(id, response_text, first_child, indent_size, parsed_lines) | ||||
| 					line["character"] = first_child.character | ||||
| 					line["character_replacements"] = first_child.character_replacements | ||||
| 					line["text"] = first_child.text | ||||
| 					line["translation_key"] = first_child.translation_key | ||||
| 					parsed_lines[str(id) + ".2"] = first_child | ||||
| 					line["next_id"] = str(id) + ".2" | ||||
| 				else: | ||||
| 					parse_response_character_and_text(id, response_text, line, indent_size, parsed_lines) | ||||
| 			else: | ||||
| 				line["text"] = l.replace("!ESCAPED_COLON!", ":") | ||||
| 				line["text"] = response_text.replace("!ESCAPED_COLON!", ":") | ||||
| 
 | ||||
| 		# Title | ||||
| 		elif is_title_line(raw_line): | ||||
| 			line["type"] = DialogueConstants.TYPE_TITLE | ||||
| 			if not raw_lines[id].begins_with("~"): | ||||
| 				add_error(id, indent_size + 2, DialogueConstants.ERR_NESTED_TITLE) | ||||
| 			else: | ||||
| 				line["type"] = DialogueConstants.TYPE_TITLE | ||||
| 				line["text"] = extract_title(raw_line) | ||||
| 				# Titles can't have numbers as the first letter (unless they are external titles which get replaced with hashes) | ||||
| 				if id >= _imported_line_count and BEGINS_WITH_NUMBER_REGEX.search(line.text): | ||||
|  | @ -255,6 +284,10 @@ func parse(text: String, path: String) -> Error: | |||
| 		# Goto | ||||
| 		elif is_goto_line(raw_line): | ||||
| 			line["type"] = DialogueConstants.TYPE_GOTO | ||||
| 
 | ||||
| 			if raw_line.begins_with("%"): | ||||
| 				apply_weighted_random(id, raw_line, indent_size, line) | ||||
| 
 | ||||
| 			line["next_id"] = extract_goto(raw_line) | ||||
| 			if is_goto_snippet_line(raw_line): | ||||
| 				line["is_snippet"] = true | ||||
|  | @ -262,14 +295,69 @@ func parse(text: String, path: String) -> Error: | |||
| 			else: | ||||
| 				line["is_snippet"] = false | ||||
| 
 | ||||
| 		# Dialogue | ||||
| 		# Nested dialogue | ||||
| 		elif is_nested_dialogue_line(raw_line, parsed_lines, raw_lines, indent_size): | ||||
| 			var parent_line: Dictionary = parsed_lines.values().back() | ||||
| 			var parent_indent_size: int = get_indent(raw_lines[parent_line.id.to_int()]) | ||||
| 			var should_update_translation_key: bool = parent_line.translation_key == parent_line.text | ||||
| 			var suffix: String = raw_line.strip_edges(true, false) | ||||
| 			if suffix == "": | ||||
| 				suffix = " " | ||||
| 			parent_line["text"] += "\n" + suffix | ||||
| 			parent_line["text_replacements"] = extract_dialogue_replacements(parent_line.text, parent_line.character.length() + 2 + parent_indent_size) | ||||
| 			for replacement in parent_line.text_replacements: | ||||
| 				if replacement.has("error"): | ||||
| 					add_error(id, replacement.index, replacement.error) | ||||
| 
 | ||||
| 			if should_update_translation_key: | ||||
| 				parent_line["translation_key"] = parent_line.text | ||||
| 
 | ||||
| 			parent_line["next_id"] = get_line_after_line(id, parent_indent_size, parent_line) | ||||
| 
 | ||||
| 			# Ignore this line when checking for indent errors | ||||
| 			remove_error(parent_line.id.to_int(), DialogueConstants.ERR_INVALID_INDENTATION) | ||||
| 
 | ||||
| 			var next_line = raw_lines[parent_line.next_id.to_int()] | ||||
| 			if not is_dialogue_line(next_line) and get_indent(next_line) >= indent_size: | ||||
| 				add_error(parent_line.next_id.to_int(), indent_size, DialogueConstants.ERR_INVALID_INDENTATION) | ||||
| 
 | ||||
| 			continue | ||||
| 
 | ||||
| 		elif raw_line.strip_edges().begins_with("##"): | ||||
| 			doc_comments.append(raw_line.replace("##", "").strip_edges()) | ||||
| 			continue | ||||
| 
 | ||||
| 		elif is_line_empty(raw_line) or is_import_line(raw_line): | ||||
| 			continue | ||||
| 
 | ||||
| 		# Regular dialogue | ||||
| 		else: | ||||
| 			# Remove escape character | ||||
| 			if raw_line.begins_with("\\using"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\if"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\elif"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\else"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\while"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\-"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\~"): raw_line = raw_line.substr(1) | ||||
| 			if raw_line.begins_with("\\=>"): raw_line = raw_line.substr(1) | ||||
| 
 | ||||
| 			# Add any doc notes | ||||
| 			line["notes"] = "\n".join(doc_comments) | ||||
| 			doc_comments = [] | ||||
| 
 | ||||
| 			# Work out any weighted random siblings | ||||
| 			if raw_line.begins_with("%"): | ||||
| 				apply_weighted_random(id, raw_line, indent_size, line) | ||||
| 				raw_line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line, "") | ||||
| 
 | ||||
| 			line["type"] = DialogueConstants.TYPE_DIALOGUE | ||||
| 
 | ||||
| 			# Extract any tags before we process the line | ||||
| 			var tag_data: ResolvedTagData = extract_tags(raw_line) | ||||
| 			line["tags"] = tag_data.tags | ||||
| 			raw_line = tag_data.line_without_tags | ||||
| 
 | ||||
| 			var l = raw_line.replace("\\:", "!ESCAPED_COLON!") | ||||
| 			if ": " in l: | ||||
| 				var bits = Array(l.strip_edges().split(": ")) | ||||
|  | @ -336,7 +424,12 @@ func parse(text: String, path: String) -> Error: | |||
| 				add_error(id, indent_size, DialogueConstants.ERR_INVALID_CONDITION_INDENTATION) | ||||
| 
 | ||||
| 		# Line after normal line is indented to the right | ||||
| 		elif line.type in [DialogueConstants.TYPE_TITLE, DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_MUTATION, DialogueConstants.TYPE_GOTO] and is_valid_id(line.next_id): | ||||
| 		elif line.type in [ | ||||
| 				DialogueConstants.TYPE_TITLE, | ||||
| 				DialogueConstants.TYPE_DIALOGUE, | ||||
| 				DialogueConstants.TYPE_MUTATION, | ||||
| 				DialogueConstants.TYPE_GOTO | ||||
| 			] and is_valid_id(line.next_id): | ||||
| 			var next_line = raw_lines[line.next_id.to_int()] | ||||
| 			if next_line != null and get_indent(next_line) > indent_size: | ||||
| 				add_error(id, indent_size, DialogueConstants.ERR_INVALID_INDENTATION) | ||||
|  | @ -369,10 +462,14 @@ func parse(text: String, path: String) -> Error: | |||
| 			else: | ||||
| 				line["next_id"] = line["parent_id"] | ||||
| 
 | ||||
| 
 | ||||
| 		# Done! | ||||
| 		parsed_lines[str(id)] = line | ||||
| 
 | ||||
| 	# Assume the last line ends the dialogue | ||||
| 	var last_line: Dictionary = parsed_lines.values()[parsed_lines.values().size() - 1] | ||||
| 	if last_line.next_id == "": | ||||
| 		last_line.next_id = DialogueConstants.ID_END | ||||
| 
 | ||||
| 	if errors.size() > 0: | ||||
| 		return ERR_PARSE_ERROR | ||||
| 
 | ||||
|  | @ -382,10 +479,13 @@ func parse(text: String, path: String) -> Error: | |||
| func get_data() -> DialogueManagerParseResult: | ||||
| 	var data: DialogueManagerParseResult = DialogueManagerParseResult.new() | ||||
| 	data.imported_paths = imported_paths | ||||
| 	data.using_states = using_states | ||||
| 	data.titles = titles | ||||
| 	data.character_names = character_names | ||||
| 	data.first_title = first_title | ||||
| 	data.lines = parsed_lines | ||||
| 	data.errors = errors | ||||
| 	data.raw_text = raw_text | ||||
| 	return data | ||||
| 
 | ||||
| 
 | ||||
|  | @ -393,11 +493,13 @@ func get_data() -> DialogueManagerParseResult: | |||
| func get_errors() -> Array[Dictionary]: | ||||
| 	return errors | ||||
| 
 | ||||
| 
 | ||||
| ## Prepare the parser by collecting all lines and titles | ||||
| func prepare(text: String, path: String, include_imported_titles_hashes: bool = true) -> void: | ||||
| 	using_states = [] | ||||
| 	errors = [] | ||||
| 	imported_paths = [] | ||||
| 	_imported_line_map = [] | ||||
| 	_imported_line_map = {} | ||||
| 	while_loopbacks = [] | ||||
| 	titles = {} | ||||
| 	character_names = [] | ||||
|  | @ -415,31 +517,33 @@ func prepare(text: String, path: String, include_imported_titles_hashes: bool = | |||
| 		var line = raw_lines[id] | ||||
| 		if is_import_line(line): | ||||
| 			var import_data = extract_import_path_and_name(line) | ||||
| 			var import_hash: int = import_data.path.hash() | ||||
| 			if import_data.size() > 0: | ||||
| 				# Make a map so we can refer compiled lines to where they were imported from | ||||
| 				_imported_line_map.append({ | ||||
| 					hash = import_data.path.hash(), | ||||
| 					imported_on_line_number = id, | ||||
| 					from_line = 0, | ||||
| 					to_line = 0 | ||||
| 				}) | ||||
| 
 | ||||
| 				# Keep track of titles so we can add imported ones later | ||||
| 				if str(import_data.path.hash()) in imported_titles.keys(): | ||||
| 					errors.append({ line_number = id, column_number = 0, error = DialogueConstants.ERR_FILE_ALREADY_IMPORTED }) | ||||
| 				if str(import_hash) in imported_titles.keys(): | ||||
| 					add_error(id, 0, DialogueConstants.ERR_FILE_ALREADY_IMPORTED) | ||||
| 				if import_data.prefix in imported_titles.values(): | ||||
| 					errors.append({ line_number = id, column_number = 0, error = DialogueConstants.ERR_DUPLICATE_IMPORT_NAME }) | ||||
| 				imported_titles[str(import_data.path.hash())] = import_data.prefix | ||||
| 					add_error(id, 0, DialogueConstants.ERR_DUPLICATE_IMPORT_NAME) | ||||
| 				imported_titles[str(import_hash)] = import_data.prefix | ||||
| 
 | ||||
| 				# Import the file content | ||||
| 				if not import_data.path.hash() in known_imports: | ||||
| 					var error: Error = import_content(import_data.path, import_data.prefix, known_imports) | ||||
| 				if not known_imports.has(import_hash): | ||||
| 					var error: Error = import_content(import_data.path, import_data.prefix, _imported_line_map, known_imports) | ||||
| 					if error != OK: | ||||
| 						errors.append({ line_number = id, column_number = 0, error = error }) | ||||
| 						add_error(id, 0, error) | ||||
| 
 | ||||
| 				# Make a map so we can refer compiled lines to where they were imported from | ||||
| 				if not _imported_line_map.has(import_hash): | ||||
| 					_imported_line_map[import_hash] = { | ||||
| 						hash = import_hash, | ||||
| 						imported_on_line_number = id, | ||||
| 						from_line = 0, | ||||
| 						to_line = 0 | ||||
| 					} | ||||
| 
 | ||||
| 	var imported_content: String =  "" | ||||
| 	var cummulative_line_number: int = 0 | ||||
| 	for item in _imported_line_map: | ||||
| 	for item in _imported_line_map.values(): | ||||
| 		item["from_line"] = cummulative_line_number | ||||
| 		if known_imports.has(item.hash): | ||||
| 			cummulative_line_number += known_imports[item.hash].split("\n").size() | ||||
|  | @ -479,12 +583,14 @@ func prepare(text: String, path: String, include_imported_titles_hashes: bool = | |||
| 
 | ||||
| func add_error(line_number: int, column_number: int, error: int) -> void: | ||||
| 	# See if the error was in an imported file | ||||
| 	for item in _imported_line_map: | ||||
| 	for item in _imported_line_map.values(): | ||||
| 		if line_number < item.to_line: | ||||
| 			errors.append({ | ||||
| 				line_number = item.imported_on_line_number, | ||||
| 				column_number = 0, | ||||
| 				error = DialogueConstants.ERR_ERRORS_IN_IMPORTED_FILE | ||||
| 				error = DialogueConstants.ERR_ERRORS_IN_IMPORTED_FILE, | ||||
| 				external_error = error, | ||||
| 				external_line_number = line_number | ||||
| 			}) | ||||
| 			return | ||||
| 
 | ||||
|  | @ -496,6 +602,16 @@ func add_error(line_number: int, column_number: int, error: int) -> void: | |||
| 	}) | ||||
| 
 | ||||
| 
 | ||||
| func remove_error(line_number: int, error: int) -> void: | ||||
| 	for i in range(errors.size() - 1, -1, -1): | ||||
| 		var err = errors[i] | ||||
| 		var is_native_error = err.line_number == line_number - _imported_line_count and err.error == error | ||||
| 		var is_external_error = err.get("external_line_number") == line_number and err.get("external_error") == error | ||||
| 		if is_native_error or is_external_error: | ||||
| 			errors.remove_at(i) | ||||
| 			return | ||||
| 
 | ||||
| 
 | ||||
| func is_import_line(line: String) -> bool: | ||||
| 	return line.begins_with("import ") and " as " in line | ||||
| 
 | ||||
|  | @ -518,19 +634,32 @@ func is_while_condition_line(line: String) -> bool: | |||
| 
 | ||||
| func is_mutation_line(line: String) -> bool: | ||||
| 	line = line.strip_edges(true, false) | ||||
| 	return line.begins_with("do ") or line.begins_with("set ") | ||||
| 	return line.begins_with("do ") or line.begins_with("do! ") or line.begins_with("set ") | ||||
| 
 | ||||
| 
 | ||||
| func is_goto_line(line: String) -> bool: | ||||
| 	line = line.strip_edges(true, false) | ||||
| 	line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(line, "") | ||||
| 	return line.begins_with("=> ") or line.begins_with("=>< ") | ||||
| 
 | ||||
| 
 | ||||
| func is_goto_snippet_line(line: String) -> bool: | ||||
| 	return line.strip_edges().begins_with("=>< ") | ||||
| 	line = WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(line.strip_edges(), "") | ||||
| 	return line.begins_with("=>< ") | ||||
| 
 | ||||
| 
 | ||||
| func is_nested_dialogue_line(raw_line: String, parsed_lines: Dictionary, raw_lines: PackedStringArray, indent_size: int) -> bool: | ||||
| 	if parsed_lines.values().is_empty(): return false | ||||
| 	if raw_line.strip_edges().begins_with("#"): return false | ||||
| 
 | ||||
| 	var parent_line: Dictionary = parsed_lines.values().back() | ||||
| 	if parent_line.type != DialogueConstants.TYPE_DIALOGUE: return false | ||||
| 	if get_indent(raw_lines[parent_line.id.to_int()]) >= indent_size: return false | ||||
| 	return true | ||||
| 
 | ||||
| 
 | ||||
| func is_dialogue_line(line: String) -> bool: | ||||
| 	if line == null: return false | ||||
| 	if is_response_line(line): return false | ||||
| 	if is_title_line(line): return false | ||||
| 	if is_condition_line(line, true): return false | ||||
|  | @ -577,7 +706,11 @@ func get_line_after_line(id: int, indent_size: int, line: Dictionary) -> String: | |||
| 
 | ||||
| 
 | ||||
| func get_indent(line: String) -> int: | ||||
| 	return line.count("\t", 0, line.find(line.strip_edges())) | ||||
| 	var tabs: RegExMatch = INDENT_REGEX.search(line) | ||||
| 	if tabs: | ||||
| 		return tabs.get_string().length() | ||||
| 	else: | ||||
| 		return 0 | ||||
| 
 | ||||
| 
 | ||||
| func get_next_nonempty_line_id(line_number: int) -> String: | ||||
|  | @ -611,34 +744,43 @@ func find_previous_response_id(line_number: int) -> String: | |||
| 
 | ||||
| 
 | ||||
| func apply_weighted_random(id: int, raw_line: String, indent_size: int, line: Dictionary) -> void: | ||||
| 	var weight: int = 1 | ||||
| 	var weight: float = 1 | ||||
| 	var found = WEIGHTED_RANDOM_SIBLINGS_REGEX.search(raw_line) | ||||
| 	if found and found.names.has("weight"): | ||||
| 		weight = found.strings[found.names.weight].to_int() | ||||
| 		weight = found.strings[found.names.weight].to_float() | ||||
| 
 | ||||
| 	# Look back up the list to find the first weighted random line in this group | ||||
| 	var original_random_line: Dictionary = {} | ||||
| 	for i in range(id, 0, -1): | ||||
| 		# Ignore doc comment lines | ||||
| 		if raw_lines[i].strip_edges().begins_with("##"): | ||||
| 			continue | ||||
| 		# Lines that aren't prefixed with the random token are a dead end | ||||
| 		if not raw_lines[i].strip_edges().begins_with("%") or get_indent(raw_lines[i]) != indent_size: | ||||
| 			break | ||||
| 		# Make sure we group random dialogue and random lines separately | ||||
| 		elif WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_line.strip_edges(), "").begins_with("=") != WEIGHTED_RANDOM_SIBLINGS_REGEX.sub(raw_lines[i].strip_edges(), "").begins_with("="): | ||||
| 			break | ||||
| 		# Otherwise we've found the origin | ||||
| 		elif parsed_lines.has(str(i)) and parsed_lines[str(i)].has("siblings"): | ||||
| 			original_random_line = parsed_lines[str(i)] | ||||
| 			break | ||||
| 
 | ||||
| 	# Attach it to the original random line and work out where to go after the line | ||||
| 	if original_random_line.size() > 0: | ||||
| 		original_random_line["siblings"] += [{ weight = weight, id = str(id) }] | ||||
| 		if original_random_line.type != DialogueConstants.TYPE_GOTO: | ||||
| 			# Update the next line for all siblings (not goto lines, though, they manager their | ||||
| 			# own next ID) | ||||
| 			original_random_line["next_id"] = get_line_after_line(id, indent_size, line) | ||||
| 			for sibling in original_random_line["siblings"]: | ||||
| 				if sibling.id in parsed_lines: | ||||
| 					parsed_lines[sibling.id]["next_id"] = original_random_line["next_id"] | ||||
| 		line["next_id"] = original_random_line.next_id | ||||
| 	# Or set up this line as the original | ||||
| 	else: | ||||
| 		line["siblings"] = [{ weight = weight, id = str(id) }] | ||||
| 		# Find the last weighted random line in this group | ||||
| 		for i in range(id, raw_lines.size()): | ||||
| 			if i + 1 >= raw_lines.size(): | ||||
| 				line["next_id"] = DialogueConstants.ID_END | ||||
| 				break | ||||
| 			if not raw_lines[i + 1].strip_edges().begins_with("%") or get_indent(raw_lines[i + 1]) != indent_size: | ||||
| 				line["next_id"] = get_line_after_line(i, indent_size, line) | ||||
| 				break | ||||
| 		line["next_id"] = get_line_after_line(id, indent_size, line) | ||||
| 
 | ||||
| 	if line.next_id == DialogueConstants.ID_NULL: | ||||
| 		line["next_id"] = DialogueConstants.ID_END | ||||
|  | @ -649,7 +791,6 @@ func find_next_condition_sibling(line_number: int) -> String: | |||
| 	var expected_indent = get_indent(line) | ||||
| 
 | ||||
| 	# Look down the list and find an elif or else at the same indent level | ||||
| 	var last_valid_id: int = line_number | ||||
| 	for i in range(line_number + 1, raw_lines.size()): | ||||
| 		line = raw_lines[i] | ||||
| 		if is_line_empty(line): continue | ||||
|  | @ -670,8 +811,6 @@ func find_next_condition_sibling(line_number: int) -> String: | |||
| 			elif (l.begins_with("elif ") or l.begins_with("else")): | ||||
| 				return str(i) | ||||
| 
 | ||||
| 		last_valid_id = i | ||||
| 
 | ||||
| 	return DialogueConstants.ID_NULL | ||||
| 
 | ||||
| 
 | ||||
|  | @ -709,7 +848,7 @@ func find_next_line_after_conditions(line_number: int) -> String: | |||
| 
 | ||||
| 				line_indent = get_indent(line) | ||||
| 				if line_indent < expected_indent: | ||||
| 					return parsed_lines[str(p)].next_id_after | ||||
| 					return parsed_lines[str(p)].get("next_id_after", DialogueConstants.ID_NULL) | ||||
| 
 | ||||
| 	return DialogueConstants.ID_END_CONVERSATION | ||||
| 
 | ||||
|  | @ -765,7 +904,10 @@ func find_next_line_after_responses(line_number: int) -> String: | |||
| 			elif indent < expected_indent: | ||||
| 				# ...outdented so check the previous parent | ||||
| 				var previous_parent = parent_stack[parent_stack.size() - 2] | ||||
| 				return parsed_lines[str(previous_parent)].next_id_after | ||||
| 				if parsed_lines.has(str(previous_parent)): | ||||
| 					return parsed_lines[str(previous_parent)].next_id_after | ||||
| 				else: | ||||
| 					return DialogueConstants.ID_NULL | ||||
| 
 | ||||
| 		# We're at the end of a conditional so jump back up to see what's after it | ||||
| 		elif line.begins_with("elif ") or line.begins_with("else"): | ||||
|  | @ -789,29 +931,52 @@ func find_next_line_after_responses(line_number: int) -> String: | |||
| 	return DialogueConstants.ID_END_CONVERSATION | ||||
| 
 | ||||
| 
 | ||||
| ## Get the names of any autoloads in the project | ||||
| func get_autoload_names() -> PackedStringArray: | ||||
| 	var autoloads: PackedStringArray = [] | ||||
| 
 | ||||
| 	var project = ConfigFile.new() | ||||
| 	project.load("res://project.godot") | ||||
| 	if project.has_section("autoload"): | ||||
| 		return Array(project.get_section_keys("autoload")).filter(func(key): return key != "DialogueManager") | ||||
| 
 | ||||
| 	return autoloads | ||||
| 
 | ||||
| 
 | ||||
| ## Import content from another dialogue file or return an ERR | ||||
| func import_content(path: String, prefix: String, known_imports: Dictionary) -> Error: | ||||
| func import_content(path: String, prefix: String, imported_line_map: Dictionary, known_imports: Dictionary) -> Error: | ||||
| 	if FileAccess.file_exists(path): | ||||
| 		var file = FileAccess.open(path, FileAccess.READ) | ||||
| 		var content: PackedStringArray = file.get_as_text().split("\n") | ||||
| 
 | ||||
| 		var imported_titles: Dictionary = {} | ||||
| 
 | ||||
| 		for line in content: | ||||
| 		for index in range(0, content.size()): | ||||
| 			var line = content[index] | ||||
| 			if is_import_line(line): | ||||
| 				var import = extract_import_path_and_name(line) | ||||
| 				if import.size() > 0: | ||||
| 					if not known_imports.has(import.path.hash()): | ||||
| 						# Add an empty record into the keys just so we don't end up with cyclic dependencies | ||||
| 						known_imports[import.path.hash()] = "" | ||||
| 						if import_content(import.path, import.prefix, known_imports) != OK: | ||||
| 						if import_content(import.path, import.prefix, imported_line_map, known_imports) != OK: | ||||
| 							return ERR_LINK_FAILED | ||||
| 
 | ||||
| 					if not imported_line_map.has(import.path.hash()): | ||||
| 						# Make a map so we can refer compiled lines to where they were imported from | ||||
| 						imported_line_map[import.path.hash()] = { | ||||
| 							hash = import.path.hash(), | ||||
| 							imported_on_line_number = index, | ||||
| 							from_line = 0, | ||||
| 							to_line = 0 | ||||
| 						} | ||||
| 
 | ||||
| 					imported_titles[import.prefix] = import.path.hash() | ||||
| 
 | ||||
| 		var origin_hash: int = -1 | ||||
| 		for hash in known_imports.keys(): | ||||
| 			if known_imports[hash] == ".": | ||||
| 				origin_hash = hash | ||||
| 		for hash_value in known_imports.keys(): | ||||
| 			if known_imports[hash_value] == ".": | ||||
| 				origin_hash = hash_value | ||||
| 
 | ||||
| 		# Replace any titles or jump points with references to the files they point to (event if they point to their own file) | ||||
| 		for i in range(0, content.size()): | ||||
|  | @ -851,7 +1016,7 @@ func import_content(path: String, prefix: String, known_imports: Dictionary) -> | |||
| 					content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump] | ||||
| 
 | ||||
| 		imported_paths.append(path) | ||||
| 		known_imports[path.hash()] = "# %s as %s\n%s\n=> END\n" % [path, path.hash(), "\n".join(content)] | ||||
| 		known_imports[path.hash()] = "\n".join(content) + "\n=> END\n" | ||||
| 		return OK | ||||
| 	else: | ||||
| 		return ERR_FILE_NOT_FOUND | ||||
|  | @ -897,6 +1062,23 @@ func extract_response_prompt(line: String) -> String: | |||
| 	return line.replace("\\n", "\n").strip_edges() | ||||
| 
 | ||||
| 
 | ||||
| func parse_response_character_and_text(id: int, text: String, line: Dictionary, indent_size: int, parsed_lines: Dictionary) -> void: | ||||
| 	var bits = Array(text.strip_edges().split(": ")) | ||||
| 	line["character"] = bits.pop_front().strip_edges() | ||||
| 	line["character_replacements"] = extract_dialogue_replacements(line.character, line.character.length() + 2 + indent_size) | ||||
| 	for replacement in line.character_replacements: | ||||
| 		if replacement.has("error"): | ||||
| 			add_error(id, replacement.index, replacement.error) | ||||
| 
 | ||||
| 	if not line["character"] in character_names: | ||||
| 		character_names.append(line["character"]) | ||||
| 
 | ||||
| 	line["text"] = ": ".join(bits).replace("!ESCAPED_COLON!", ":").strip_edges() | ||||
| 
 | ||||
| 	if line.get("translation_key", null) == null: | ||||
| 		line["translation_key"] = line.text | ||||
| 
 | ||||
| 
 | ||||
| func extract_mutation(line: String) -> Dictionary: | ||||
| 	var found: RegExMatch = MUTATION_REGEX.search(line) | ||||
| 
 | ||||
|  | @ -920,7 +1102,8 @@ func extract_mutation(line: String) -> Dictionary: | |||
| 			} | ||||
| 		else: | ||||
| 			return { | ||||
| 				expression = expression | ||||
| 				expression = expression, | ||||
| 				is_blocking = not "!" in found.strings[found.names.keyword] | ||||
| 			} | ||||
| 
 | ||||
| 	else: | ||||
|  | @ -1016,13 +1199,41 @@ func extract_goto(line: String) -> String: | |||
| 		return DialogueConstants.ID_ERROR | ||||
| 
 | ||||
| 
 | ||||
| func extract_markers(line: String) -> Dictionary: | ||||
| func extract_tags(line: String) -> ResolvedTagData: | ||||
| 	var resolved_tags: PackedStringArray = [] | ||||
| 	var tag_matches: Array[RegExMatch] = TAGS_REGEX.search_all(line) | ||||
| 	for tag_match in tag_matches: | ||||
| 		line = line.replace(tag_match.get_string(), "") | ||||
| 		var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",") | ||||
| 		for tag in tags: | ||||
| 			tag = tag.replace("#", "") | ||||
| 			if not tag in resolved_tags: | ||||
| 				resolved_tags.append(tag) | ||||
| 
 | ||||
| 	return ResolvedTagData.new({ | ||||
| 		tags = resolved_tags, | ||||
| 		line_without_tags = line | ||||
| 	}) | ||||
| 
 | ||||
| 
 | ||||
| func extract_markers(line: String) -> ResolvedLineData: | ||||
| 	var text: String = line | ||||
| 	var pauses: Dictionary = {} | ||||
| 	var speeds: Dictionary = {} | ||||
| 	var mutations: Array[Array] = [] | ||||
| 	var bbcodes: Array = [] | ||||
| 	var time = null | ||||
| 	var time: String = "" | ||||
| 
 | ||||
| 	# Remove any escaped brackets (ie. "\[") | ||||
| 	var escaped_open_brackets: PackedInt32Array = [] | ||||
| 	var escaped_close_brackets: PackedInt32Array = [] | ||||
| 	for i in range(0, text.length() - 1): | ||||
| 		if text.substr(i, 2) == "\\[": | ||||
| 			text = text.substr(0, i) + "!" + text.substr(i + 2) | ||||
| 			escaped_open_brackets.append(i) | ||||
| 		elif text.substr(i, 2) == "\\]": | ||||
| 			text = text.substr(0, i) + "!" + text.substr(i + 2) | ||||
| 			escaped_close_brackets.append(i) | ||||
| 
 | ||||
| 	# Extract all of the BB codes so that we know the actual text (we could do this easier with | ||||
| 	# a RichTextLabel but then we'd need to await idle_frame which is annoying) | ||||
|  | @ -1030,7 +1241,7 @@ func extract_markers(line: String) -> Dictionary: | |||
| 	var accumulaive_length_offset = 0 | ||||
| 	for position in bbcode_positions: | ||||
| 		# Ignore our own markers | ||||
| 		if position.code in ["wait", "speed", "/speed", "do", "set", "next"]: | ||||
| 		if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]: | ||||
| 			continue | ||||
| 
 | ||||
| 		bbcodes.append({ | ||||
|  | @ -1055,7 +1266,7 @@ func extract_markers(line: String) -> Dictionary: | |||
| 		var code = bbcode.code | ||||
| 		var raw_args = bbcode.raw_args | ||||
| 		var args = {} | ||||
| 		if code in ["do", "set"]: | ||||
| 		if code in ["do", "do!", "set"]: | ||||
| 			args["value"] = extract_mutation("%s %s" % [code, raw_args]) | ||||
| 		else: | ||||
| 			# Could be something like: | ||||
|  | @ -1078,7 +1289,7 @@ func extract_markers(line: String) -> Dictionary: | |||
| 				speeds[index] = args.get("value").to_float() | ||||
| 			"/speed": | ||||
| 				speeds[index] = 1.0 | ||||
| 			"do", "set": | ||||
| 			"do", "do!", "set": | ||||
| 				mutations.append([index, args.get("value")]) | ||||
| 			"next": | ||||
| 				time = args.get("value") if args.has("value") else "0" | ||||
|  | @ -1090,6 +1301,14 @@ func extract_markers(line: String) -> Dictionary: | |||
| 				bb.offset_start -= length | ||||
| 				bb.start -= length | ||||
| 
 | ||||
| 		# Find any escaped brackets after this that need moving | ||||
| 		for i in range(0, escaped_open_brackets.size()): | ||||
| 			if escaped_open_brackets[i] > bbcode.start: | ||||
| 				escaped_open_brackets[i] -= length | ||||
| 		for i in range(0, escaped_close_brackets.size()): | ||||
| 			if escaped_close_brackets[i] > bbcode.start: | ||||
| 				escaped_close_brackets[i] -= length | ||||
| 
 | ||||
| 		text = text.substr(0, index) + text.substr(index + length) | ||||
| 		next_bbcode_position = find_bbcode_positions_in_string(text, false) | ||||
| 
 | ||||
|  | @ -1097,13 +1316,19 @@ func extract_markers(line: String) -> Dictionary: | |||
| 	for bb in bbcodes: | ||||
| 		text = text.insert(bb.start, bb.bbcode) | ||||
| 
 | ||||
| 	return { | ||||
| 		"text": text, | ||||
| 		"pauses": pauses, | ||||
| 		"speeds": speeds, | ||||
| 		"mutations": mutations, | ||||
| 		"time": time | ||||
| 	} | ||||
| 	# Put the escaped brackets back in | ||||
| 	for index in escaped_open_brackets: | ||||
| 		text = text.left(index) + "[" + text.right(text.length() - index - 1) | ||||
| 	for index in escaped_close_brackets: | ||||
| 		text = text.left(index) + "]" + text.right(text.length() - index - 1) | ||||
| 
 | ||||
| 	return ResolvedLineData.new({ | ||||
| 		text = text, | ||||
| 		pauses = pauses, | ||||
| 		speeds = speeds, | ||||
| 		mutations = mutations, | ||||
| 		time = time | ||||
| 	}) | ||||
| 
 | ||||
| 
 | ||||
| func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> Array[Dictionary]: | ||||
|  | @ -1126,7 +1351,7 @@ func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> A | |||
| 			open_brace_count += 1 | ||||
| 
 | ||||
| 		else: | ||||
| 			if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/"): | ||||
| 			if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"): | ||||
| 				code += string[i] | ||||
| 			else: | ||||
| 				is_finished_code = true | ||||
|  | @ -1136,7 +1361,7 @@ func find_bbcode_positions_in_string(string: String, find_all: bool = true) -> A | |||
| 
 | ||||
| 		if string[i] == "]": | ||||
| 			open_brace_count -= 1 | ||||
| 			if open_brace_count == 0: | ||||
| 			if open_brace_count == 0 and not code in ["if", "else", "/if"]: | ||||
| 				positions.append({ | ||||
| 					bbcode = bbcode, | ||||
| 					code = code, | ||||
|  | @ -1184,7 +1409,7 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo | |||
| 		limit += 1 | ||||
| 		var token = tokens.pop_front() | ||||
| 
 | ||||
| 		var error = check_next_token(token, tokens, line_type) | ||||
| 		var error = check_next_token(token, tokens, line_type, expected_close_token) | ||||
| 		if error != OK: | ||||
| 			return [build_token_tree_error(error, token.index), tokens] | ||||
| 
 | ||||
|  | @ -1227,10 +1452,19 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo | |||
| 				if sub_tree[0].size() > 0 and sub_tree[0][0].type == DialogueConstants.TOKEN_ERROR: | ||||
| 					return [build_token_tree_error(sub_tree[0][0].value, token.index), tokens] | ||||
| 
 | ||||
| 				var t = sub_tree[0] | ||||
| 				for i in range(0, t.size() - 2): | ||||
| 					# Convert Lua style dictionaries to string keys | ||||
| 					if t[i].type == DialogueConstants.TOKEN_VARIABLE and t[i+1].type == DialogueConstants.TOKEN_ASSIGNMENT: | ||||
| 						t[i].type = DialogueConstants.TOKEN_STRING | ||||
| 						t[i+1].type = DialogueConstants.TOKEN_COLON | ||||
| 						t[i+1].erase("value") | ||||
| 
 | ||||
| 				tree.append({ | ||||
| 					type = DialogueConstants.TOKEN_DICTIONARY, | ||||
| 					value = tokens_to_dictionary(sub_tree[0]) | ||||
| 				}) | ||||
| 
 | ||||
| 				tokens = sub_tree[1] | ||||
| 
 | ||||
| 			DialogueConstants.TOKEN_BRACKET_OPEN: | ||||
|  | @ -1317,25 +1551,45 @@ func build_token_tree(tokens: Array[Dictionary], line_type: String, expected_clo | |||
| 				}) | ||||
| 
 | ||||
| 			DialogueConstants.TOKEN_NUMBER: | ||||
| 				tree.append({ | ||||
| 					type = token.type, | ||||
| 					value = token.value.to_float() if "." in token.value else token.value.to_int() | ||||
| 				}) | ||||
| 				var value = token.value.to_float() if "." in token.value else token.value.to_int() | ||||
| 				# If previous token is a number and this one is a negative number then | ||||
| 				# inject a minus operator token in between them. | ||||
| 				if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DialogueConstants.TOKEN_NUMBER: | ||||
| 					tree.append(({ | ||||
| 						type = DialogueConstants.TOKEN_OPERATOR, | ||||
| 						value = "-" | ||||
| 					})) | ||||
| 					tree.append({ | ||||
| 						type = token.type, | ||||
| 						value = -1 * value | ||||
| 					}) | ||||
| 				else: | ||||
| 					tree.append({ | ||||
| 						type = token.type, | ||||
| 						value = value | ||||
| 					}) | ||||
| 
 | ||||
| 	if expected_close_token != "": | ||||
| 		return [build_token_tree_error(DialogueConstants.ERR_MISSING_CLOSING_BRACKET, tokens[0].index), tokens] | ||||
| 		var index: int = tokens[0].index if tokens.size() > 0 else 0 | ||||
| 		return [build_token_tree_error(DialogueConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] | ||||
| 
 | ||||
| 	return [tree, tokens] | ||||
| 
 | ||||
| 
 | ||||
| func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String) -> int: | ||||
| 	var next_token_type = null | ||||
| func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error: | ||||
| 	var next_token: Dictionary = { type = null } | ||||
| 	if next_tokens.size() > 0: | ||||
| 		next_token_type = next_tokens.front().type | ||||
| 		next_token = next_tokens.front() | ||||
| 
 | ||||
| 	if token.type == DialogueConstants.TOKEN_ASSIGNMENT and line_type == DialogueConstants.TYPE_CONDITION: | ||||
| 	# Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary | ||||
| 	# then it's an unexpected assignment in a condition line. | ||||
| 	if token.type == DialogueConstants.TOKEN_ASSIGNMENT and line_type == DialogueConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token): | ||||
| 		return DialogueConstants.ERR_UNEXPECTED_ASSIGNMENT | ||||
| 
 | ||||
| 	# Special case for a negative number after this one | ||||
| 	if token.type == DialogueConstants.TOKEN_NUMBER and next_token.type == DialogueConstants.TOKEN_NUMBER and next_token.value.begins_with("-"): | ||||
| 		return OK | ||||
| 
 | ||||
| 	var expected_token_types = [] | ||||
| 	var unexpected_token_types = [] | ||||
| 	match token.type: | ||||
|  | @ -1364,6 +1618,7 @@ func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_ty | |||
| 		DialogueConstants.TOKEN_BRACE_OPEN: | ||||
| 			expected_token_types = [ | ||||
| 				DialogueConstants.TOKEN_STRING, | ||||
| 				DialogueConstants.TOKEN_VARIABLE, | ||||
| 				DialogueConstants.TOKEN_NUMBER, | ||||
| 				DialogueConstants.TOKEN_BRACE_CLOSE | ||||
| 			] | ||||
|  | @ -1443,8 +1698,8 @@ func check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_ty | |||
| 				DialogueConstants.TOKEN_BRACKET_OPEN | ||||
| 			] | ||||
| 
 | ||||
| 	if (expected_token_types.size() > 0 and not next_token_type in expected_token_types or unexpected_token_types.size() > 0 and next_token_type in unexpected_token_types): | ||||
| 		match next_token_type: | ||||
| 	if (expected_token_types.size() > 0 and not next_token.type in expected_token_types or unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types): | ||||
| 		match next_token.type: | ||||
| 			null: | ||||
| 				return DialogueConstants.ERR_UNEXPECTED_END_OF_EXPRESSION | ||||
| 
 | ||||
|  | @ -1503,7 +1758,10 @@ func tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary: | |||
| 	var dictionary = {} | ||||
| 	for i in range(0, tokens.size()): | ||||
| 		if tokens[i].type == DialogueConstants.TOKEN_COLON: | ||||
| 			dictionary[tokens[i-1]] = tokens[i+1] | ||||
| 			if tokens.size() == i + 2: | ||||
| 				dictionary[tokens[i-1]] = tokens[i+1] | ||||
| 			else: | ||||
| 				dictionary[tokens[i-1]] = { type = DialogueConstants.TOKEN_GROUP, value = tokens.slice(i+1) } | ||||
| 
 | ||||
| 	return dictionary | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,15 @@ | |||
| extends RefCounted | ||||
| 
 | ||||
| var text: String = "" | ||||
| var pauses: Dictionary = {} | ||||
| var speeds: Dictionary = {} | ||||
| var mutations: Array[Array] = [] | ||||
| var time: String = "" | ||||
| 
 | ||||
| 
 | ||||
| func _init(data: Dictionary) -> void: | ||||
| 	text = data.text | ||||
| 	pauses = data.pauses | ||||
| 	speeds = data.speeds | ||||
| 	mutations = data.mutations | ||||
| 	time = data.time | ||||
|  | @ -0,0 +1,10 @@ | |||
| extends RefCounted | ||||
| 
 | ||||
| 
 | ||||
| var tags: PackedStringArray = [] | ||||
| var line_without_tags: String = "" | ||||
| 
 | ||||
| 
 | ||||
| func _init(data: Dictionary) -> void: | ||||
| 	tags = data.tags | ||||
| 	line_without_tags = data.line_without_tags | ||||
|  | @ -6,7 +6,7 @@ signal open_requested() | |||
| signal close_requested() | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| @onready var input: LineEdit = $Search/Input | ||||
|  | @ -14,6 +14,7 @@ const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | |||
| @onready var previous_button: Button = $Search/PreviousButton | ||||
| @onready var next_button: Button = $Search/NextButton | ||||
| @onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox | ||||
| @onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton | ||||
| @onready var replace_panel: HBoxContainer = $Replace | ||||
| @onready var replace_input: LineEdit = $Replace/Input | ||||
| @onready var replace_button: Button = $Replace/ReplaceButton | ||||
|  | @ -40,32 +41,38 @@ var result_index: int = -1: | |||
| 			result_index = -1 | ||||
| 			if is_instance_valid(code_edit): | ||||
| 				code_edit.deselect() | ||||
| 		 | ||||
| 		result_label.text = DialogueConstants.translate("n_of_n").format({ index = result_index + 1, total = results.size() }) | ||||
| 
 | ||||
| 		result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() }) | ||||
| 	get: | ||||
| 		return result_index | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
| 	apply_theme() | ||||
| 	 | ||||
| 	previous_button.tooltip_text = DialogueConstants.translate("search.previous") | ||||
| 	next_button.tooltip_text = DialogueConstants.translate("search.next") | ||||
| 	match_case_button.text = DialogueConstants.translate("search.match_case") | ||||
| 	$Search/ReplaceCheckButton.text = DialogueConstants.translate("search.toggle_replace") | ||||
| 	replace_button.text = DialogueConstants.translate("search.replace") | ||||
| 	replace_all_button.text = DialogueConstants.translate("search.replace_all") | ||||
| 	$Replace/ReplaceLabel.text = DialogueConstants.translate("search.replace_with") | ||||
| 	 | ||||
| 
 | ||||
| 	input.placeholder_text = DialogueConstants.translate(&"search.placeholder") | ||||
| 	previous_button.tooltip_text = DialogueConstants.translate(&"search.previous") | ||||
| 	next_button.tooltip_text = DialogueConstants.translate(&"search.next") | ||||
| 	match_case_button.text = DialogueConstants.translate(&"search.match_case") | ||||
| 	$Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace") | ||||
| 	replace_button.text = DialogueConstants.translate(&"search.replace") | ||||
| 	replace_all_button.text = DialogueConstants.translate(&"search.replace_all") | ||||
| 	$Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with") | ||||
| 
 | ||||
| 	self.result_index = -1 | ||||
| 	 | ||||
| 
 | ||||
| 	replace_panel.hide() | ||||
| 	replace_button.disabled = true | ||||
| 	replace_all_button.disabled = true | ||||
| 	 | ||||
| 
 | ||||
| 	hide() | ||||
| 
 | ||||
| 
 | ||||
| func focus_line_edit() -> void: | ||||
| 	input.grab_focus() | ||||
| 	input.select_all() | ||||
| 
 | ||||
| 
 | ||||
| func apply_theme() -> void: | ||||
| 	if is_instance_valid(previous_button): | ||||
| 		previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons") | ||||
|  | @ -76,26 +83,26 @@ func apply_theme() -> void: | |||
| # Find text in the code | ||||
| func search(text: String = "", default_result_index: int = 0) -> void: | ||||
| 	results.clear() | ||||
| 	 | ||||
| 
 | ||||
| 	if text == "": | ||||
| 		text = input.text | ||||
| 	 | ||||
| 
 | ||||
| 	var lines = code_edit.text.split("\n") | ||||
| 	for line_number in range(0, lines.size()): | ||||
| 		var line = lines[line_number] | ||||
| 		 | ||||
| 
 | ||||
| 		var column = find_in_line(line, text, 0) | ||||
| 		while column > -1: | ||||
| 			results.append([line_number, column, text.length()]) | ||||
| 			column = find_in_line(line, text, column + 1) | ||||
| 	 | ||||
| 
 | ||||
| 	if results.size() > 0: | ||||
| 		replace_button.disabled = false | ||||
| 		replace_all_button.disabled = false | ||||
| 	else: | ||||
| 		replace_button.disabled = true | ||||
| 		replace_all_button.disabled = true | ||||
| 	 | ||||
| 
 | ||||
| 	self.result_index = clamp(default_result_index, 0, results.size() - 1) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -111,8 +118,13 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int: | |||
| 
 | ||||
| 
 | ||||
| func _on_text_edit_gui_input(event: InputEvent) -> void: | ||||
| 	if event is InputEventKey and event.is_pressed() and event.as_text() == "Ctrl+F": | ||||
| 		emit_signal("open_requested") | ||||
| 	if event is InputEventKey and event.is_pressed(): | ||||
| 		match event.as_text(): | ||||
| 			"Ctrl+F", "Command+F": | ||||
| 				open_requested.emit() | ||||
| 			"Ctrl+Shift+R", "Command+Shift+R": | ||||
| 				replace_check_button.set_pressed(true) | ||||
| 				open_requested.emit() | ||||
| 
 | ||||
| 
 | ||||
| func _on_text_edit_text_changed() -> void: | ||||
|  | @ -160,7 +172,7 @@ func _on_input_gui_input(event: InputEvent) -> void: | |||
| 
 | ||||
| func _on_replace_button_pressed() -> void: | ||||
| 	if result_index == -1: return | ||||
| 	 | ||||
| 
 | ||||
| 	# Replace the selection at result index | ||||
| 	var r: Array = results[result_index] | ||||
| 	var lines: PackedStringArray = code_edit.text.split("\n") | ||||
|  | @ -169,6 +181,7 @@ func _on_replace_button_pressed() -> void: | |||
| 	lines[r[0]] = line | ||||
| 	code_edit.text = "\n".join(lines) | ||||
| 	search(input.text, result_index) | ||||
| 	code_edit.text_changed.emit() | ||||
| 
 | ||||
| 
 | ||||
| func _on_replace_all_button_pressed() -> void: | ||||
|  | @ -177,6 +190,7 @@ func _on_replace_all_button_pressed() -> void: | |||
| 	else: | ||||
| 		code_edit.text = code_edit.text.replacen(input.text, replace_input.text) | ||||
| 	search() | ||||
| 	code_edit.text_changed.emit() | ||||
| 
 | ||||
| 
 | ||||
| func _on_replace_check_button_toggled(button_pressed: bool) -> void: | ||||
|  |  | |||
|  | @ -1,20 +1,9 @@ | |||
| [gd_scene load_steps=4 format=3 uid="uid://gr8nakpbrhby"] | ||||
| [gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"] | ||||
| 
 | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"] | ||||
| 
 | ||||
| [sub_resource type="Image" id="Image_mirhx"] | ||||
| data = { | ||||
| "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), | ||||
| "format": "RGBA8", | ||||
| "height": 16, | ||||
| "mipmaps": false, | ||||
| "width": 16 | ||||
| } | ||||
| 
 | ||||
| [sub_resource type="ImageTexture" id="ImageTexture_wy68i"] | ||||
| image = SubResource("Image_mirhx") | ||||
| 
 | ||||
| [node name="SearchAndReplace" type="VBoxContainer"] | ||||
| visible = false | ||||
| anchors_preset = 10 | ||||
| anchor_right = 1.0 | ||||
| offset_bottom = 31.0 | ||||
|  | @ -28,6 +17,7 @@ layout_mode = 2 | |||
| [node name="Input" type="LineEdit" parent="Search"] | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| placeholder_text = "Text to search for" | ||||
| metadata/_edit_use_custom_anchors = true | ||||
| 
 | ||||
| [node name="MatchCaseCheckBox" type="CheckBox" parent="Search"] | ||||
|  | @ -39,7 +29,7 @@ layout_mode = 2 | |||
| 
 | ||||
| [node name="PreviousButton" type="Button" parent="Search"] | ||||
| layout_mode = 2 | ||||
| icon = SubResource("ImageTexture_wy68i") | ||||
| tooltip_text = "Previous" | ||||
| flat = true | ||||
| 
 | ||||
| [node name="ResultLabel" type="Label" parent="Search"] | ||||
|  | @ -48,7 +38,7 @@ text = "0 of 0" | |||
| 
 | ||||
| [node name="NextButton" type="Button" parent="Search"] | ||||
| layout_mode = 2 | ||||
| icon = SubResource("ImageTexture_wy68i") | ||||
| tooltip_text = "Next" | ||||
| flat = true | ||||
| 
 | ||||
| [node name="VSeparator2" type="VSeparator" parent="Search"] | ||||
|  | @ -79,7 +69,7 @@ flat = true | |||
| [node name="ReplaceAllButton" type="Button" parent="Replace"] | ||||
| layout_mode = 2 | ||||
| disabled = true | ||||
| text = "Replace All" | ||||
| text = "Replace all" | ||||
| flat = true | ||||
| 
 | ||||
| [connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"] | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ extends VBoxContainer | |||
| signal title_selected(title: String) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| @onready var filter_edit: LineEdit = $FilterEdit | ||||
|  | @ -27,8 +27,8 @@ var filter: String: | |||
| 
 | ||||
| func _ready() -> void: | ||||
| 	apply_theme() | ||||
| 	 | ||||
| 	filter_edit.placeholder_text = DialogueConstants.translate("titles_list.filter") | ||||
| 
 | ||||
| 	filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter") | ||||
| 
 | ||||
| 
 | ||||
| func select_title(title: String) -> void: | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| @tool | ||||
| extends Button | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| const DialogueSettings = preload("../settings.gd") | ||||
| 
 | ||||
| const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases" | ||||
| const LOCAL_CONFIG_PATH = "res://addons/dialogue_manager/plugin.cfg" | ||||
| 
 | ||||
| 
 | ||||
| @onready var http_request: HTTPRequest = $HTTPRequest | ||||
|  | @ -34,13 +34,6 @@ func _ready() -> void: | |||
| 	timer.start(60 * 60 * 12) | ||||
| 
 | ||||
| 
 | ||||
| # Get the current version | ||||
| func get_version() -> String: | ||||
| 	var config: ConfigFile = ConfigFile.new() | ||||
| 	config.load(LOCAL_CONFIG_PATH) | ||||
| 	return config.get_value("plugin", "version") | ||||
| 
 | ||||
| 
 | ||||
| # Convert a version number to an actually comparable number | ||||
| func version_to_number(version: String) -> int: | ||||
| 	var bits = version.split(".") | ||||
|  | @ -63,7 +56,8 @@ func apply_theme() -> void: | |||
| 
 | ||||
| 
 | ||||
| func check_for_update() -> void: | ||||
| 	http_request.request(REMOTE_RELEASES_URL) | ||||
| 	if DialogueSettings.get_user_value("check_for_updates", true): | ||||
| 		http_request.request(REMOTE_RELEASES_URL) | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
|  | @ -72,7 +66,7 @@ func check_for_update() -> void: | |||
| func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: | ||||
| 	if result != HTTPRequest.RESULT_SUCCESS: return | ||||
| 
 | ||||
| 	var current_version: String = get_version() | ||||
| 	var current_version: String = editor_plugin.get_version() | ||||
| 
 | ||||
| 	# Work out the next version from the releases information on GitHub | ||||
| 	var response = JSON.parse_string(body.get_string_from_utf8()) | ||||
|  | @ -85,7 +79,7 @@ func _on_http_request_request_completed(result: int, response_code: int, headers | |||
| 	) | ||||
| 	if versions.size() > 0: | ||||
| 		download_update_panel.next_version_release = versions[0] | ||||
| 		text = DialogueConstants.translate("update.available").format({ version = versions[0].tag_name.substr(1) }) | ||||
| 		text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) }) | ||||
| 		show() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -107,19 +101,19 @@ func _on_download_dialog_close_requested() -> void: | |||
| func _on_download_update_panel_updated(updated_to_version: String) -> void: | ||||
| 	download_dialog.hide() | ||||
| 
 | ||||
| 	needs_reload_dialog.dialog_text = DialogueConstants.translate("update.needs_reload") | ||||
| 	needs_reload_dialog.ok_button_text = DialogueConstants.translate("update.reload_ok_button") | ||||
| 	needs_reload_dialog.cancel_button_text = DialogueConstants.translate("update.reload_cancel_button") | ||||
| 	needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload") | ||||
| 	needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button") | ||||
| 	needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button") | ||||
| 	needs_reload_dialog.popup_centered() | ||||
| 
 | ||||
| 	needs_reload = true | ||||
| 	text = DialogueConstants.translate("update.reload_project") | ||||
| 	text = DialogueConstants.translate(&"update.reload_project") | ||||
| 	apply_theme() | ||||
| 
 | ||||
| 
 | ||||
| func _on_download_update_panel_failed() -> void: | ||||
| 	download_dialog.hide() | ||||
| 	update_failed_dialog.dialog_text = DialogueConstants.translate("update.failed") | ||||
| 	update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed") | ||||
| 	update_failed_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,56 +6,56 @@ const CACHE_PATH = "user://dialogue_manager_cache.json" | |||
| 
 | ||||
| # Token types | ||||
| 
 | ||||
| const TOKEN_FUNCTION = "function" | ||||
| const TOKEN_DICTIONARY_REFERENCE = "dictionary_reference" | ||||
| const TOKEN_DICTIONARY_NESTED_REFERENCE = "dictionary_nested_reference" | ||||
| const TOKEN_GROUP = "group" | ||||
| const TOKEN_ARRAY = "array" | ||||
| const TOKEN_DICTIONARY = "dictionary" | ||||
| const TOKEN_PARENS_OPEN = "parens_open" | ||||
| const TOKEN_PARENS_CLOSE = "parens_close" | ||||
| const TOKEN_BRACKET_OPEN = "bracket_open" | ||||
| const TOKEN_BRACKET_CLOSE = "bracket_close" | ||||
| const TOKEN_BRACE_OPEN = "brace_open" | ||||
| const TOKEN_BRACE_CLOSE = "brace_close" | ||||
| const TOKEN_COLON = "colon" | ||||
| const TOKEN_COMPARISON = "comparison" | ||||
| const TOKEN_ASSIGNMENT = "assignment" | ||||
| const TOKEN_OPERATOR = "operator" | ||||
| const TOKEN_COMMA = "comma" | ||||
| const TOKEN_DOT = "dot" | ||||
| const TOKEN_CONDITION = "condition" | ||||
| const TOKEN_BOOL = "bool" | ||||
| const TOKEN_NOT = "not" | ||||
| const TOKEN_AND_OR = "and_or" | ||||
| const TOKEN_STRING = "string" | ||||
| const TOKEN_NUMBER = "number" | ||||
| const TOKEN_VARIABLE = "variable" | ||||
| const TOKEN_COMMENT = "comment" | ||||
| const TOKEN_FUNCTION = &"function" | ||||
| const TOKEN_DICTIONARY_REFERENCE = &"dictionary_reference" | ||||
| const TOKEN_DICTIONARY_NESTED_REFERENCE = &"dictionary_nested_reference" | ||||
| const TOKEN_GROUP = &"group" | ||||
| const TOKEN_ARRAY = &"array" | ||||
| const TOKEN_DICTIONARY = &"dictionary" | ||||
| const TOKEN_PARENS_OPEN = &"parens_open" | ||||
| const TOKEN_PARENS_CLOSE = &"parens_close" | ||||
| const TOKEN_BRACKET_OPEN = &"bracket_open" | ||||
| const TOKEN_BRACKET_CLOSE = &"bracket_close" | ||||
| const TOKEN_BRACE_OPEN = &"brace_open" | ||||
| const TOKEN_BRACE_CLOSE = &"brace_close" | ||||
| const TOKEN_COLON = &"colon" | ||||
| const TOKEN_COMPARISON = &"comparison" | ||||
| const TOKEN_ASSIGNMENT = &"assignment" | ||||
| const TOKEN_OPERATOR = &"operator" | ||||
| const TOKEN_COMMA = &"comma" | ||||
| const TOKEN_DOT = &"dot" | ||||
| const TOKEN_CONDITION = &"condition" | ||||
| const TOKEN_BOOL = &"bool" | ||||
| const TOKEN_NOT = &"not" | ||||
| const TOKEN_AND_OR = &"and_or" | ||||
| const TOKEN_STRING = &"string" | ||||
| const TOKEN_NUMBER = &"number" | ||||
| const TOKEN_VARIABLE = &"variable" | ||||
| const TOKEN_COMMENT = &"comment" | ||||
| 
 | ||||
| const TOKEN_ERROR = "error" | ||||
| const TOKEN_ERROR = &"error" | ||||
| 
 | ||||
| # Line types | ||||
| 
 | ||||
| const TYPE_UNKNOWN = "unknown" | ||||
| const TYPE_RESPONSE = "response" | ||||
| const TYPE_TITLE = "title" | ||||
| const TYPE_CONDITION = "condition" | ||||
| const TYPE_MUTATION = "mutation" | ||||
| const TYPE_GOTO = "goto" | ||||
| const TYPE_DIALOGUE = "dialogue" | ||||
| const TYPE_ERROR = "error" | ||||
| const TYPE_UNKNOWN = &"unknown" | ||||
| const TYPE_RESPONSE = &"response" | ||||
| const TYPE_TITLE = &"title" | ||||
| const TYPE_CONDITION = &"condition" | ||||
| const TYPE_MUTATION = &"mutation" | ||||
| const TYPE_GOTO = &"goto" | ||||
| const TYPE_DIALOGUE = &"dialogue" | ||||
| const TYPE_ERROR = &"error" | ||||
| 
 | ||||
| const TYPE_ELSE = "else" | ||||
| const TYPE_ELSE = &"else" | ||||
| 
 | ||||
| # Line IDs | ||||
| 
 | ||||
| const ID_NULL = "" | ||||
| const ID_ERROR = "error" | ||||
| const ID_ERROR_INVALID_TITLE = "invalid title" | ||||
| const ID_ERROR_TITLE_HAS_NO_BODY = "title has no body" | ||||
| const ID_END = "end" | ||||
| const ID_END_CONVERSATION = "end!" | ||||
| const ID_NULL = &"" | ||||
| const ID_ERROR = &"error" | ||||
| const ID_ERROR_INVALID_TITLE = &"invalid title" | ||||
| const ID_ERROR_TITLE_HAS_NO_BODY = &"title has no body" | ||||
| const ID_END = &"end" | ||||
| const ID_END_CONVERSATION = &"end!" | ||||
| 
 | ||||
| # Errors | ||||
| 
 | ||||
|  | @ -94,88 +94,94 @@ const ERR_UNEXPECTED_NUMBER = 131 | |||
| const ERR_UNEXPECTED_VARIABLE = 132 | ||||
| const ERR_INVALID_INDEX = 133 | ||||
| const ERR_UNEXPECTED_ASSIGNMENT = 134 | ||||
| const ERR_UNKNOWN_USING = 135 | ||||
| 
 | ||||
| 
 | ||||
| ## Get the error message | ||||
| static func get_error_message(error: int) -> String: | ||||
| 	match error: | ||||
| 		ERR_ERRORS_IN_IMPORTED_FILE: | ||||
| 			return translate("errors.import_errors") | ||||
| 			return translate(&"errors.import_errors") | ||||
| 		ERR_FILE_ALREADY_IMPORTED: | ||||
| 			return translate("errors.already_imported") | ||||
| 			return translate(&"errors.already_imported") | ||||
| 		ERR_DUPLICATE_IMPORT_NAME: | ||||
| 			return translate("errors.duplicate_import") | ||||
| 			return translate(&"errors.duplicate_import") | ||||
| 		ERR_EMPTY_TITLE: | ||||
| 			return translate("errors.empty_title") | ||||
| 			return translate(&"errors.empty_title") | ||||
| 		ERR_DUPLICATE_TITLE: | ||||
| 			return translate("errors.duplicate_title") | ||||
| 			return translate(&"errors.duplicate_title") | ||||
| 		ERR_NESTED_TITLE: | ||||
| 			return translate("errors.nested_title") | ||||
| 			return translate(&"errors.nested_title") | ||||
| 		ERR_TITLE_INVALID_CHARACTERS: | ||||
| 			return translate("errors.invalid_title_string") | ||||
| 			return translate(&"errors.invalid_title_string") | ||||
| 		ERR_TITLE_BEGINS_WITH_NUMBER: | ||||
| 			return translate("errors.invalid_title_number") | ||||
| 			return translate(&"errors.invalid_title_number") | ||||
| 		ERR_UNKNOWN_TITLE: | ||||
| 			return translate("errors.unknown_title") | ||||
| 			return translate(&"errors.unknown_title") | ||||
| 		ERR_INVALID_TITLE_REFERENCE: | ||||
| 			return translate("errors.jump_to_invalid_title") | ||||
| 			return translate(&"errors.jump_to_invalid_title") | ||||
| 		ERR_TITLE_REFERENCE_HAS_NO_CONTENT: | ||||
| 			return translate("errors.title_has_no_content") | ||||
| 			return translate(&"errors.title_has_no_content") | ||||
| 		ERR_INVALID_EXPRESSION: | ||||
| 			return translate("errors.invalid_expression") | ||||
| 			return translate(&"errors.invalid_expression") | ||||
| 		ERR_UNEXPECTED_CONDITION: | ||||
| 			return translate("errors.unexpected_condition") | ||||
| 			return translate(&"errors.unexpected_condition") | ||||
| 		ERR_DUPLICATE_ID: | ||||
| 			return translate("errors.duplicate_id") | ||||
| 			return translate(&"errors.duplicate_id") | ||||
| 		ERR_MISSING_ID: | ||||
| 			return translate("errors.missing_id") | ||||
| 			return translate(&"errors.missing_id") | ||||
| 		ERR_INVALID_INDENTATION: | ||||
| 			return translate("errors.invalid_indentation") | ||||
| 			return translate(&"errors.invalid_indentation") | ||||
| 		ERR_INVALID_CONDITION_INDENTATION: | ||||
| 			return translate("errors.condition_has_no_content") | ||||
| 			return translate(&"errors.condition_has_no_content") | ||||
| 		ERR_INCOMPLETE_EXPRESSION: | ||||
| 			return translate("errors.incomplete_expression") | ||||
| 			return translate(&"errors.incomplete_expression") | ||||
| 		ERR_INVALID_EXPRESSION_FOR_VALUE: | ||||
| 			return translate("errors.invalid_expression_for_value") | ||||
| 			return translate(&"errors.invalid_expression_for_value") | ||||
| 		ERR_FILE_NOT_FOUND: | ||||
| 			return translate("errors.file_not_found") | ||||
| 			return translate(&"errors.file_not_found") | ||||
| 		ERR_UNEXPECTED_END_OF_EXPRESSION: | ||||
| 			return translate("errors.unexpected_end_of_expression") | ||||
| 			return translate(&"errors.unexpected_end_of_expression") | ||||
| 		ERR_UNEXPECTED_FUNCTION: | ||||
| 			return translate("errors.unexpected_function") | ||||
| 			return translate(&"errors.unexpected_function") | ||||
| 		ERR_UNEXPECTED_BRACKET: | ||||
| 			return translate("errors.unexpected_bracket") | ||||
| 			return translate(&"errors.unexpected_bracket") | ||||
| 		ERR_UNEXPECTED_CLOSING_BRACKET: | ||||
| 			return translate("errors.unexpected_closing_bracket") | ||||
| 			return translate(&"errors.unexpected_closing_bracket") | ||||
| 		ERR_MISSING_CLOSING_BRACKET: | ||||
| 			return translate("errors.missing_closing_bracket") | ||||
| 			return translate(&"errors.missing_closing_bracket") | ||||
| 		ERR_UNEXPECTED_OPERATOR: | ||||
| 			return translate("errors.unexpected_operator") | ||||
| 			return translate(&"errors.unexpected_operator") | ||||
| 		ERR_UNEXPECTED_COMMA: | ||||
| 			return translate("errors.unexpected_comma") | ||||
| 			return translate(&"errors.unexpected_comma") | ||||
| 		ERR_UNEXPECTED_COLON: | ||||
| 			return translate("errors.unexpected_colon") | ||||
| 			return translate(&"errors.unexpected_colon") | ||||
| 		ERR_UNEXPECTED_DOT: | ||||
| 			return translate("errors.unexpected_dot") | ||||
| 			return translate(&"errors.unexpected_dot") | ||||
| 		ERR_UNEXPECTED_BOOLEAN: | ||||
| 			return translate("errors.unexpected_boolean") | ||||
| 			return translate(&"errors.unexpected_boolean") | ||||
| 		ERR_UNEXPECTED_STRING: | ||||
| 			return translate("errors.unexpected_string") | ||||
| 			return translate(&"errors.unexpected_string") | ||||
| 		ERR_UNEXPECTED_NUMBER: | ||||
| 			return translate("errors.unexpected_number") | ||||
| 			return translate(&"errors.unexpected_number") | ||||
| 		ERR_UNEXPECTED_VARIABLE: | ||||
| 			return translate("errors.unexpected_variable") | ||||
| 			return translate(&"errors.unexpected_variable") | ||||
| 		ERR_INVALID_INDEX: | ||||
| 			return translate("errors.invalid_index") | ||||
| 			return translate(&"errors.invalid_index") | ||||
| 		ERR_UNEXPECTED_ASSIGNMENT: | ||||
| 			return translate("errors.unexpected_assignment") | ||||
| 			return translate(&"errors.unexpected_assignment") | ||||
| 		ERR_UNKNOWN_USING: | ||||
| 			return translate(&"errors.unknown_using") | ||||
| 
 | ||||
| 	return translate("errors.unknown") | ||||
| 	return translate(&"errors.unknown") | ||||
| 
 | ||||
| 
 | ||||
| static func translate(string: String) -> String: | ||||
| 	var language: String = TranslationServer.get_tool_locale().substr(0, 2) | ||||
| 	var translations_path: String = "res://addons/dialogue_manager/l10n/%s.po" % language | ||||
| 	var fallback_translations_path: String = "res://addons/dialogue_manager/l10n/en.po" | ||||
| 	var translations: Translation = load(translations_path if FileAccess.file_exists(translations_path) else fallback_translations_path) | ||||
| 	var base_path = new().get_script().resource_path.get_base_dir() | ||||
| 
 | ||||
| 	var language: String = TranslationServer.get_tool_locale() | ||||
| 	var translations_path: String = "%s/l10n/%s.po" % [base_path, language] | ||||
| 	var fallback_translations_path: String = "%s/l10n/%s.po" % [base_path, TranslationServer.get_tool_locale().substr(0, 2)] | ||||
| 	var en_translations_path: String = "%s/l10n/en.po" % base_path | ||||
| 	var translations: Translation = load(translations_path if FileAccess.file_exists(translations_path) else (fallback_translations_path if FileAccess.file_exists(fallback_translations_path) else en_translations_path)) | ||||
| 	return translations.get_message(string) | ||||
|  |  | |||
|  | @ -1,21 +1,46 @@ | |||
| extends RichTextLabel | ||||
| @icon("./assets/icon.svg") | ||||
| 
 | ||||
| @tool | ||||
| 
 | ||||
| ## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue. | ||||
| class_name DialogueLabel extends RichTextLabel | ||||
| 
 | ||||
| 
 | ||||
| ## Emitted for each letter typed out. | ||||
| signal spoke(letter: String, letter_index: int, speed: float) | ||||
| 
 | ||||
| ## Emitted when typing paused for a `[wait]` | ||||
| signal paused_typing(duration: float) | ||||
| 
 | ||||
| ## Emitted when the player skips the typing of dialogue. | ||||
| signal skipped_typing() | ||||
| 
 | ||||
| ## Emitted when typing finishes. | ||||
| signal finished_typing() | ||||
| 
 | ||||
| 
 | ||||
| ## The action to press to skip typing | ||||
| @export var skip_action: String = "ui_cancel" | ||||
| # The action to press to skip typing. | ||||
| @export var skip_action: StringName = &"ui_cancel" | ||||
| 
 | ||||
| ## The speed with which the text types out | ||||
| ## The speed with which the text types out. | ||||
| @export var seconds_per_step: float = 0.02 | ||||
| 
 | ||||
| ## Automatically have a brief pause when these characters are encountered | ||||
| ## Automatically have a brief pause when these characters are encountered. | ||||
| @export var pause_at_characters: String = ".?!" | ||||
| 
 | ||||
| ## Don't auto pause if the charcter after the pause is one of these. | ||||
| @export var skip_pause_at_character_if_followed_by: String = ")\"" | ||||
| 
 | ||||
| ## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br] | ||||
| ## Abbreviations are limitted to 5 characters in length [br] | ||||
| ## Does not support multi-period abbreviations (ex. "p.m.") | ||||
| @export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"] | ||||
| 
 | ||||
| ## The amount of time to pause when exposing a character present in pause_at_characters. | ||||
| @export var seconds_per_pause_step: float = 0.3 | ||||
| 
 | ||||
| 
 | ||||
| ## The current line of dialogue. | ||||
| var dialogue_line: | ||||
| 	set(next_dialogue_line): | ||||
| 		dialogue_line = next_dialogue_line | ||||
|  | @ -24,102 +49,119 @@ var dialogue_line: | |||
| 	get: | ||||
| 		return dialogue_line | ||||
| 
 | ||||
| var last_wait_index: int = -1 | ||||
| var last_mutation_index: int = -1 | ||||
| var waiting_seconds: float = 0 | ||||
| 
 | ||||
| ## Whether the label is currently typing itself out. | ||||
| var is_typing: bool = false: | ||||
| 	set(value): | ||||
| 		if is_typing != value and value == false: | ||||
| 			finished_typing.emit() | ||||
| 		var is_finished: bool = is_typing != value and value == false | ||||
| 		is_typing = value | ||||
| 		if is_finished: | ||||
| 			finished_typing.emit() | ||||
| 	get: | ||||
| 		return is_typing | ||||
| 
 | ||||
| var _last_wait_index: int = -1 | ||||
| var _last_mutation_index: int = -1 | ||||
| var _waiting_seconds: float = 0 | ||||
| var _is_awaiting_mutation: bool = false | ||||
| 
 | ||||
| 
 | ||||
| func _process(delta: float) -> void: | ||||
| 	if self.is_typing: | ||||
| 		# Type out text | ||||
| 		if visible_ratio < 1: | ||||
| 			# See if we are waiting | ||||
| 			if waiting_seconds > 0: | ||||
| 				waiting_seconds = waiting_seconds - delta | ||||
| 			if _waiting_seconds > 0: | ||||
| 				_waiting_seconds = _waiting_seconds - delta | ||||
| 			# If we are no longer waiting then keep typing | ||||
| 			if waiting_seconds <= 0: | ||||
| 				type_next(delta, waiting_seconds) | ||||
| 			if _waiting_seconds <= 0: | ||||
| 				_type_next(delta, _waiting_seconds) | ||||
| 		else: | ||||
| 			# Make sure any mutations at the end of the line get run | ||||
| 			_mutate_inline_mutations(get_total_character_count()) | ||||
| 			self.is_typing = false | ||||
| 
 | ||||
| 
 | ||||
| func _unhandled_input(event: InputEvent) -> void: | ||||
| 	if self.is_typing and visible_ratio < 1 and event.is_action_pressed(skip_action): | ||||
| 		# Run any inline mutations that haven't been run yet | ||||
| 		for i in range(visible_characters, get_total_character_count()): | ||||
| 			mutate_inline_mutations(i) | ||||
| 		visible_characters = get_total_character_count() | ||||
| 		self.is_typing = false | ||||
| 		finished_typing.emit() | ||||
| 	# Note: this will no longer be reached if using Dialogue Manager > 2.32.2. To make skip handling | ||||
| 	# simpler (so all of mouse/keyboard/joypad are together) it is now the responsibility of the | ||||
| 	# dialogue balloon. | ||||
| 	if self.is_typing and visible_ratio < 1 and InputMap.has_action(skip_action) and event.is_action_pressed(skip_action): | ||||
| 		get_viewport().set_input_as_handled() | ||||
| 		skip_typing() | ||||
| 
 | ||||
| 
 | ||||
| # Start typing out the text | ||||
| ## Start typing out the text | ||||
| func type_out() -> void: | ||||
| 	text = dialogue_line.text | ||||
| 	visible_characters = 0 | ||||
| 	self.is_typing = true | ||||
| 	waiting_seconds = 0 | ||||
| 	visible_ratio = 0 | ||||
| 	_waiting_seconds = 0 | ||||
| 	_last_wait_index = -1 | ||||
| 	_last_mutation_index = -1 | ||||
| 
 | ||||
| 	# Text isn't calculated until the next frame | ||||
| 	self.is_typing = true | ||||
| 
 | ||||
| 	# Allow typing listeners a chance to connect | ||||
| 	await get_tree().process_frame | ||||
| 
 | ||||
| 	if get_total_character_count() == 0: | ||||
| 		self.is_typing = false | ||||
| 	elif seconds_per_step == 0: | ||||
| 		# Run any inline mutations | ||||
| 		for i in range(0, get_total_character_count()): | ||||
| 			mutate_inline_mutations(i) | ||||
| 		_mutate_remaining_mutations() | ||||
| 		visible_characters = get_total_character_count() | ||||
| 		self.is_typing = false | ||||
| 
 | ||||
| 
 | ||||
| ## Stop typing out the text and jump right to the end | ||||
| func skip_typing() -> void: | ||||
| 	_mutate_remaining_mutations() | ||||
| 	visible_characters = get_total_character_count() | ||||
| 	self.is_typing = false | ||||
| 	skipped_typing.emit() | ||||
| 
 | ||||
| 
 | ||||
| # Type out the next character(s) | ||||
| func type_next(delta: float, seconds_needed: float) -> void: | ||||
| func _type_next(delta: float, seconds_needed: float) -> void: | ||||
| 	if _is_awaiting_mutation: return | ||||
| 
 | ||||
| 	if visible_characters == get_total_character_count(): | ||||
| 		return | ||||
| 
 | ||||
| 	if last_mutation_index != visible_characters: | ||||
| 		last_mutation_index = visible_characters | ||||
| 		mutate_inline_mutations(visible_characters) | ||||
| 	if _last_mutation_index != visible_characters: | ||||
| 		_last_mutation_index = visible_characters | ||||
| 		_mutate_inline_mutations(visible_characters) | ||||
| 		if _is_awaiting_mutation: return | ||||
| 
 | ||||
| 	var additional_waiting_seconds: float = get_pause(visible_characters) | ||||
| 	var additional_waiting_seconds: float = _get_pause(visible_characters) | ||||
| 
 | ||||
| 	# Pause on characters like "." | ||||
| 	if visible_characters > 0 and get_parsed_text()[visible_characters - 1] in pause_at_characters.split(): | ||||
| 		additional_waiting_seconds += seconds_per_step * 15 | ||||
| 	if _should_auto_pause(): | ||||
| 		additional_waiting_seconds += seconds_per_pause_step | ||||
| 
 | ||||
| 	# Pause at literal [wait] directives | ||||
| 	if last_wait_index != visible_characters and additional_waiting_seconds > 0: | ||||
| 		last_wait_index = visible_characters | ||||
| 		waiting_seconds += additional_waiting_seconds | ||||
| 		paused_typing.emit(get_pause(visible_characters)) | ||||
| 	if _last_wait_index != visible_characters and additional_waiting_seconds > 0: | ||||
| 		_last_wait_index = visible_characters | ||||
| 		_waiting_seconds += additional_waiting_seconds | ||||
| 		paused_typing.emit(_get_pause(visible_characters)) | ||||
| 	else: | ||||
| 		visible_characters += 1 | ||||
| 		if visible_characters <= get_total_character_count(): | ||||
| 			spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, get_speed(visible_characters)) | ||||
| 			spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters)) | ||||
| 		# See if there's time to type out some more in this frame | ||||
| 		seconds_needed += seconds_per_step * (1.0 / get_speed(visible_characters)) | ||||
| 		seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters)) | ||||
| 		if seconds_needed > delta: | ||||
| 			waiting_seconds += seconds_needed | ||||
| 			_waiting_seconds += seconds_needed | ||||
| 		else: | ||||
| 			type_next(delta, seconds_needed) | ||||
| 			_type_next(delta, seconds_needed) | ||||
| 
 | ||||
| 
 | ||||
| # Get the pause for the current typing position if there is one | ||||
| func get_pause(at_index: int) -> float: | ||||
| func _get_pause(at_index: int) -> float: | ||||
| 	return dialogue_line.pauses.get(at_index, 0) | ||||
| 
 | ||||
| 
 | ||||
| # Get the speed for the current typing position | ||||
| func get_speed(at_index: int) -> float: | ||||
| func _get_speed(at_index: int) -> float: | ||||
| 	var speed: float = 1 | ||||
| 	for index in dialogue_line.speeds: | ||||
| 		if index > at_index: | ||||
|  | @ -128,12 +170,57 @@ func get_speed(at_index: int) -> float: | |||
| 	return speed | ||||
| 
 | ||||
| 
 | ||||
| # Run any inline mutations that haven't been run yet | ||||
| func _mutate_remaining_mutations() -> void: | ||||
| 	for i in range(visible_characters, get_total_character_count() + 1): | ||||
| 		_mutate_inline_mutations(i) | ||||
| 
 | ||||
| 
 | ||||
| # Run any mutations at the current typing position | ||||
| func mutate_inline_mutations(index: int) -> void: | ||||
| func _mutate_inline_mutations(index: int) -> void: | ||||
| 	for inline_mutation in dialogue_line.inline_mutations: | ||||
| 		# inline mutations are an array of arrays in the form of [character index, resolvable function] | ||||
| 		if inline_mutation[0] > index: | ||||
| 			return | ||||
| 		if inline_mutation[0] == index: | ||||
| 			_is_awaiting_mutation = true | ||||
| 			# The DialogueManager can't be referenced directly here so we need to get it by its path | ||||
| 			Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true) | ||||
| 			await Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true) | ||||
| 			_is_awaiting_mutation = false | ||||
| 
 | ||||
| 
 | ||||
| # Determine if the current autopause character at the cursor should qualify to pause typing. | ||||
| func _should_auto_pause() -> bool: | ||||
| 	if visible_characters == 0: return false | ||||
| 
 | ||||
| 	var parsed_text: String = get_parsed_text() | ||||
| 
 | ||||
| 	# Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out | ||||
| 	# Note: visible characters can be larger than parsed_text after a translation event | ||||
| 	if visible_characters >= parsed_text.length(): return false | ||||
| 
 | ||||
| 	# Ignore pause characters if they are next to a non-pause character | ||||
| 	if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split(): | ||||
| 		return false | ||||
| 
 | ||||
| 	# Ignore "." if it's between two numbers | ||||
| 	if visible_characters > 3 and parsed_text[visible_characters - 1] == ".": | ||||
| 		var possible_number: String = parsed_text.substr(visible_characters - 2, 3) | ||||
| 		if str(float(possible_number)) == possible_number: | ||||
| 			return false | ||||
| 
 | ||||
| 	# Ignore "." if it's used in an abbreviation | ||||
| 	# Note: does NOT support multi-period abbreviations (ex. p.m.) | ||||
| 	if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".": | ||||
| 		for abbreviation in skip_pause_at_abbreviations: | ||||
| 			if visible_characters >= abbreviation.length(): | ||||
| 				var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length()) | ||||
| 				if previous_characters == abbreviation: | ||||
| 					return false | ||||
| 
 | ||||
| 	# Ignore two non-"." characters next to each other | ||||
| 	var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split() | ||||
| 	if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters: | ||||
| 		return false | ||||
| 
 | ||||
| 	return parsed_text[visible_characters - 1] in pause_at_characters.split() | ||||
|  |  | |||
|  | @ -16,3 +16,4 @@ hint_underlined = false | |||
| deselect_on_focus_loss_enabled = false | ||||
| visible_characters_behavior = 1 | ||||
| script = ExtResource("1_cital") | ||||
| skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex") | ||||
|  |  | |||
|  | @ -1,43 +1,98 @@ | |||
| ## A line of dialogue returned from [code]DialogueManager[/code]. | ||||
| class_name DialogueLine extends RefCounted | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueResponse = preload("res://addons/dialogue_manager/dialogue_response.gd") | ||||
| const _DialogueConstants = preload("./constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| var type: String = DialogueConstants.TYPE_DIALOGUE | ||||
| ## The ID of this line | ||||
| var id: String | ||||
| 
 | ||||
| ## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code] | ||||
| var type: String = _DialogueConstants.TYPE_DIALOGUE | ||||
| 
 | ||||
| ## The next line ID after this line. | ||||
| var next_id: String = "" | ||||
| 
 | ||||
| ## The character name that is saying this line. | ||||
| var character: String = "" | ||||
| 
 | ||||
| ## A dictionary of variable replacements fo the character name. Generally for internal use only. | ||||
| var character_replacements: Array[Dictionary] = [] | ||||
| 
 | ||||
| ## The dialogue being spoken. | ||||
| var text: String = "" | ||||
| 
 | ||||
| ## A dictionary of replacements for the text. Generally for internal use only. | ||||
| var text_replacements: Array[Dictionary] = [] | ||||
| 
 | ||||
| ## The key to use for translating this line. | ||||
| var translation_key: String = "" | ||||
| 
 | ||||
| ## A map for when and for how long to pause while typing out the dialogue text. | ||||
| var pauses: Dictionary = {} | ||||
| 
 | ||||
| ## A map for speed changes when typing out the dialogue text. | ||||
| var speeds: Dictionary = {} | ||||
| 
 | ||||
| ## A map of any mutations to run while typing out the dialogue text. | ||||
| var inline_mutations: Array[Array] = [] | ||||
| 
 | ||||
| ## A list of responses attached to this line of dialogue. | ||||
| var responses: Array[DialogueResponse] = [] | ||||
| 
 | ||||
| ## A list of any extra game states to check when resolving variables and mutations. | ||||
| var extra_game_states: Array = [] | ||||
| var time = null | ||||
| 
 | ||||
| ## How long to show this line before advancing to the next. Either a float (of seconds), [code]"auto"[/code], or [code]null[/code]. | ||||
| var time: String = "" | ||||
| 
 | ||||
| ## Any #tags that were included in the line | ||||
| var tags: PackedStringArray = [] | ||||
| 
 | ||||
| ## The mutation details if this is a mutation line (where [code]type == TYPE_MUTATION[/code]). | ||||
| var mutation: Dictionary = {} | ||||
| 
 | ||||
| ## The conditions to check before including this line in the flow of dialogue. If failed the line will be skipped over. | ||||
| var conditions: Dictionary = {} | ||||
| 
 | ||||
| 
 | ||||
| func _init(data: Dictionary = {}) -> void: | ||||
| 	if data.size() > 0: | ||||
| 		id = data.id | ||||
| 		next_id = data.next_id | ||||
| 		type = data.type | ||||
| 		extra_game_states = data.extra_game_states | ||||
| 		extra_game_states = data.get("extra_game_states", []) | ||||
| 
 | ||||
| 		match type: | ||||
| 			DialogueConstants.TYPE_DIALOGUE: | ||||
| 			_DialogueConstants.TYPE_DIALOGUE: | ||||
| 				character = data.character | ||||
| 				character_replacements = data.character_replacements | ||||
| 				character_replacements = data.get("character_replacements", [] as Array[Dictionary]) | ||||
| 				text = data.text | ||||
| 				text_replacements = data.text_replacements | ||||
| 				translation_key = data.translation_key | ||||
| 				pauses = data.pauses | ||||
| 				speeds = data.speeds | ||||
| 				inline_mutations = data.inline_mutations | ||||
| 				time = data.time | ||||
| 				text_replacements = data.get("text_replacements", [] as Array[Dictionary]) | ||||
| 				translation_key = data.get("translation_key", data.text) | ||||
| 				pauses = data.get("pauses", {}) | ||||
| 				speeds = data.get("speeds", {}) | ||||
| 				inline_mutations = data.get("inline_mutations", [] as Array[Array]) | ||||
| 				time = data.get("time", "") | ||||
| 				tags = data.get("tags", []) | ||||
| 
 | ||||
| 			DialogueConstants.TYPE_MUTATION: | ||||
| 			_DialogueConstants.TYPE_MUTATION: | ||||
| 				mutation = data.mutation | ||||
| 
 | ||||
| 
 | ||||
| func _to_string() -> String: | ||||
| 	match type: | ||||
| 		_DialogueConstants.TYPE_DIALOGUE: | ||||
| 			return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text] | ||||
| 		_DialogueConstants.TYPE_MUTATION: | ||||
| 			return "<DialogueLine mutation>" | ||||
| 	return "" | ||||
| 
 | ||||
| 
 | ||||
| func get_tag_value(tag_name: String) -> String: | ||||
| 	var wrapped := "%s=" % tag_name | ||||
| 	for t in tags: | ||||
| 		if t.begins_with(wrapped): | ||||
| 			return t.replace(wrapped, "").strip_edges() | ||||
| 	return "" | ||||
|  |  | |||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								|  | @ -0,0 +1,131 @@ | |||
| @icon("./assets/responses_menu.svg") | ||||
| 
 | ||||
| ## A VBoxContainer for dialogue responses provided by [b]Dialogue Manager[/b]. | ||||
| class_name DialogueResponsesMenu extends VBoxContainer | ||||
| 
 | ||||
| 
 | ||||
| ## Emitted when a response is selected. | ||||
| signal response_selected(response) | ||||
| 
 | ||||
| 
 | ||||
| ## Optionally specify a control to duplicate for each response | ||||
| @export var response_template: Control | ||||
| 
 | ||||
| ## The action for accepting a response (is possibly overridden by parent dialogue balloon). | ||||
| @export var next_action: StringName = &"" | ||||
| 
 | ||||
| # The list of dialogue responses. | ||||
| var responses: Array = []: | ||||
| 	set(value): | ||||
| 		responses = value | ||||
| 
 | ||||
| 		# Remove any current items | ||||
| 		for item in get_children(): | ||||
| 			if item == response_template: continue | ||||
| 
 | ||||
| 			remove_child(item) | ||||
| 			item.queue_free() | ||||
| 
 | ||||
| 		# Add new items | ||||
| 		if responses.size() > 0: | ||||
| 			for response in responses: | ||||
| 				var item: Control | ||||
| 				if is_instance_valid(response_template): | ||||
| 					item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS) | ||||
| 					item.show() | ||||
| 				else: | ||||
| 					item = Button.new() | ||||
| 				item.name = "Response%d" % get_child_count() | ||||
| 				if not response.is_allowed: | ||||
| 					item.name = String(item.name) + "Disallowed" | ||||
| 					item.disabled = true | ||||
| 
 | ||||
| 				# If the item has a response property then use that | ||||
| 				if "response" in item: | ||||
| 					item.response = response | ||||
| 				# Otherwise assume we can just set the text | ||||
| 				else: | ||||
| 					item.text = response.text | ||||
| 
 | ||||
| 				item.set_meta("response", response) | ||||
| 
 | ||||
| 				add_child(item) | ||||
| 
 | ||||
| 			_configure_focus() | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
| 	visibility_changed.connect(func(): | ||||
| 		if visible and get_menu_items().size() > 0: | ||||
| 			get_menu_items()[0].grab_focus() | ||||
| 	) | ||||
| 
 | ||||
| 	if is_instance_valid(response_template): | ||||
| 		response_template.hide() | ||||
| 
 | ||||
| 
 | ||||
| # This is deprecated. | ||||
| func set_responses(next_responses: Array) -> void: | ||||
| 	self.responses = next_responses | ||||
| 
 | ||||
| 
 | ||||
| # Prepare the menu for keyboard and mouse navigation. | ||||
| func _configure_focus() -> void: | ||||
| 	var items = get_menu_items() | ||||
| 	for i in items.size(): | ||||
| 		var item: Control = items[i] | ||||
| 
 | ||||
| 		item.focus_mode = Control.FOCUS_ALL | ||||
| 
 | ||||
| 		item.focus_neighbor_left = item.get_path() | ||||
| 		item.focus_neighbor_right = item.get_path() | ||||
| 
 | ||||
| 		if i == 0: | ||||
| 			item.focus_neighbor_top = item.get_path() | ||||
| 			item.focus_previous = item.get_path() | ||||
| 		else: | ||||
| 			item.focus_neighbor_top = items[i - 1].get_path() | ||||
| 			item.focus_previous = items[i - 1].get_path() | ||||
| 
 | ||||
| 		if i == items.size() - 1: | ||||
| 			item.focus_neighbor_bottom = item.get_path() | ||||
| 			item.focus_next = item.get_path() | ||||
| 		else: | ||||
| 			item.focus_neighbor_bottom = items[i + 1].get_path() | ||||
| 			item.focus_next = items[i + 1].get_path() | ||||
| 
 | ||||
| 		item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) | ||||
| 		item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response"))) | ||||
| 
 | ||||
| 	items[0].grab_focus() | ||||
| 
 | ||||
| 
 | ||||
| ## Get the selectable items in the menu. | ||||
| func get_menu_items() -> Array: | ||||
| 	var items: Array = [] | ||||
| 	for child in get_children(): | ||||
| 		if not child.visible: continue | ||||
| 		if "Disallowed" in child.name: continue | ||||
| 		items.append(child) | ||||
| 
 | ||||
| 	return items | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
| 
 | ||||
| 
 | ||||
| func _on_response_mouse_entered(item: Control) -> void: | ||||
| 	if "Disallowed" in item.name: return | ||||
| 
 | ||||
| 	item.grab_focus() | ||||
| 
 | ||||
| 
 | ||||
| func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: | ||||
| 	if "Disallowed" in item.name: return | ||||
| 
 | ||||
| 	get_viewport().set_input_as_handled() | ||||
| 
 | ||||
| 	if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: | ||||
| 		response_selected.emit(response) | ||||
| 	elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items(): | ||||
| 		response_selected.emit(response) | ||||
|  | @ -1,20 +1,42 @@ | |||
| @tool | ||||
| @icon("./assets/icon.svg") | ||||
| 
 | ||||
| ## A collection of dialogue lines for use with [code]DialogueManager[/code]. | ||||
| class_name DialogueResource extends Resource | ||||
| 
 | ||||
| 
 | ||||
| const DialogueManager = preload("res://addons/dialogue_manager/dialogue_manager.gd") | ||||
| const _DialogueManager = preload("./dialogue_manager.gd") | ||||
| 
 | ||||
| ## A list of state shortcuts | ||||
| @export var using_states: PackedStringArray = [] | ||||
| 
 | ||||
| ## A map of titles and the lines they point to. | ||||
| @export var titles: Dictionary = {} | ||||
| 
 | ||||
| ## A list of character names. | ||||
| @export var character_names: PackedStringArray = [] | ||||
| 
 | ||||
| ## The first title in the file. | ||||
| @export var first_title: String = "" | ||||
| 
 | ||||
| ## A map of the encoded lines of dialogue. | ||||
| @export var lines: Dictionary = {} | ||||
| 
 | ||||
| ## raw version of the text | ||||
| @export var raw_text: String | ||||
| 
 | ||||
| func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: DialogueManager.MutationBehaviour = DialogueManager.MutationBehaviour.Wait) -> DialogueLine: | ||||
| 
 | ||||
| ## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can | ||||
| ## be a title string or a stringified line number). Runs any mutations along the way and then returns | ||||
| ## the first dialogue line encountered. | ||||
| func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: _DialogueManager.MutationBehaviour = _DialogueManager.MutationBehaviour.Wait) -> DialogueLine: | ||||
| 	return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour) | ||||
| 
 | ||||
| 
 | ||||
| ## Get the list of any titles found in the file. | ||||
| func get_titles() -> PackedStringArray: | ||||
| 	return titles.keys() | ||||
| 
 | ||||
| 
 | ||||
| func _to_string() -> String: | ||||
| 	return "<DialogueResource titles=\"%s\">" % [",".join(titles.keys())] | ||||
|  |  | |||
|  | @ -1,22 +1,62 @@ | |||
| ## A response to a line of dialogue, usualy attached to a [code]DialogueLine[/code]. | ||||
| class_name DialogueResponse extends RefCounted | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const _DialogueConstants = preload("./constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| var type: String = DialogueConstants.TYPE_RESPONSE | ||||
| ## The ID of this response | ||||
| var id: String | ||||
| 
 | ||||
| ## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code]. | ||||
| var type: String = _DialogueConstants.TYPE_RESPONSE | ||||
| 
 | ||||
| ## The next line ID to use if this response is selected by the player. | ||||
| var next_id: String = "" | ||||
| 
 | ||||
| ## [code]true[/code] if the condition of this line was met. | ||||
| var is_allowed: bool = true | ||||
| 
 | ||||
| ## A character (depending on the "characters in responses" behaviour setting). | ||||
| var character: String = "" | ||||
| 
 | ||||
| ## A dictionary of varialbe replaces for the character name. Generally for internal use only. | ||||
| var character_replacements: Array[Dictionary] = [] | ||||
| 
 | ||||
| ## The prompt for this response. | ||||
| var text: String = "" | ||||
| 
 | ||||
| ## A dictionary of variable replaces for the text. Generally for internal use only. | ||||
| var text_replacements: Array[Dictionary] = [] | ||||
| 
 | ||||
| ## Any #tags | ||||
| var tags: PackedStringArray = [] | ||||
| 
 | ||||
| ## The key to use for translating the text. | ||||
| var translation_key: String = "" | ||||
| 
 | ||||
| 
 | ||||
| func _init(data: Dictionary = {}) -> void: | ||||
| 	if data.size() > 0: | ||||
| 		id = data.id | ||||
| 		type = data.type | ||||
| 		next_id = data.next_id | ||||
| 		is_allowed = data.is_allowed | ||||
| 		character = data.character | ||||
| 		character_replacements = data.character_replacements | ||||
| 		text = data.text | ||||
| 		text_replacements = data.text_replacements | ||||
| 		tags = data.tags | ||||
| 		translation_key = data.translation_key | ||||
| 
 | ||||
| 
 | ||||
| func _to_string() -> String: | ||||
| 	return "<DialogueResponse text=\"%s\">" % text | ||||
| 
 | ||||
| 
 | ||||
| func get_tag_value(tag_name: String) -> String: | ||||
| 	var wrapped := "%s=" % tag_name | ||||
| 	for t in tags: | ||||
| 		if t.begins_with(wrapped): | ||||
| 			return t.replace(wrapped, "").strip_edges() | ||||
| 	return "" | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| extends EditorTranslationParserPlugin | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("./constants.gd") | ||||
| const DialogueSettings = preload("./settings.gd") | ||||
| const DialogueManagerParseResult = preload("./components/parse_result.gd") | ||||
| 
 | ||||
| 
 | ||||
| func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> void: | ||||
|  | @ -11,14 +13,15 @@ func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> v | |||
| 	var data: DialogueManagerParseResult = DialogueManagerParser.parse_string(text, path) | ||||
| 	var known_keys: PackedStringArray = PackedStringArray([]) | ||||
| 
 | ||||
| 	# Add all character names | ||||
| 	var character_names: PackedStringArray = data.character_names | ||||
| 	for character_name in character_names: | ||||
| 		if character_name in known_keys: continue | ||||
| 	# Add all character names if settings ask for it | ||||
| 	if DialogueSettings.get_setting("export_characters_in_translation", true): | ||||
| 		var character_names: PackedStringArray = data.character_names | ||||
| 		for character_name in character_names: | ||||
| 			if character_name in known_keys: continue | ||||
| 
 | ||||
| 		known_keys.append(character_name) | ||||
| 			known_keys.append(character_name) | ||||
| 
 | ||||
| 		msgids_context_plural.append([character_name, "dialogue", ""]) | ||||
| 			msgids_context_plural.append([character_name.replace('"', '\\"'), "dialogue", ""]) | ||||
| 
 | ||||
| 	# Add all dialogue lines and responses | ||||
| 	var dialogue: Dictionary = data.lines | ||||
|  | @ -31,9 +34,9 @@ func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> v | |||
| 		known_keys.append(line.translation_key) | ||||
| 
 | ||||
| 		if line.translation_key == "" or line.translation_key == line.text: | ||||
| 			msgids_context_plural.append([line.text, "", ""]) | ||||
| 			msgids_context_plural.append([line.text.replace('"', '\\"'), "", ""]) | ||||
| 		else: | ||||
| 			msgids_context_plural.append([line.text, line.translation_key, ""]) | ||||
| 			msgids_context_plural.append([line.text.replace('"', '\\"'), line.translation_key.replace('"', '\\"'), ""]) | ||||
| 
 | ||||
| 
 | ||||
| func _get_recognized_extensions() -> PackedStringArray: | ||||
|  |  | |||
|  | @ -0,0 +1,204 @@ | |||
| using Godot; | ||||
| using Godot.Collections; | ||||
| 
 | ||||
| namespace DialogueManagerRuntime | ||||
| { | ||||
|   public partial class ExampleBalloon : CanvasLayer | ||||
|   { | ||||
|     [Export] public string NextAction = "ui_accept"; | ||||
|     [Export] public string SkipAction = "ui_cancel"; | ||||
| 
 | ||||
| 
 | ||||
|     Control balloon; | ||||
|     RichTextLabel characterLabel; | ||||
|     RichTextLabel dialogueLabel; | ||||
|     VBoxContainer responsesMenu; | ||||
| 
 | ||||
|     Resource resource; | ||||
|     Array<Variant> temporaryGameStates = new Array<Variant>(); | ||||
|     bool isWaitingForInput = false; | ||||
|     bool willHideBalloon = false; | ||||
| 
 | ||||
|     DialogueLine dialogueLine; | ||||
|     DialogueLine DialogueLine | ||||
|     { | ||||
|       get => dialogueLine; | ||||
|       set | ||||
|       { | ||||
|         isWaitingForInput = false; | ||||
|         balloon.FocusMode = Control.FocusModeEnum.All; | ||||
|         balloon.GrabFocus(); | ||||
| 
 | ||||
|         if (value == null) | ||||
|         { | ||||
|           QueueFree(); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         dialogueLine = value; | ||||
|         UpdateDialogue(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public override void _Ready() | ||||
|     { | ||||
|       balloon = GetNode<Control>("%Balloon"); | ||||
|       characterLabel = GetNode<RichTextLabel>("%CharacterLabel"); | ||||
|       dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel"); | ||||
|       responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu"); | ||||
| 
 | ||||
|       balloon.Hide(); | ||||
| 
 | ||||
|       balloon.GuiInput += (@event) => | ||||
|       { | ||||
|         if ((bool)dialogueLabel.Get("is_typing")) | ||||
|         { | ||||
|           bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed(); | ||||
|           bool skipButtonWasPressed = @event.IsActionPressed(SkipAction); | ||||
|           if (mouseWasClicked || skipButtonWasPressed) | ||||
|           { | ||||
|             GetViewport().SetInputAsHandled(); | ||||
|             dialogueLabel.Call("skip_typing"); | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (!isWaitingForInput) return; | ||||
|         if (dialogueLine.Responses.Count > 0) return; | ||||
| 
 | ||||
|         GetViewport().SetInputAsHandled(); | ||||
| 
 | ||||
|         if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left) | ||||
|         { | ||||
|           Next(dialogueLine.NextId); | ||||
|         } | ||||
|         else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon) | ||||
|         { | ||||
|           Next(dialogueLine.NextId); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action"))) | ||||
|       { | ||||
|         responsesMenu.Set("next_action", NextAction); | ||||
|       } | ||||
|       responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => | ||||
|       { | ||||
|         Next(response.NextId); | ||||
|       })); | ||||
| 
 | ||||
|       DialogueManager.Mutated += OnMutated; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public override void _ExitTree() | ||||
|     { | ||||
|       DialogueManager.Mutated -= OnMutated; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public override void _UnhandledInput(InputEvent @event) | ||||
|     { | ||||
|       // Only the balloon is allowed to handle input while it's showing | ||||
|       GetViewport().SetInputAsHandled(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null) | ||||
|     { | ||||
|       temporaryGameStates = extraGameStates ?? new Array<Variant>(); | ||||
|       isWaitingForInput = false; | ||||
|       resource = dialogueResource; | ||||
| 
 | ||||
|       DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public async void Next(string nextId) | ||||
|     { | ||||
|       DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     #region Helpers | ||||
| 
 | ||||
| 
 | ||||
|     private async void UpdateDialogue() | ||||
|     { | ||||
|       if (!IsNodeReady()) | ||||
|       { | ||||
|         await ToSignal(this, SignalName.Ready); | ||||
|       } | ||||
| 
 | ||||
|       // Set up the character name | ||||
|       characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); | ||||
|       characterLabel.Text = Tr(dialogueLine.Character, "dialogue"); | ||||
| 
 | ||||
|       // Set up the dialogue | ||||
|       dialogueLabel.Hide(); | ||||
|       dialogueLabel.Set("dialogue_line", dialogueLine); | ||||
| 
 | ||||
|       // Set up the responses | ||||
|       responsesMenu.Hide(); | ||||
|       responsesMenu.Set("responses", dialogueLine.Responses); | ||||
| 
 | ||||
|       // Type out the text | ||||
|       balloon.Show(); | ||||
|       willHideBalloon = false; | ||||
|       dialogueLabel.Show(); | ||||
|       if (!string.IsNullOrEmpty(dialogueLine.Text)) | ||||
|       { | ||||
|         dialogueLabel.Call("type_out"); | ||||
|         await ToSignal(dialogueLabel, "finished_typing"); | ||||
|       } | ||||
| 
 | ||||
|       // Wait for input | ||||
|       if (dialogueLine.Responses.Count > 0) | ||||
|       { | ||||
|         balloon.FocusMode = Control.FocusModeEnum.None; | ||||
|         responsesMenu.Show(); | ||||
|       } | ||||
|       else if (!string.IsNullOrEmpty(dialogueLine.Time)) | ||||
|       { | ||||
|         float time = 0f; | ||||
|         if (!float.TryParse(dialogueLine.Time, out time)) | ||||
|         { | ||||
|           time = dialogueLine.Text.Length * 0.02f; | ||||
|         } | ||||
|         await ToSignal(GetTree().CreateTimer(time), "timeout"); | ||||
|         Next(dialogueLine.NextId); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         isWaitingForInput = true; | ||||
|         balloon.FocusMode = Control.FocusModeEnum.All; | ||||
|         balloon.GrabFocus(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     #endregion | ||||
| 
 | ||||
| 
 | ||||
|     #region signals | ||||
| 
 | ||||
| 
 | ||||
|     private void OnMutated(Dictionary _mutation) | ||||
|     { | ||||
|       isWaitingForInput = false; | ||||
|       willHideBalloon = true; | ||||
|       GetTree().CreateTimer(0.1f).Timeout += () => | ||||
|       { | ||||
|         if (willHideBalloon) | ||||
|         { | ||||
|           willHideBalloon = false; | ||||
|           balloon.Hide(); | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     #endregion | ||||
|   } | ||||
| } | ||||
|  | @ -1,12 +1,15 @@ | |||
| extends CanvasLayer | ||||
| 
 | ||||
| ## The action to use for advancing the dialogue | ||||
| @export var next_action: StringName = &"ui_accept" | ||||
| 
 | ||||
| @onready var balloon: ColorRect = $Balloon | ||||
| @onready var margin: MarginContainer = $Balloon/Margin | ||||
| @onready var character_label: RichTextLabel = $Balloon/Margin/VBox/CharacterLabel | ||||
| @onready var dialogue_label := $Balloon/Margin/VBox/DialogueLabel | ||||
| @onready var responses_menu: VBoxContainer = $Balloon/Margin/VBox/Responses | ||||
| @onready var response_template: RichTextLabel = %ResponseTemplate | ||||
| ## The action to use to skip typing the dialogue | ||||
| @export var skip_action: StringName = &"ui_cancel" | ||||
| 
 | ||||
| @onready var balloon: Control = %Balloon | ||||
| @onready var character_label: RichTextLabel = %CharacterLabel | ||||
| @onready var dialogue_label: DialogueLabel = %DialogueLabel | ||||
| @onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu | ||||
| 
 | ||||
| ## The dialogue resource | ||||
| var resource: DialogueResource | ||||
|  | @ -22,191 +25,117 @@ var will_hide_balloon: bool = false | |||
| 
 | ||||
| ## The current line | ||||
| var dialogue_line: DialogueLine: | ||||
|     set(next_dialogue_line): | ||||
|         is_waiting_for_input = false | ||||
| 	set(next_dialogue_line): | ||||
| 		is_waiting_for_input = false | ||||
| 		balloon.focus_mode = Control.FOCUS_ALL | ||||
| 		balloon.grab_focus() | ||||
| 
 | ||||
|         if not next_dialogue_line: | ||||
|             queue_free() | ||||
|             return | ||||
| 		# The dialogue has finished so close the balloon | ||||
| 		if not next_dialogue_line: | ||||
| 			queue_free() | ||||
| 			return | ||||
| 
 | ||||
|         # Remove any previous responses | ||||
|         for child in responses_menu.get_children(): | ||||
|             responses_menu.remove_child(child) | ||||
|             child.queue_free() | ||||
| 		# If the node isn't ready yet then none of the labels will be ready yet either | ||||
| 		if not is_node_ready(): | ||||
| 			await ready | ||||
| 
 | ||||
|         dialogue_line = next_dialogue_line | ||||
| 		dialogue_line = next_dialogue_line | ||||
| 
 | ||||
|         character_label.visible = not dialogue_line.character.is_empty() | ||||
|         character_label.text = tr(dialogue_line.character, "dialogue") | ||||
| 		character_label.visible = not dialogue_line.character.is_empty() | ||||
| 		character_label.text = tr(dialogue_line.character, "dialogue") | ||||
| 
 | ||||
|         dialogue_label.modulate.a = 0 | ||||
|         dialogue_label.custom_minimum_size.x = dialogue_label.get_parent().size.x - 1 | ||||
|         dialogue_label.dialogue_line = dialogue_line | ||||
| 		dialogue_label.hide() | ||||
| 		dialogue_label.dialogue_line = dialogue_line | ||||
| 
 | ||||
|         # Show any responses we have | ||||
|         responses_menu.modulate.a = 0 | ||||
|         if dialogue_line.responses.size() > 0: | ||||
|             for response in dialogue_line.responses: | ||||
|                 # Duplicate the template so we can grab the fonts, sizing, etc | ||||
|                 var item: RichTextLabel = response_template.duplicate(0) | ||||
|                 item.name = "Response%d" % responses_menu.get_child_count() | ||||
|                 if not response.is_allowed: | ||||
|                     item.name = String(item.name) + "Disallowed" | ||||
|                     item.modulate.a = 0.4 | ||||
|                 item.text = response.text | ||||
|                 item.show() | ||||
|                 responses_menu.add_child(item) | ||||
| 		responses_menu.hide() | ||||
| 		responses_menu.set_responses(dialogue_line.responses) | ||||
| 
 | ||||
|         # Show our balloon | ||||
|         balloon.show() | ||||
|         will_hide_balloon = false | ||||
| 		# Show our balloon | ||||
| 		balloon.show() | ||||
| 		will_hide_balloon = false | ||||
| 
 | ||||
|         dialogue_label.modulate.a = 1 | ||||
|         if not dialogue_line.text.is_empty(): | ||||
|             dialogue_label.type_out() | ||||
|             await dialogue_label.finished_typing | ||||
| 		dialogue_label.show() | ||||
| 		if not dialogue_line.text.is_empty(): | ||||
| 			dialogue_label.type_out() | ||||
| 			await dialogue_label.finished_typing | ||||
| 
 | ||||
|         # Wait for input | ||||
|         if dialogue_line.responses.size() > 0: | ||||
|             responses_menu.modulate.a = 1 | ||||
|             configure_menu() | ||||
|         elif dialogue_line.time != null: | ||||
|             var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float() | ||||
|             await get_tree().create_timer(time).timeout | ||||
|             next(dialogue_line.next_id) | ||||
|         else: | ||||
|             is_waiting_for_input = true | ||||
|             balloon.focus_mode = Control.FOCUS_ALL | ||||
|             balloon.grab_focus() | ||||
|     get: | ||||
|         return dialogue_line | ||||
| 		# Wait for input | ||||
| 		if dialogue_line.responses.size() > 0: | ||||
| 			balloon.focus_mode = Control.FOCUS_NONE | ||||
| 			responses_menu.show() | ||||
| 		elif dialogue_line.time != "": | ||||
| 			var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float() | ||||
| 			await get_tree().create_timer(time).timeout | ||||
| 			next(dialogue_line.next_id) | ||||
| 		else: | ||||
| 			is_waiting_for_input = true | ||||
| 			balloon.focus_mode = Control.FOCUS_ALL | ||||
| 			balloon.grab_focus() | ||||
| 	get: | ||||
| 		return dialogue_line | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
|     response_template.hide() | ||||
|     balloon.hide() | ||||
|     balloon.custom_minimum_size.x = balloon.get_viewport_rect().size.x | ||||
| 	balloon.hide() | ||||
| 	Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated) | ||||
| 
 | ||||
|     Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated) | ||||
| 	# If the responses menu doesn't have a next action set, use this one | ||||
| 	if responses_menu.next_action.is_empty(): | ||||
| 		responses_menu.next_action = next_action | ||||
| 
 | ||||
| 
 | ||||
| func _unhandled_input(_event: InputEvent) -> void: | ||||
|     # Only the balloon is allowed to handle input while it's showing | ||||
|     get_viewport().set_input_as_handled() | ||||
| 	# Only the balloon is allowed to handle input while it's showing | ||||
| 	get_viewport().set_input_as_handled() | ||||
| 
 | ||||
| 
 | ||||
| ## Start some dialogue | ||||
| func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: | ||||
|     temporary_game_states = extra_game_states | ||||
|     is_waiting_for_input = false | ||||
|     resource = dialogue_resource | ||||
| 
 | ||||
|     self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) | ||||
| 	temporary_game_states =  [self] + extra_game_states | ||||
| 	is_waiting_for_input = false | ||||
| 	resource = dialogue_resource | ||||
| 	self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) | ||||
| 
 | ||||
| 
 | ||||
| ## Go to the next line | ||||
| func next(next_id: String) -> void: | ||||
|     self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) | ||||
| 
 | ||||
| 
 | ||||
| ### Helpers | ||||
| 
 | ||||
| 
 | ||||
| # Set up keyboard movement and signals for the response menu | ||||
| func configure_menu() -> void: | ||||
|     balloon.focus_mode = Control.FOCUS_NONE | ||||
| 
 | ||||
|     var items = get_responses() | ||||
|     for i in items.size(): | ||||
|         var item: Control = items[i] | ||||
| 
 | ||||
|         item.focus_mode = Control.FOCUS_ALL | ||||
| 
 | ||||
|         item.focus_neighbor_left = item.get_path() | ||||
|         item.focus_neighbor_right = item.get_path() | ||||
| 
 | ||||
|         if i == 0: | ||||
|             item.focus_neighbor_top = item.get_path() | ||||
|             item.focus_previous = item.get_path() | ||||
|         else: | ||||
|             item.focus_neighbor_top = items[i - 1].get_path() | ||||
|             item.focus_previous = items[i - 1].get_path() | ||||
| 
 | ||||
|         if i == items.size() - 1: | ||||
|             item.focus_neighbor_bottom = item.get_path() | ||||
|             item.focus_next = item.get_path() | ||||
|         else: | ||||
|             item.focus_neighbor_bottom = items[i + 1].get_path() | ||||
|             item.focus_next = items[i + 1].get_path() | ||||
| 
 | ||||
|         item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) | ||||
|         item.gui_input.connect(_on_response_gui_input.bind(item)) | ||||
| 
 | ||||
|     items[0].grab_focus() | ||||
| 
 | ||||
| 
 | ||||
| # Get a list of enabled items | ||||
| func get_responses() -> Array: | ||||
|     var items: Array = [] | ||||
|     for child in responses_menu.get_children(): | ||||
|         if "Disallowed" in child.name: continue | ||||
|         items.append(child) | ||||
| 
 | ||||
|     return items | ||||
| 
 | ||||
| 
 | ||||
| func handle_resize() -> void: | ||||
|     if not is_instance_valid(margin): | ||||
|         call_deferred("handle_resize") | ||||
|         return | ||||
| 
 | ||||
|     balloon.custom_minimum_size.y = margin.size.y | ||||
|     # Force a resize on only the height | ||||
|     balloon.size.y = 0 | ||||
|     var viewport_size = balloon.get_viewport_rect().size | ||||
|     balloon.global_position = Vector2((viewport_size.x - balloon.size.x) * 0.5, viewport_size.y - balloon.size.y) | ||||
| 	self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
| 
 | ||||
| 
 | ||||
| func _on_mutated(_mutation: Dictionary) -> void: | ||||
|     is_waiting_for_input = false | ||||
|     will_hide_balloon = true | ||||
|     get_tree().create_timer(0.1).timeout.connect(func(): | ||||
|         if will_hide_balloon: | ||||
|             will_hide_balloon = false | ||||
|             balloon.hide() | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| func _on_response_mouse_entered(item: Control) -> void: | ||||
|     if "Disallowed" in item.name: return | ||||
| 
 | ||||
|     item.grab_focus() | ||||
| 
 | ||||
| 
 | ||||
| func _on_response_gui_input(event: InputEvent, item: Control) -> void: | ||||
|     if "Disallowed" in item.name: return | ||||
| 
 | ||||
|     if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1: | ||||
|         next(dialogue_line.responses[item.get_index()].next_id) | ||||
|     elif event.is_action_pressed("ui_accept") and item in get_responses(): | ||||
|         next(dialogue_line.responses[item.get_index()].next_id) | ||||
| 	is_waiting_for_input = false | ||||
| 	will_hide_balloon = true | ||||
| 	get_tree().create_timer(0.1).timeout.connect(func(): | ||||
| 		if will_hide_balloon: | ||||
| 			will_hide_balloon = false | ||||
| 			balloon.hide() | ||||
| 	) | ||||
| 
 | ||||
| 
 | ||||
| func _on_balloon_gui_input(event: InputEvent) -> void: | ||||
|     if not is_waiting_for_input: return | ||||
|     if dialogue_line.responses.size() > 0: return | ||||
| 	# See if we need to skip typing of the dialogue | ||||
| 	if dialogue_label.is_typing: | ||||
| 		var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() | ||||
| 		var skip_button_was_pressed: bool = event.is_action_pressed(skip_action) | ||||
| 		if mouse_was_clicked or skip_button_was_pressed: | ||||
| 			get_viewport().set_input_as_handled() | ||||
| 			dialogue_label.skip_typing() | ||||
| 			return | ||||
| 
 | ||||
|     # When there are no response options the balloon itself is the clickable thing | ||||
|     get_viewport().set_input_as_handled() | ||||
| 	if not is_waiting_for_input: return | ||||
| 	if dialogue_line.responses.size() > 0: return | ||||
| 
 | ||||
|     if event is InputEventMouseButton and event.is_pressed() and event.button_index == 1: | ||||
|         next(dialogue_line.next_id) | ||||
|     elif event.is_action_pressed("ui_accept") and get_viewport().gui_get_focus_owner() == balloon: | ||||
|         next(dialogue_line.next_id) | ||||
| 	# When there are no response options the balloon itself is the clickable thing | ||||
| 	get_viewport().set_input_as_handled() | ||||
| 
 | ||||
| 	if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: | ||||
| 		next(dialogue_line.next_id) | ||||
| 	elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon: | ||||
| 		next(dialogue_line.next_id) | ||||
| 
 | ||||
| 
 | ||||
| func _on_margin_resized() -> void: | ||||
|     handle_resize() | ||||
| func _on_responses_menu_response_selected(response: DialogueResponse) -> void: | ||||
| 	next(response.next_id) | ||||
|  |  | |||
|  | @ -1,45 +1,110 @@ | |||
| [gd_scene load_steps=5 format=3 uid="uid://73jm5qjy52vq"] | ||||
| [gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"] | ||||
| 
 | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_4u26j"] | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"] | ||||
| [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_72ixx"] | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5d24i"] | ||||
| content_margin_left = 40.0 | ||||
| content_margin_top = 5.0 | ||||
| content_margin_right = 5.0 | ||||
| content_margin_bottom = 5.0 | ||||
| bg_color = Color(1, 1, 1, 0.25098) | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] | ||||
| bg_color = Color(0, 0, 0, 1) | ||||
| border_width_left = 3 | ||||
| border_width_top = 3 | ||||
| border_width_right = 3 | ||||
| border_width_bottom = 3 | ||||
| border_color = Color(0.329412, 0.329412, 0.329412, 1) | ||||
| corner_radius_top_left = 5 | ||||
| corner_radius_top_right = 5 | ||||
| corner_radius_bottom_right = 5 | ||||
| corner_radius_bottom_left = 5 | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oj3c8"] | ||||
| content_margin_left = 40.0 | ||||
| content_margin_top = 5.0 | ||||
| content_margin_right = 5.0 | ||||
| content_margin_bottom = 5.0 | ||||
| draw_center = false | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ri4m3"] | ||||
| bg_color = Color(0.121569, 0.121569, 0.121569, 1) | ||||
| border_width_left = 3 | ||||
| border_width_top = 3 | ||||
| border_width_right = 3 | ||||
| border_width_bottom = 3 | ||||
| border_color = Color(1, 1, 1, 1) | ||||
| corner_radius_top_left = 5 | ||||
| corner_radius_top_right = 5 | ||||
| corner_radius_bottom_right = 5 | ||||
| corner_radius_bottom_left = 5 | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0njw"] | ||||
| bg_color = Color(0, 0, 0, 1) | ||||
| border_width_left = 3 | ||||
| border_width_top = 3 | ||||
| border_width_right = 3 | ||||
| border_width_bottom = 3 | ||||
| border_color = Color(0.6, 0.6, 0.6, 1) | ||||
| corner_radius_top_left = 5 | ||||
| corner_radius_top_right = 5 | ||||
| corner_radius_bottom_right = 5 | ||||
| corner_radius_bottom_left = 5 | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] | ||||
| bg_color = Color(0, 0, 0, 1) | ||||
| border_width_left = 3 | ||||
| border_width_top = 3 | ||||
| border_width_right = 3 | ||||
| border_width_bottom = 3 | ||||
| corner_radius_top_left = 5 | ||||
| corner_radius_top_right = 5 | ||||
| corner_radius_bottom_right = 5 | ||||
| corner_radius_bottom_left = 5 | ||||
| 
 | ||||
| [sub_resource type="Theme" id="Theme_qq3yp"] | ||||
| default_font_size = 20 | ||||
| Button/styles/disabled = SubResource("StyleBoxFlat_spyqn") | ||||
| Button/styles/focus = SubResource("StyleBoxFlat_ri4m3") | ||||
| Button/styles/hover = SubResource("StyleBoxFlat_e0njw") | ||||
| Button/styles/normal = SubResource("StyleBoxFlat_e0njw") | ||||
| MarginContainer/constants/margin_bottom = 15 | ||||
| MarginContainer/constants/margin_left = 30 | ||||
| MarginContainer/constants/margin_right = 30 | ||||
| MarginContainer/constants/margin_top = 15 | ||||
| Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") | ||||
| 
 | ||||
| [node name="ExampleBalloon" type="CanvasLayer"] | ||||
| layer = 100 | ||||
| script = ExtResource("1_4u26j") | ||||
| script = ExtResource("1_36de5") | ||||
| 
 | ||||
| [node name="Balloon" type="ColorRect" parent="."] | ||||
| color = Color(0, 0, 0, 1) | ||||
| 
 | ||||
| [node name="Margin" type="MarginContainer" parent="Balloon"] | ||||
| layout_mode = 0 | ||||
| [node name="Balloon" type="Control" parent="."] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 3 | ||||
| anchors_preset = 15 | ||||
| anchor_right = 1.0 | ||||
| offset_bottom = 119.0 | ||||
| anchor_bottom = 1.0 | ||||
| grow_horizontal = 2 | ||||
| theme_override_constants/margin_left = 20 | ||||
| theme_override_constants/margin_top = 10 | ||||
| theme_override_constants/margin_right = 20 | ||||
| theme_override_constants/margin_bottom = 10 | ||||
| metadata/_edit_layout_mode = 1 | ||||
| grow_vertical = 2 | ||||
| theme = SubResource("Theme_qq3yp") | ||||
| 
 | ||||
| [node name="VBox" type="VBoxContainer" parent="Balloon/Margin"] | ||||
| [node name="Panel" type="Panel" parent="Balloon"] | ||||
| clip_children = 2 | ||||
| layout_mode = 1 | ||||
| anchors_preset = 12 | ||||
| anchor_top = 1.0 | ||||
| anchor_right = 1.0 | ||||
| anchor_bottom = 1.0 | ||||
| offset_left = 21.0 | ||||
| offset_top = -183.0 | ||||
| offset_right = -19.0 | ||||
| offset_bottom = -19.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 0 | ||||
| mouse_filter = 1 | ||||
| 
 | ||||
| [node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 15 | ||||
| anchor_right = 1.0 | ||||
| anchor_bottom = 1.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 2 | ||||
| 
 | ||||
| [node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] | ||||
| layout_mode = 2 | ||||
| theme_override_constants/separation = 10 | ||||
| 
 | ||||
| [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Margin/VBox"] | ||||
| [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] | ||||
| unique_name_in_owner = true | ||||
| modulate = Color(1, 1, 1, 0.501961) | ||||
| layout_mode = 2 | ||||
| mouse_filter = 1 | ||||
|  | @ -48,26 +113,37 @@ text = "Character" | |||
| fit_content = true | ||||
| scroll_active = false | ||||
| 
 | ||||
| [node name="DialogueLabel" parent="Balloon/Margin/VBox" instance=ExtResource("2_a8ve6")] | ||||
| layout_mode = 2 | ||||
| text = "Dialogue" | ||||
| 
 | ||||
| [node name="Responses" type="VBoxContainer" parent="Balloon/Margin/VBox"] | ||||
| layout_mode = 2 | ||||
| theme_override_constants/separation = 2 | ||||
| 
 | ||||
| [node name="ResponseTemplate" type="RichTextLabel" parent="Balloon/Margin/VBox"] | ||||
| [node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_a8ve6")] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| theme_override_styles/focus = SubResource("StyleBoxFlat_5d24i") | ||||
| theme_override_styles/normal = SubResource("StyleBoxFlat_oj3c8") | ||||
| bbcode_enabled = true | ||||
| text = "Response" | ||||
| fit_content = true | ||||
| scroll_active = false | ||||
| shortcut_keys_enabled = false | ||||
| meta_underlined = false | ||||
| hint_underlined = false | ||||
| size_flags_vertical = 3 | ||||
| text = "Dialogue..." | ||||
| 
 | ||||
| [node name="Responses" type="MarginContainer" parent="Balloon"] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 7 | ||||
| anchor_left = 0.5 | ||||
| anchor_top = 1.0 | ||||
| anchor_right = 0.5 | ||||
| anchor_bottom = 1.0 | ||||
| offset_left = -147.0 | ||||
| offset_top = -558.0 | ||||
| offset_right = 494.0 | ||||
| offset_bottom = -154.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 0 | ||||
| 
 | ||||
| [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses" node_paths=PackedStringArray("response_template")] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| size_flags_vertical = 8 | ||||
| theme_override_constants/separation = 2 | ||||
| script = ExtResource("3_72ixx") | ||||
| response_template = NodePath("ResponseExample") | ||||
| 
 | ||||
| [node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] | ||||
| layout_mode = 2 | ||||
| text = "Response example" | ||||
| 
 | ||||
| [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] | ||||
| [connection signal="resized" from="Balloon/Margin" to="." method="_on_margin_resized"] | ||||
| [connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] | ||||
|  |  | |||
|  | @ -1,87 +1,173 @@ | |||
| [gd_scene load_steps=8 format=3 uid="uid://b361p61jmf257"] | ||||
| [gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"] | ||||
| 
 | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_4u26j"] | ||||
| [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"] | ||||
| [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"] | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_1j1j0"] | ||||
| 
 | ||||
| [sub_resource type="Theme" id="Theme_isg48"] | ||||
| default_font_size = 9 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"] | ||||
| content_margin_left = 6.0 | ||||
| content_margin_top = 3.0 | ||||
| content_margin_right = 6.0 | ||||
| content_margin_bottom = 3.0 | ||||
| bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) | ||||
| border_width_left = 1 | ||||
| border_width_top = 1 | ||||
| border_width_right = 1 | ||||
| border_width_bottom = 1 | ||||
| border_color = Color(0.345098, 0.345098, 0.345098, 1) | ||||
| corner_radius_top_left = 3 | ||||
| corner_radius_top_right = 3 | ||||
| corner_radius_bottom_right = 3 | ||||
| corner_radius_bottom_left = 3 | ||||
| 
 | ||||
| [sub_resource type="Theme" id="Theme_owda0"] | ||||
| default_font_size = 9 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ufjut"] | ||||
| content_margin_left = 6.0 | ||||
| content_margin_top = 3.0 | ||||
| content_margin_right = 6.0 | ||||
| content_margin_bottom = 3.0 | ||||
| bg_color = Color(0.227451, 0.227451, 0.227451, 1) | ||||
| border_width_left = 1 | ||||
| border_width_top = 1 | ||||
| border_width_right = 1 | ||||
| border_width_bottom = 1 | ||||
| border_color = Color(1, 1, 1, 1) | ||||
| corner_radius_top_left = 3 | ||||
| corner_radius_top_right = 3 | ||||
| corner_radius_bottom_right = 3 | ||||
| corner_radius_bottom_left = 3 | ||||
| 
 | ||||
| [sub_resource type="Theme" id="Theme_fakos"] | ||||
| default_font_size = 9 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fcbqo"] | ||||
| content_margin_left = 6.0 | ||||
| content_margin_top = 3.0 | ||||
| content_margin_right = 6.0 | ||||
| content_margin_bottom = 3.0 | ||||
| bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) | ||||
| border_width_left = 1 | ||||
| border_width_top = 1 | ||||
| border_width_right = 1 | ||||
| border_width_bottom = 1 | ||||
| corner_radius_top_left = 3 | ||||
| corner_radius_top_right = 3 | ||||
| corner_radius_bottom_right = 3 | ||||
| corner_radius_bottom_left = 3 | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5d24i"] | ||||
| content_margin_left = 20.0 | ||||
| content_margin_top = 2.0 | ||||
| content_margin_right = 2.0 | ||||
| content_margin_bottom = 2.0 | ||||
| bg_color = Color(1, 1, 1, 0.25098) | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t6i7a"] | ||||
| content_margin_left = 6.0 | ||||
| content_margin_top = 3.0 | ||||
| content_margin_right = 6.0 | ||||
| content_margin_bottom = 3.0 | ||||
| bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) | ||||
| border_width_left = 1 | ||||
| border_width_top = 1 | ||||
| border_width_right = 1 | ||||
| border_width_bottom = 1 | ||||
| corner_radius_top_left = 3 | ||||
| corner_radius_top_right = 3 | ||||
| corner_radius_bottom_right = 3 | ||||
| corner_radius_bottom_left = 3 | ||||
| 
 | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oj3c8"] | ||||
| content_margin_left = 20.0 | ||||
| content_margin_top = 2.0 | ||||
| content_margin_right = 2.0 | ||||
| content_margin_bottom = 2.0 | ||||
| draw_center = false | ||||
| [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] | ||||
| bg_color = Color(0, 0, 0, 1) | ||||
| border_width_left = 1 | ||||
| border_width_top = 1 | ||||
| border_width_right = 1 | ||||
| border_width_bottom = 1 | ||||
| corner_radius_top_left = 3 | ||||
| corner_radius_top_right = 3 | ||||
| corner_radius_bottom_right = 3 | ||||
| corner_radius_bottom_left = 3 | ||||
| 
 | ||||
| [sub_resource type="Theme" id="Theme_qq3yp"] | ||||
| default_font_size = 8 | ||||
| Button/styles/disabled = SubResource("StyleBoxFlat_235ry") | ||||
| Button/styles/focus = SubResource("StyleBoxFlat_ufjut") | ||||
| Button/styles/hover = SubResource("StyleBoxFlat_fcbqo") | ||||
| Button/styles/normal = SubResource("StyleBoxFlat_t6i7a") | ||||
| MarginContainer/constants/margin_bottom = 4 | ||||
| MarginContainer/constants/margin_left = 8 | ||||
| MarginContainer/constants/margin_right = 8 | ||||
| MarginContainer/constants/margin_top = 4 | ||||
| Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") | ||||
| 
 | ||||
| [node name="ExampleBalloon" type="CanvasLayer"] | ||||
| layer = 100 | ||||
| script = ExtResource("1_4u26j") | ||||
| script = ExtResource("1_s2gbs") | ||||
| 
 | ||||
| [node name="Balloon" type="ColorRect" parent="."] | ||||
| color = Color(0, 0, 0, 1) | ||||
| 
 | ||||
| [node name="Margin" type="MarginContainer" parent="Balloon"] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 10 | ||||
| [node name="Balloon" type="Control" parent="."] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 3 | ||||
| anchors_preset = 15 | ||||
| anchor_right = 1.0 | ||||
| offset_bottom = 75.0 | ||||
| anchor_bottom = 1.0 | ||||
| grow_horizontal = 2 | ||||
| theme_override_constants/margin_left = 20 | ||||
| theme_override_constants/margin_top = 10 | ||||
| theme_override_constants/margin_right = 20 | ||||
| theme_override_constants/margin_bottom = 10 | ||||
| metadata/_edit_layout_mode = 1 | ||||
| grow_vertical = 2 | ||||
| theme = SubResource("Theme_qq3yp") | ||||
| 
 | ||||
| [node name="VBox" type="VBoxContainer" parent="Balloon/Margin"] | ||||
| [node name="Panel" type="Panel" parent="Balloon"] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 12 | ||||
| anchor_top = 1.0 | ||||
| anchor_right = 1.0 | ||||
| anchor_bottom = 1.0 | ||||
| offset_left = 3.0 | ||||
| offset_top = -62.0 | ||||
| offset_right = -4.0 | ||||
| offset_bottom = -4.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 0 | ||||
| 
 | ||||
| [node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 15 | ||||
| anchor_right = 1.0 | ||||
| anchor_bottom = 1.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 2 | ||||
| 
 | ||||
| [node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] | ||||
| layout_mode = 2 | ||||
| theme_override_constants/separation = 4 | ||||
| 
 | ||||
| [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Margin/VBox"] | ||||
| [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] | ||||
| unique_name_in_owner = true | ||||
| modulate = Color(1, 1, 1, 0.501961) | ||||
| layout_mode = 2 | ||||
| mouse_filter = 1 | ||||
| theme = SubResource("Theme_isg48") | ||||
| bbcode_enabled = true | ||||
| text = "Character" | ||||
| fit_content = true | ||||
| scroll_active = false | ||||
| 
 | ||||
| [node name="DialogueLabel" parent="Balloon/Margin/VBox" instance=ExtResource("2_a8ve6")] | ||||
| layout_mode = 2 | ||||
| theme = SubResource("Theme_owda0") | ||||
| text = "Dialogue" | ||||
| 
 | ||||
| [node name="Responses" type="VBoxContainer" parent="Balloon/Margin/VBox"] | ||||
| layout_mode = 2 | ||||
| theme_override_constants/separation = 1 | ||||
| 
 | ||||
| [node name="ResponseTemplate" type="RichTextLabel" parent="Balloon/Margin/VBox"] | ||||
| [node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_hfvdi")] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| focus_mode = 2 | ||||
| theme = SubResource("Theme_fakos") | ||||
| theme_override_styles/focus = SubResource("StyleBoxFlat_5d24i") | ||||
| theme_override_styles/normal = SubResource("StyleBoxFlat_oj3c8") | ||||
| bbcode_enabled = true | ||||
| text = "Response" | ||||
| fit_content = true | ||||
| scroll_active = false | ||||
| shortcut_keys_enabled = false | ||||
| meta_underlined = false | ||||
| hint_underlined = false | ||||
| size_flags_vertical = 3 | ||||
| text = "Dialogue..." | ||||
| skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex") | ||||
| 
 | ||||
| [node name="Responses" type="MarginContainer" parent="Balloon"] | ||||
| layout_mode = 1 | ||||
| anchors_preset = 7 | ||||
| anchor_left = 0.5 | ||||
| anchor_top = 1.0 | ||||
| anchor_right = 0.5 | ||||
| anchor_bottom = 1.0 | ||||
| offset_left = -124.0 | ||||
| offset_top = -218.0 | ||||
| offset_right = 125.0 | ||||
| offset_bottom = -50.0 | ||||
| grow_horizontal = 2 | ||||
| grow_vertical = 0 | ||||
| 
 | ||||
| [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| size_flags_vertical = 8 | ||||
| theme_override_constants/separation = 2 | ||||
| script = ExtResource("3_1j1j0") | ||||
| 
 | ||||
| [node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] | ||||
| layout_mode = 2 | ||||
| text = "Response Example" | ||||
| 
 | ||||
| [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] | ||||
| [connection signal="resized" from="Balloon/Margin" to="." method="_on_margin_resized"] | ||||
| [connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] | ||||
|  |  | |||
|  | @ -5,11 +5,10 @@ extends EditorImportPlugin | |||
| signal compiled_resource(resource: Resource) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueResource = preload("res://addons/dialogue_manager/dialogue_resource.gd") | ||||
| const compiler_version = 8 | ||||
| const DialogueResource = preload("./dialogue_resource.gd") | ||||
| const DialogueManagerParseResult = preload("./components/parse_result.gd") | ||||
| 
 | ||||
| 
 | ||||
| var editor_plugin | ||||
| const compiler_version = 11 | ||||
| 
 | ||||
| 
 | ||||
| func _get_importer_name() -> String: | ||||
|  | @ -63,26 +62,24 @@ func _get_option_visibility(path: String, option_name: StringName, options: Dict | |||
| 
 | ||||
| 
 | ||||
| func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error: | ||||
| 	return compile_file(source_file, "%s.%s" % [save_path, _get_save_extension()]) | ||||
| 	var cache = Engine.get_meta("DialogueCache") | ||||
| 
 | ||||
| 
 | ||||
| func compile_file(path: String, resource_path: String, will_cascade_cache_data: bool = true) -> Error: | ||||
| 	# Get the raw file contents | ||||
| 	if not FileAccess.file_exists(path): return ERR_FILE_NOT_FOUND | ||||
| 	if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND | ||||
| 
 | ||||
| 	var file: FileAccess = FileAccess.open(path, FileAccess.READ) | ||||
| 	var file: FileAccess = FileAccess.open(source_file, FileAccess.READ) | ||||
| 	var raw_text: String = file.get_as_text() | ||||
| 
 | ||||
| 	# Parse the text | ||||
| 	var parser: DialogueManagerParser = DialogueManagerParser.new() | ||||
| 	var err: Error = parser.parse(raw_text, path) | ||||
| 	var err: Error = parser.parse(raw_text, source_file) | ||||
| 	var data: DialogueManagerParseResult = parser.get_data() | ||||
| 	var errors: Array[Dictionary] = parser.get_errors() | ||||
| 	parser.free() | ||||
| 
 | ||||
| 	if err != OK: | ||||
| 		printerr("%d errors found in %s" % [errors.size(), path]) | ||||
| 		editor_plugin.add_errors_to_dialogue_file_cache(path, errors) | ||||
| 		printerr("%d errors found in %s" % [errors.size(), source_file]) | ||||
| 		cache.add_errors_to_file(source_file, errors) | ||||
| 		return err | ||||
| 
 | ||||
| 	# Get the current addon version | ||||
|  | @ -94,16 +91,23 @@ func compile_file(path: String, resource_path: String, will_cascade_cache_data: | |||
| 	var resource: DialogueResource = DialogueResource.new() | ||||
| 	resource.set_meta("dialogue_manager_version", version) | ||||
| 
 | ||||
| 	resource.using_states = data.using_states | ||||
| 	resource.titles = data.titles | ||||
| 	resource.first_title = data.first_title | ||||
| 	resource.character_names = data.character_names | ||||
| 	resource.lines = data.lines | ||||
| 	resource.raw_text = data.raw_text | ||||
| 
 | ||||
| 	if will_cascade_cache_data: | ||||
| 		editor_plugin.add_to_dialogue_file_cache(path, resource_path, data) | ||||
| 	# Clear errors and possibly trigger any cascade recompiles | ||||
| 	cache.add_file(source_file, data) | ||||
| 
 | ||||
| 	err = ResourceSaver.save(resource, resource_path) | ||||
| 	err = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()]) | ||||
| 
 | ||||
| 	compiled_resource.emit(resource) | ||||
| 
 | ||||
| 	# Recompile any dependencies | ||||
| 	var dependent_paths: PackedStringArray = cache.get_dependent_paths_for_reimport(source_file) | ||||
| 	for path in dependent_paths: | ||||
| 		append_import_external_resource(path) | ||||
| 
 | ||||
| 	return err | ||||
|  |  | |||
											
												Binary file not shown.
											
										
									
								|  | @ -30,6 +30,9 @@ msgstr "Clear recent files" | |||
| msgid "save_all_files" | ||||
| msgstr "Save all files" | ||||
| 
 | ||||
| msgid "find_in_files" | ||||
| msgstr "Find in files..." | ||||
| 
 | ||||
| msgid "test_dialogue" | ||||
| msgstr "Test dialogue" | ||||
| 
 | ||||
|  | @ -45,6 +48,12 @@ msgstr "Translations" | |||
| msgid "settings" | ||||
| msgstr "Settings" | ||||
| 
 | ||||
| msgid "sponsor" | ||||
| msgstr "Sponsor" | ||||
| 
 | ||||
| msgid "show_support" | ||||
| msgstr "Support Dialogue Manager" | ||||
| 
 | ||||
| msgid "docs" | ||||
| msgstr "Docs" | ||||
| 
 | ||||
|  | @ -132,11 +141,23 @@ msgstr "Copy file path" | |||
| msgid "buffer.show_in_filesystem" | ||||
| msgstr "Show in FileSystem" | ||||
| 
 | ||||
| msgid "settings.invalid_test_scene" | ||||
| msgstr "\"{path}\" does not extend BaseDialogueTestScene." | ||||
| 
 | ||||
| msgid "settings.revert_to_default_test_scene" | ||||
| msgstr "Revert to default test scene" | ||||
| 
 | ||||
| msgid "settings.default_balloon_hint" | ||||
| msgstr "Custom balloon to use when calling \"DialogueManager.show_balloon()\"" | ||||
| 
 | ||||
| msgid "settings.revert_to_default_balloon" | ||||
| msgstr "Revert to default balloon" | ||||
| 
 | ||||
| msgid "settings.default_balloon_path" | ||||
| msgstr "<example balloon>" | ||||
| 
 | ||||
| msgid "settings.autoload" | ||||
| msgstr "Autload" | ||||
| msgstr "Autoload" | ||||
| 
 | ||||
| msgid "settings.path" | ||||
| msgstr "Path" | ||||
|  | @ -150,15 +171,24 @@ msgstr "Treat missing translation keys as errors" | |||
| msgid "settings.missing_keys_hint" | ||||
| msgstr "If you are using static translation keys then having this enabled will help you find any lines that you haven't added a key to yet." | ||||
| 
 | ||||
| msgid "settings.characters_translations" | ||||
| msgstr "Export character names in translation files" | ||||
| 
 | ||||
| msgid "settings.wrap_long_lines" | ||||
| msgstr "Wrap long lines" | ||||
| 
 | ||||
| msgid "settings.include_failed_responses" | ||||
| msgstr "Include responses with failed conditions" | ||||
| 
 | ||||
| msgid "settings.ignore_missing_state_values" | ||||
| msgstr "Skip over missing state value errors (not recommended)" | ||||
| 
 | ||||
| msgid "settings.custom_test_scene" | ||||
| msgstr "Custom test scene (must extend BaseDialogueTestScene)" | ||||
| 
 | ||||
| msgid "settings.default_csv_locale" | ||||
| msgstr "Default CSV Locale" | ||||
| 
 | ||||
| msgid "settings.states_shortcuts" | ||||
| msgstr "State Shortcuts" | ||||
| 
 | ||||
|  | @ -168,9 +198,45 @@ msgstr "If an autoload is enabled here you can refer to its properties and metho | |||
| msgid "settings.states_hint" | ||||
| msgstr "ie. Instead of \"SomeState.some_property\" you could just use \"some_property\"" | ||||
| 
 | ||||
| msgid "settings.recompile_warning" | ||||
| msgstr "Changing these settings will force a recompile of all dialogue. Only change them if you know what you are doing." | ||||
| 
 | ||||
| msgid "settings.create_lines_for_responses_with_characters" | ||||
| msgstr "Create child dialogue line for responses with character names in them" | ||||
| 
 | ||||
| msgid "settings.open_in_external_editor" | ||||
| msgstr "Open dialogue files in external editor" | ||||
| 
 | ||||
| msgid "settings.external_editor_warning" | ||||
| msgstr "Note: Syntax highlighting and detailed error checking are not supported in external editors." | ||||
| 
 | ||||
| msgid "settings.include_characters_in_translations" | ||||
| msgstr "Include character names in translation exports" | ||||
| 
 | ||||
| msgid "settings.include_notes_in_translations" | ||||
| msgstr "Include notes (## comments) in translation exports" | ||||
| 
 | ||||
| msgid "settings.check_for_updates" | ||||
| msgstr "Check for updates" | ||||
| 
 | ||||
| msgid "n_of_n" | ||||
| msgstr "{index} of {total}" | ||||
| 
 | ||||
| msgid "search.find" | ||||
| msgstr "Find:" | ||||
| 
 | ||||
| msgid "search.find_all" | ||||
| msgstr "Find all..." | ||||
| 
 | ||||
| msgid "search.placeholder" | ||||
| msgstr "Text to search for" | ||||
| 
 | ||||
| msgid "search.replace_placeholder" | ||||
| msgstr "Text to replace it with" | ||||
| 
 | ||||
| msgid "search.replace_selected" | ||||
| msgstr "Replace selected" | ||||
| 
 | ||||
| msgid "search.previous" | ||||
| msgstr "Previous" | ||||
| 
 | ||||
|  | @ -198,6 +264,9 @@ msgstr "Filter files" | |||
| msgid "titles_list.filter" | ||||
| msgstr "Filter titles" | ||||
| 
 | ||||
| msgid "errors.key_not_found" | ||||
| msgstr "Key \"{key}\" not found." | ||||
| 
 | ||||
| msgid "errors.line_and_message" | ||||
| msgstr "Error at {line}, {column}: {message}" | ||||
| 
 | ||||
|  | @ -216,6 +285,9 @@ msgstr "File already imported." | |||
| msgid "errors.duplicate_import" | ||||
| msgstr "Duplicate import name." | ||||
| 
 | ||||
| msgid "errors.unknown_using" | ||||
| msgstr "Unknown autoload in using statement." | ||||
| 
 | ||||
| msgid "errors.empty_title" | ||||
| msgstr "Titles cannot be empty." | ||||
| 
 | ||||
|  | @ -223,7 +295,7 @@ msgid "errors.duplicate_title" | |||
| msgstr "There is already a title with that name." | ||||
| 
 | ||||
| msgid "errors.nested_title" | ||||
| msgstr "Titles cannot be nested." | ||||
| msgstr "Titles cannot be indented." | ||||
| 
 | ||||
| msgid "errors.invalid_title_string" | ||||
| msgstr "Titles can only contain alphanumeric characters and numbers." | ||||
|  | @ -259,7 +331,7 @@ msgid "errors.condition_has_no_content" | |||
| msgstr "A condition line needs an indented line below it." | ||||
| 
 | ||||
| msgid "errors.incomplete_expression" | ||||
| msgstr "Incomplate expression." | ||||
| msgstr "Incomplete expression." | ||||
| 
 | ||||
| msgid "errors.invalid_expression_for_value" | ||||
| msgstr "Invalid expression for value." | ||||
|  | @ -366,12 +438,6 @@ msgstr "You have {count} errors in your dialogue text. See Output for details." | |||
| msgid "runtime.invalid_expression" | ||||
| msgstr "\"{expression}\" is not a valid expression: {error}" | ||||
| 
 | ||||
| msgid "runtime.unsupported_array_method" | ||||
| msgstr "Calling \"{method_name}\" on an array isn't supported." | ||||
| 
 | ||||
| msgid "runtime.unsupported_dictionary_method" | ||||
| msgstr "Calling \"{method_name}\" on a dictionary isn't supported." | ||||
| 
 | ||||
| msgid "runtime.array_index_out_of_bounds" | ||||
| msgstr "Index {index} out of bounds of array \"{array}\"." | ||||
| 
 | ||||
|  | @ -384,6 +450,9 @@ msgstr "Key \"{key}\" not found in dictionary \"{dictionary}\"" | |||
| msgid "runtime.property_not_found" | ||||
| msgstr "\"{property}\" is not a property on any game states ({states})." | ||||
| 
 | ||||
| msgid "runtime.property_not_found_missing_export" | ||||
| msgstr "\"{property}\" is not a property on any game states ({states}). You might need to add an [Export] decorator." | ||||
| 
 | ||||
| msgid "runtime.method_not_found" | ||||
| msgstr "\"{method}\" is not a method on any game states ({states})" | ||||
| 
 | ||||
|  | @ -396,5 +465,17 @@ msgstr "\"{method}\" is not a callable method on \"{object}\"" | |||
| msgid "runtime.unknown_operator" | ||||
| msgstr "Unknown operator." | ||||
| 
 | ||||
| msgid "runtime.unknown_autoload" | ||||
| msgstr "\"{autoload}\" doesn't appear to be a valid autoload." | ||||
| 
 | ||||
| msgid "runtime.something_went_wrong" | ||||
| msgstr "Something went wrong." | ||||
| msgstr "Something went wrong." | ||||
| 
 | ||||
| msgid "runtime.expected_n_got_n_args" | ||||
| msgstr "\"{method}\" was called with {received} arguments but it only has {expected}." | ||||
| 
 | ||||
| msgid "runtime.unsupported_array_type" | ||||
| msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead." | ||||
| 
 | ||||
| msgid "runtime.dialogue_balloon_missing_start_method" | ||||
| msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method." | ||||
|  | @ -0,0 +1,457 @@ | |||
| # | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: Dialogue Manager\n" | ||||
| "POT-Creation-Date: 2024-02-25 20:58\n" | ||||
| "PO-Revision-Date: 2024-02-25 20:58\n" | ||||
| "Last-Translator: you <you@example.com>\n" | ||||
| "Language-Team: Spanish <yourteam@example.com>\n" | ||||
| "Language: es\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=utf-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| 
 | ||||
| msgid "start_a_new_file" | ||||
| msgstr "Crear un nuevo archivo" | ||||
| 
 | ||||
| msgid "open_a_file" | ||||
| msgstr "Abrir un archivo" | ||||
| 
 | ||||
| msgid "open.open" | ||||
| msgstr "Abrir..." | ||||
| 
 | ||||
| msgid "open.no_recent_files" | ||||
| msgstr "No hay archivos recientes" | ||||
| 
 | ||||
| msgid "open.clear_recent_files" | ||||
| msgstr "Limpiar archivos recientes" | ||||
| 
 | ||||
| msgid "save_all_files" | ||||
| msgstr "Guardar todos los archivos" | ||||
| 
 | ||||
| msgid "test_dialogue" | ||||
| msgstr "Diálogo de prueba" | ||||
| 
 | ||||
| msgid "search_for_text" | ||||
| msgstr "Buscar texto" | ||||
| 
 | ||||
| msgid "insert" | ||||
| msgstr "Insertar" | ||||
| 
 | ||||
| msgid "translations" | ||||
| msgstr "Traducciones" | ||||
| 
 | ||||
| msgid "settings" | ||||
| msgstr "Ajustes" | ||||
| 
 | ||||
| msgid "show_support" | ||||
| msgstr "Contribuye con Dialogue Manager" | ||||
| 
 | ||||
| msgid "docs" | ||||
| msgstr "Docs" | ||||
| 
 | ||||
| msgid "insert.wave_bbcode" | ||||
| msgstr "BBCode ondulado" | ||||
| 
 | ||||
| msgid "insert.shake_bbcode" | ||||
| msgstr "BBCode agitado" | ||||
| 
 | ||||
| msgid "insert.typing_pause" | ||||
| msgstr "Pausa de escritura" | ||||
| 
 | ||||
| msgid "insert.typing_speed_change" | ||||
| msgstr "Cambiar la velocidad de escritura" | ||||
| 
 | ||||
| msgid "insert.auto_advance" | ||||
| msgstr "Avance automático" | ||||
| 
 | ||||
| msgid "insert.templates" | ||||
| msgstr "Plantillas" | ||||
| 
 | ||||
| msgid "insert.title" | ||||
| msgstr "Título" | ||||
| 
 | ||||
| msgid "insert.dialogue" | ||||
| msgstr "Diálogo" | ||||
| 
 | ||||
| msgid "insert.response" | ||||
| msgstr "Respuesta" | ||||
| 
 | ||||
| msgid "insert.random_lines" | ||||
| msgstr "Líneas aleatorias" | ||||
| 
 | ||||
| msgid "insert.random_text" | ||||
| msgstr "Texto aleatorio" | ||||
| 
 | ||||
| msgid "insert.actions" | ||||
| msgstr "Acciones" | ||||
| 
 | ||||
| msgid "insert.jump" | ||||
| msgstr "Ir al título" | ||||
| 
 | ||||
| msgid "insert.end_dialogue" | ||||
| msgstr "Finalizar diálogo" | ||||
| 
 | ||||
| msgid "generate_line_ids" | ||||
| msgstr "Generar IDs de línea" | ||||
| 
 | ||||
| msgid "save_characters_to_csv" | ||||
| msgstr "Guardar los nombres de los personajes en un archivo CSV..." | ||||
| 
 | ||||
| msgid "save_to_csv" | ||||
| msgstr "Guardar líneas en CSV..." | ||||
| 
 | ||||
| msgid "import_from_csv" | ||||
| msgstr "Importar cambios de línea desde CSV..." | ||||
| 
 | ||||
| msgid "confirm_close" | ||||
| msgstr "¿Guardar los cambios en '{path}'?" | ||||
| 
 | ||||
| msgid "confirm_close.save" | ||||
| msgstr "Guardar cambios" | ||||
| 
 | ||||
| msgid "confirm_close.discard" | ||||
| msgstr "Descartar" | ||||
| 
 | ||||
| msgid "buffer.save" | ||||
| msgstr "Guardar" | ||||
| 
 | ||||
| msgid "buffer.save_as" | ||||
| msgstr "Guardar como..." | ||||
| 
 | ||||
| msgid "buffer.close" | ||||
| msgstr "Cerrar" | ||||
| 
 | ||||
| msgid "buffer.close_all" | ||||
| msgstr "Cerrar todo" | ||||
| 
 | ||||
| msgid "buffer.close_other_files" | ||||
| msgstr "Cerrar otros archivos" | ||||
| 
 | ||||
| msgid "buffer.copy_file_path" | ||||
| msgstr "Copiar la ruta del archivo" | ||||
| 
 | ||||
| msgid "buffer.show_in_filesystem" | ||||
| msgstr "Mostrar en el sistema de archivos" | ||||
| 
 | ||||
| msgid "settings.invalid_test_scene" | ||||
| msgstr "\"{path}\" no extiende BaseDialogueTestScene." | ||||
| 
 | ||||
| msgid "settings.revert_to_default_test_scene" | ||||
| msgstr "Revertir a la escena de prueba por defecto" | ||||
| 
 | ||||
| msgid "settings.default_balloon_hint" | ||||
| msgstr "" | ||||
| "Globo personalizado para usar al llamar a \"DialogueManager.show_balloon()\"" | ||||
| 
 | ||||
| msgid "settings.revert_to_default_balloon" | ||||
| msgstr "Volver al globo predeterminado" | ||||
| 
 | ||||
| msgid "settings.default_balloon_path" | ||||
| msgstr "<globo de ejemplo>" | ||||
| 
 | ||||
| msgid "settings.autoload" | ||||
| msgstr "Autocarga" | ||||
| 
 | ||||
| msgid "settings.path" | ||||
| msgstr "Ruta" | ||||
| 
 | ||||
| msgid "settings.new_template" | ||||
| msgstr "Los nuevos archivos de diálogo empezarán con una plantilla" | ||||
| 
 | ||||
| msgid "settings.missing_keys" | ||||
| msgstr "Tratar las claves de traducción faltantes como errores" | ||||
| 
 | ||||
| msgid "settings.missing_keys_hint" | ||||
| msgstr "Si estás utilizando claves de traducción estáticas, tener esta opción habilitada te ayudará a encontrar cualquier línea a la que aún no le hayas añadido una clave." | ||||
| 
 | ||||
| msgid "settings.characters_translations" | ||||
| msgstr "Exportar nombres de personajes en archivos de traducción" | ||||
| 
 | ||||
| msgid "settings.wrap_long_lines" | ||||
| msgstr "Romper líneas largas" | ||||
| 
 | ||||
| msgid "settings.include_failed_responses" | ||||
| msgstr "Incluir respuestas con condiciones fallidas" | ||||
| 
 | ||||
| msgid "settings.ignore_missing_state_values" | ||||
| msgstr "Omitir errores de valores de estado faltantes (no recomendado)" | ||||
| 
 | ||||
| msgid "settings.custom_test_scene" | ||||
| msgstr "Escena de prueba personalizada (debe extender BaseDialogueTestScene)" | ||||
| 
 | ||||
| msgid "settings.default_csv_locale" | ||||
| msgstr "Localización CSV por defecto" | ||||
| 
 | ||||
| msgid "settings.states_shortcuts" | ||||
| msgstr "Atajos de teclado" | ||||
| 
 | ||||
| msgid "settings.states_message" | ||||
| msgstr "Si un autoload está habilitado aquí, puedes referirte a sus propiedades y métodos sin tener que usar su nombre." | ||||
| 
 | ||||
| msgid "settings.states_hint" | ||||
| msgstr "ie. En lugar de \"SomeState.some_property\" podría simplemente usar \"some_property\"" | ||||
| 
 | ||||
| msgid "settings.recompile_warning" | ||||
| msgstr "Cambiar estos ajustes obligará a recompilar todo el diálogo. Hazlo solo si sabes lo que estás haciendo." | ||||
| 
 | ||||
| msgid "settings.create_lines_for_responses_with_characters" | ||||
| msgstr "Crear línea de diálogo para respuestas con nombres de personajes dentro." | ||||
| 
 | ||||
| msgid "settings.open_in_external_editor" | ||||
| msgstr "Abrir archivos de diálogo en el editor externo" | ||||
| 
 | ||||
| msgid "settings.external_editor_warning" | ||||
| msgstr "Nota: El resaltado de sintaxis y la verificación detallada de errores no están soportados en editores externos." | ||||
| 
 | ||||
| msgid "settings.include_characters_in_translations" | ||||
| msgstr "Incluir nombres de personajes en las exportaciones de traducción" | ||||
| 
 | ||||
| msgid "settings.include_notes_in_translations" | ||||
| msgstr "Incluir notas (## comentarios) en las exportaciones de traducción" | ||||
| 
 | ||||
| msgid "n_of_n" | ||||
| msgstr "{index} de {total}" | ||||
| 
 | ||||
| msgid "search.previous" | ||||
| msgstr "Anterior" | ||||
| 
 | ||||
| msgid "search.next" | ||||
| msgstr "Siguiente" | ||||
| 
 | ||||
| msgid "search.match_case" | ||||
| msgstr "Coincidir mayúsculas/minúsculas" | ||||
| 
 | ||||
| msgid "search.toggle_replace" | ||||
| msgstr "Reemplazar" | ||||
| 
 | ||||
| msgid "search.replace_with" | ||||
| msgstr "Reemplazar con:" | ||||
| 
 | ||||
| msgid "search.replace" | ||||
| msgstr "Reemplazar" | ||||
| 
 | ||||
| msgid "search.replace_all" | ||||
| msgstr "Reemplazar todo" | ||||
| 
 | ||||
| msgid "files_list.filter" | ||||
| msgstr "Filtrar archivos" | ||||
| 
 | ||||
| msgid "titles_list.filter" | ||||
| msgstr "Filtrar títulos" | ||||
| 
 | ||||
| msgid "errors.key_not_found" | ||||
| msgstr "La tecla \"{key}\" no se encuentra." | ||||
| 
 | ||||
| msgid "errors.line_and_message" | ||||
| msgstr "Error en {line}, {column}: {message}" | ||||
| 
 | ||||
| msgid "errors_in_script" | ||||
| msgstr "Tienes errores en tu guion. Corrígelos y luego inténtalo de nuevo." | ||||
| 
 | ||||
| msgid "errors_with_build" | ||||
| msgstr "Debes corregir los errores de diálogo antes de poder ejecutar tu juego." | ||||
| 
 | ||||
| msgid "errors.import_errors" | ||||
| msgstr "Hay errores en este archivo importado." | ||||
| 
 | ||||
| msgid "errors.already_imported" | ||||
| msgstr "Archivo ya importado." | ||||
| 
 | ||||
| msgid "errors.duplicate_import" | ||||
| msgstr "Nombre de importación duplicado." | ||||
| 
 | ||||
| msgid "errors.unknown_using" | ||||
| msgstr "Autoload desconocida en la declaración de uso." | ||||
| 
 | ||||
| msgid "errors.empty_title" | ||||
| msgstr "Los títulos no pueden estar vacíos." | ||||
| 
 | ||||
| msgid "errors.duplicate_title" | ||||
| msgstr "Ya hay un título con ese nombre." | ||||
| 
 | ||||
| msgid "errors.nested_title" | ||||
| msgstr "Los títulos no pueden tener sangría." | ||||
| 
 | ||||
| msgid "errors.invalid_title_string" | ||||
| msgstr "Los títulos solo pueden contener caracteres alfanuméricos y números." | ||||
| 
 | ||||
| msgid "errors.invalid_title_number" | ||||
| msgstr "Los títulos no pueden empezar con un número." | ||||
| 
 | ||||
| msgid "errors.unknown_title" | ||||
| msgstr "Título desconocido." | ||||
| 
 | ||||
| msgid "errors.jump_to_invalid_title" | ||||
| msgstr "Este salto está apuntando a un título inválido." | ||||
| 
 | ||||
| msgid "errors.title_has_no_content" | ||||
| msgstr "Ese título no tiene contenido. Quizá cambiarlo a \"=> FIN\"." | ||||
| 
 | ||||
| msgid "errors.invalid_expression" | ||||
| msgstr "La expresión es inválida." | ||||
| 
 | ||||
| msgid "errors.unexpected_condition" | ||||
| msgstr "Condición inesperada." | ||||
| 
 | ||||
| msgid "errors.duplicate_id" | ||||
| msgstr "Este ID ya está en otra línea." | ||||
| 
 | ||||
| msgid "errors.missing_id" | ||||
| msgstr "Esta línea está sin ID." | ||||
| 
 | ||||
| msgid "errors.invalid_indentation" | ||||
| msgstr "Sangría no válida." | ||||
| 
 | ||||
| msgid "errors.condition_has_no_content" | ||||
| msgstr "Una línea de condición necesita una línea sangrada debajo de ella." | ||||
| 
 | ||||
| msgid "errors.incomplete_expression" | ||||
| msgstr "Expresión incompleta." | ||||
| 
 | ||||
| msgid "errors.invalid_expression_for_value" | ||||
| msgstr "Expresión no válida para valor." | ||||
| 
 | ||||
| msgid "errors.file_not_found" | ||||
| msgstr "Archivo no encontrado." | ||||
| 
 | ||||
| msgid "errors.unexpected_end_of_expression" | ||||
| msgstr "Fin de expresión inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_function" | ||||
| msgstr "Función inesperada." | ||||
| 
 | ||||
| msgid "errors.unexpected_bracket" | ||||
| msgstr "Corchete inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_closing_bracket" | ||||
| msgstr "Bracket de cierre inesperado." | ||||
| 
 | ||||
| msgid "errors.missing_closing_bracket" | ||||
| msgstr "Falta cerrar corchete." | ||||
| 
 | ||||
| msgid "errors.unexpected_operator" | ||||
| msgstr "Operador inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_comma" | ||||
| msgstr "Coma inesperada." | ||||
| 
 | ||||
| msgid "errors.unexpected_colon" | ||||
| msgstr "Dos puntos inesperados" | ||||
| 
 | ||||
| msgid "errors.unexpected_dot" | ||||
| msgstr "Punto inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_boolean" | ||||
| msgstr "Booleano inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_string" | ||||
| msgstr "String inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_number" | ||||
| msgstr "Número inesperado." | ||||
| 
 | ||||
| msgid "errors.unexpected_variable" | ||||
| msgstr "Variable inesperada." | ||||
| 
 | ||||
| msgid "errors.invalid_index" | ||||
| msgstr "Índice no válido." | ||||
| 
 | ||||
| msgid "errors.unexpected_assignment" | ||||
| msgstr "Asignación inesperada." | ||||
| 
 | ||||
| msgid "errors.unknown" | ||||
| msgstr "Sintaxis desconocida." | ||||
| 
 | ||||
| msgid "update.available" | ||||
| msgstr "v{version} disponible" | ||||
| 
 | ||||
| msgid "update.is_available_for_download" | ||||
| msgstr "¡La versión %s ya está disponible para su descarga!" | ||||
| 
 | ||||
| msgid "update.downloading" | ||||
| msgstr "Descargando..." | ||||
| 
 | ||||
| msgid "update.download_update" | ||||
| msgstr "Descargar actualización" | ||||
| 
 | ||||
| msgid "update.needs_reload" | ||||
| msgstr "El proyecto debe ser recargado para instalar la actualización." | ||||
| 
 | ||||
| msgid "update.reload_ok_button" | ||||
| msgstr "Recargar proyecto" | ||||
| 
 | ||||
| msgid "update.reload_cancel_button" | ||||
| msgstr "Hazlo más tarde" | ||||
| 
 | ||||
| msgid "update.reload_project" | ||||
| msgstr "Recargar proyecto" | ||||
| 
 | ||||
| msgid "update.release_notes" | ||||
| msgstr "Leer las notas de la versión" | ||||
| 
 | ||||
| msgid "update.success" | ||||
| msgstr "El Gestor de Diálogo ahora es v{versión}." | ||||
| 
 | ||||
| msgid "update.failed" | ||||
| msgstr "Hubo un problema al descargar la actualización." | ||||
| 
 | ||||
| msgid "runtime.no_resource" | ||||
| msgstr "Recurso de diálogo no proporcionado." | ||||
| 
 | ||||
| msgid "runtime.no_content" | ||||
| msgstr "\"{file_path}\" no tiene contenido." | ||||
| 
 | ||||
| msgid "runtime.errors" | ||||
| msgstr "Tienes {count} errores en tu diálogo de texto." | ||||
| 
 | ||||
| msgid "runtime.error_detail" | ||||
| msgstr "Línea {line}: {message}" | ||||
| 
 | ||||
| msgid "runtime.errors_see_details" | ||||
| msgstr "Tienes {count} errores en tu texto de diálogo. Consulta la salida para más detalles." | ||||
| 
 | ||||
| msgid "runtime.invalid_expression" | ||||
| msgstr "\"{expression}\" no es una expresión válida: {error}" | ||||
| 
 | ||||
| msgid "runtime.array_index_out_of_bounds" | ||||
| msgstr "Índice {index} fuera de los límites del array \"{array}\"." | ||||
| 
 | ||||
| msgid "runtime.left_hand_size_cannot_be_assigned_to" | ||||
| msgstr "El lado izquierdo de la expresión no se puede asignar." | ||||
| 
 | ||||
| msgid "runtime.key_not_found" | ||||
| msgstr "Clave \"{key}\" no encontrada en el diccionario \"{dictionary}\"" | ||||
| 
 | ||||
| msgid "runtime.property_not_found" | ||||
| msgstr "\"{property}\" no es una propiedad en ningún estado del juego ({states})." | ||||
| 
 | ||||
| msgid "runtime.property_not_found_missing_export" | ||||
| msgstr "\"{property}\" no es una propiedad en ningún estado del juego ({states}). Es posible que necesites añadir un decorador [Export]." | ||||
| 
 | ||||
| msgid "runtime.method_not_found" | ||||
| msgstr "\"{method}\" no es un método en ningún estado del juego ({states})" | ||||
| 
 | ||||
| msgid "runtime.signal_not_found" | ||||
| msgstr "\"{signal_name}\" no es una señal en ningún estado del juego ({states})" | ||||
| 
 | ||||
| msgid "runtime.method_not_callable" | ||||
| msgstr "\"{method}\" no es un método llamable en \"{object}\"" | ||||
| 
 | ||||
| msgid "runtime.unknown_operator" | ||||
| msgstr "Operador desconocido." | ||||
| 
 | ||||
| msgid "runtime.unknown_autoload" | ||||
| msgstr "\"{autoload}\" parece no ser un autoload válido." | ||||
| 
 | ||||
| msgid "runtime.something_went_wrong" | ||||
| msgstr "Algo salió mal." | ||||
| 
 | ||||
| msgid "runtime.expected_n_got_n_args" | ||||
| msgstr "El método \"{method}\" se llamó con {received} argumentos, pero solo tiene {expected}." | ||||
| 
 | ||||
| msgid "runtime.unsupported_array_type" | ||||
| msgstr "Array[{type}] no está soportado en mutaciones. Utiliza Array como tipo en su lugar." | ||||
| 
 | ||||
| msgid "runtime.dialogue_balloon_missing_start_method" | ||||
| msgstr "Tu globo de diálogo no tiene un método  \"start\" o \"Start\"." | ||||
|  | @ -23,6 +23,9 @@ msgstr "" | |||
| msgid "save_all_files" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "find_in_files" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "test_dialogue" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -38,6 +41,12 @@ msgstr "" | |||
| msgid "settings" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "sponsor" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "show_support" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "docs" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -122,9 +131,21 @@ msgstr "" | |||
| msgid "buffer.show_in_filesystem" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.invalid_test_scene" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.revert_to_default_test_scene" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.default_balloon_hint" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.revert_to_default_balloon" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.default_balloon_path" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.autoload" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -140,15 +161,24 @@ msgstr "" | |||
| msgid "settings.missing_keys_hint" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.characters_translations" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.wrap_long_lines" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.include_failed_responses" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.ignore_missing_state_values" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.custom_test_scene" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.default_csv_locale" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.states_shortcuts" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -158,9 +188,45 @@ msgstr "" | |||
| msgid "settings.states_hint" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.recompile_warning" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.create_lines_for_responses_with_characters" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.open_in_external_editor" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.external_editor_warning" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.include_characters_in_translations" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.include_notes_in_translations" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "settings.check_for_updates" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "n_of_n" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "search.find" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "search.find_all" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "search.placeholder" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "search.replace_placeholder" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "search.replace_selected" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "search.previous" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -188,6 +254,9 @@ msgstr "" | |||
| msgid "titles_list.filter" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "errors.key_not_found" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "errors.line_and_message" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -206,6 +275,9 @@ msgstr "" | |||
| msgid "errors.duplicate_import" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "errors.unknown_using" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "errors.empty_title" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -356,12 +428,6 @@ msgstr "" | |||
| msgid "runtime.invalid_expression" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.unsupported_array_method" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.unsupported_dictionary_method" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.array_index_out_of_bounds" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -374,6 +440,9 @@ msgstr "" | |||
| msgid "runtime.property_not_found" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.property_not_found_missing_export" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.method_not_found" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -386,5 +455,17 @@ msgstr "" | |||
| msgid "runtime.unknown_operator" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.unknown_autoload" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.something_went_wrong" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.expected_n_got_n_args" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.unsupported_array_type" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "runtime.dialogue_balloon_missing_start_method" | ||||
| msgstr "" | ||||
|  | @ -0,0 +1,408 @@ | |||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: Dialogue Manager\n" | ||||
| "POT-Creation-Date: \n" | ||||
| "PO-Revision-Date: \n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: penghao123456、憨憨羊の宇航鸽鸽\n" | ||||
| "Language: zh\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "X-Generator: Poedit 3.4\n" | ||||
| 
 | ||||
| msgid "start_a_new_file" | ||||
| msgstr "创建新文件" | ||||
| 
 | ||||
| msgid "open_a_file" | ||||
| msgstr "打开已有文件" | ||||
| 
 | ||||
| msgid "open.open" | ||||
| msgstr "打开……" | ||||
| 
 | ||||
| msgid "open.no_recent_files" | ||||
| msgstr "无历史记录" | ||||
| 
 | ||||
| msgid "open.clear_recent_files" | ||||
| msgstr "清空历史记录" | ||||
| 
 | ||||
| msgid "save_all_files" | ||||
| msgstr "保存所有文件" | ||||
| 
 | ||||
| msgid "test_dialogue" | ||||
| msgstr "测试对话" | ||||
| 
 | ||||
| msgid "search_for_text" | ||||
| msgstr "查找……" | ||||
| 
 | ||||
| msgid "insert" | ||||
| msgstr "插入" | ||||
| 
 | ||||
| msgid "translations" | ||||
| msgstr "翻译" | ||||
| 
 | ||||
| msgid "settings" | ||||
| msgstr "设置" | ||||
| 
 | ||||
| msgid "show_support" | ||||
| msgstr "支持 Dialogue Manager" | ||||
| 
 | ||||
| msgid "docs" | ||||
| msgstr "文档" | ||||
| 
 | ||||
| msgid "insert.wave_bbcode" | ||||
| msgstr "BBCode [lb]wave[rb]" | ||||
| 
 | ||||
| msgid "insert.shake_bbcode" | ||||
| msgstr "BBCode [lb]wave[rb]" | ||||
| 
 | ||||
| msgid "insert.typing_pause" | ||||
| msgstr "输入间隔" | ||||
| 
 | ||||
| msgid "insert.typing_speed_change" | ||||
| msgstr "输入速度变更" | ||||
| 
 | ||||
| msgid "insert.auto_advance" | ||||
| msgstr "自动切行" | ||||
| 
 | ||||
| msgid "insert.templates" | ||||
| msgstr "模板" | ||||
| 
 | ||||
| msgid "insert.title" | ||||
| msgstr "标题" | ||||
| 
 | ||||
| msgid "insert.dialogue" | ||||
| msgstr "对话" | ||||
| 
 | ||||
| msgid "insert.response" | ||||
| msgstr "回复选项" | ||||
| 
 | ||||
| msgid "insert.random_lines" | ||||
| msgstr "随机行" | ||||
| 
 | ||||
| msgid "insert.random_text" | ||||
| msgstr "随机文本" | ||||
| 
 | ||||
| msgid "insert.actions" | ||||
| msgstr "操作" | ||||
| 
 | ||||
| msgid "insert.jump" | ||||
| msgstr "标题间跳转" | ||||
| 
 | ||||
| msgid "insert.end_dialogue" | ||||
| msgstr "结束对话" | ||||
| 
 | ||||
| msgid "generate_line_ids" | ||||
| msgstr "生成行 ID" | ||||
| 
 | ||||
| msgid "save_to_csv" | ||||
| msgstr "生成 CSV" | ||||
| 
 | ||||
| msgid "import_from_csv" | ||||
| msgstr "从 CSV 导入" | ||||
| 
 | ||||
| msgid "confirm_close" | ||||
| msgstr "是否要保存到“{path}”?" | ||||
| 
 | ||||
| msgid "confirm_close.save" | ||||
| msgstr "保存" | ||||
| 
 | ||||
| msgid "confirm_close.discard" | ||||
| msgstr "不保存" | ||||
| 
 | ||||
| msgid "buffer.save" | ||||
| msgstr "保存" | ||||
| 
 | ||||
| msgid "buffer.save_as" | ||||
| msgstr "另存为……" | ||||
| 
 | ||||
| msgid "buffer.close" | ||||
| msgstr "关闭" | ||||
| 
 | ||||
| msgid "buffer.close_all" | ||||
| msgstr "全部关闭" | ||||
| 
 | ||||
| msgid "buffer.close_other_files" | ||||
| msgstr "关闭其他文件" | ||||
| 
 | ||||
| msgid "buffer.copy_file_path" | ||||
| msgstr "复制文件路径" | ||||
| 
 | ||||
| msgid "buffer.show_in_filesystem" | ||||
| msgstr "在 Godot 侧边栏中显示" | ||||
| 
 | ||||
| msgid "settings.revert_to_default_test_scene" | ||||
| msgstr "重置测试场景设定" | ||||
| 
 | ||||
| msgid "settings.autoload" | ||||
| msgstr "Autoload" | ||||
| 
 | ||||
| msgid "settings.path" | ||||
| msgstr "路径" | ||||
| 
 | ||||
| msgid "settings.new_template" | ||||
| msgstr "新建文件时自动插入模板" | ||||
| 
 | ||||
| msgid "settings.missing_keys" | ||||
| msgstr "将翻译键缺失视为错误" | ||||
| 
 | ||||
| msgid "settings.missing_keys_hint" | ||||
| msgstr "如果你使用静态键,这将会帮助你寻找未添加至翻译文件的键。" | ||||
| 
 | ||||
| msgid "settings.characters_translations" | ||||
| msgstr "在翻译文件中导出角色名。" | ||||
| 
 | ||||
| msgid "settings.wrap_long_lines" | ||||
| msgstr "自动折行" | ||||
| 
 | ||||
| msgid "settings.include_failed_responses" | ||||
| msgstr "在判断条件失败时仍显示回复选项" | ||||
| 
 | ||||
| msgid "settings.ignore_missing_state_values" | ||||
| msgstr "忽略全局变量缺失错误(不建议)" | ||||
| 
 | ||||
| msgid "settings.custom_test_scene" | ||||
| msgstr "自定义测试场景(必须继承自BaseDialogueTestScene)" | ||||
| 
 | ||||
| msgid "settings.default_csv_locale" | ||||
| msgstr "默认 CSV 区域格式" | ||||
| 
 | ||||
| msgid "settings.states_shortcuts" | ||||
| msgstr "全局变量映射" | ||||
| 
 | ||||
| msgid "settings.states_message" | ||||
| msgstr "当一个 Autoload 在这里被勾选,他的所有成员会被映射为全局变量。" | ||||
| 
 | ||||
| msgid "settings.states_hint" | ||||
| msgstr "比如,当你开启对于“Foo”的映射时,你可以将“Foo.bar”简写成“bar”。" | ||||
| 
 | ||||
| msgid "n_of_n" | ||||
| msgstr "第{index}个,共{total}个" | ||||
| 
 | ||||
| msgid "search.previous" | ||||
| msgstr "查找上一个" | ||||
| 
 | ||||
| msgid "search.next" | ||||
| msgstr "查找下一个" | ||||
| 
 | ||||
| msgid "search.match_case" | ||||
| msgstr "大小写敏感" | ||||
| 
 | ||||
| msgid "search.toggle_replace" | ||||
| msgstr "替换" | ||||
| 
 | ||||
| msgid "search.replace_with" | ||||
| msgstr "替换为" | ||||
| 
 | ||||
| msgid "search.replace" | ||||
| msgstr "替换" | ||||
| 
 | ||||
| msgid "search.replace_all" | ||||
| msgstr "全部替换" | ||||
| 
 | ||||
| msgid "files_list.filter" | ||||
| msgstr "查找文件" | ||||
| 
 | ||||
| msgid "titles_list.filter" | ||||
| msgstr "查找标题" | ||||
| 
 | ||||
| msgid "errors.key_not_found" | ||||
| msgstr "键“{key}”未找到" | ||||
| 
 | ||||
| msgid "errors.line_and_message" | ||||
| msgstr "第{line}行第{colume}列发生错误:{message}" | ||||
| 
 | ||||
| msgid "errors_in_script" | ||||
| msgstr "你的脚本中存在错误。请修复错误,然后重试。" | ||||
| 
 | ||||
| msgid "errors_with_build" | ||||
| msgstr "请先解决 Dialogue 中的错误。" | ||||
| 
 | ||||
| msgid "errors.import_errors" | ||||
| msgstr "被导入的文件存在问题。" | ||||
| 
 | ||||
| msgid "errors.already_imported" | ||||
| msgstr "文件已被导入。" | ||||
| 
 | ||||
| msgid "errors.duplicate_import" | ||||
| msgstr "导入名不能重复。" | ||||
| 
 | ||||
| msgid "errors.empty_title" | ||||
| msgstr "标题名不能为空。" | ||||
| 
 | ||||
| msgid "errors.duplicate_title" | ||||
| msgstr "标题名不能重复。" | ||||
| 
 | ||||
| msgid "errors.nested_title" | ||||
| msgstr "标题不能嵌套。" | ||||
| 
 | ||||
| msgid "errors.invalid_title_string" | ||||
| msgstr "标题名无效。" | ||||
| 
 | ||||
| msgid "errors.invalid_title_number" | ||||
| msgstr "标题不能以数字开始。" | ||||
| 
 | ||||
| msgid "errors.unknown_title" | ||||
| msgstr "标题未定义。" | ||||
| 
 | ||||
| msgid "errors.jump_to_invalid_title" | ||||
| msgstr "标题名无效。" | ||||
| 
 | ||||
| msgid "errors.title_has_no_content" | ||||
| msgstr "目标标题为空。请替换为“=> END”。" | ||||
| 
 | ||||
| msgid "errors.invalid_expression" | ||||
| msgstr "表达式无效。" | ||||
| 
 | ||||
| msgid "errors.unexpected_condition" | ||||
| msgstr "未知条件。" | ||||
| 
 | ||||
| msgid "errors.duplicate_id" | ||||
| msgstr "ID 重复。" | ||||
| 
 | ||||
| msgid "errors.missing_id" | ||||
| msgstr "ID 不存在。" | ||||
| 
 | ||||
| msgid "errors.invalid_indentation" | ||||
| msgstr "缩进无效。" | ||||
| 
 | ||||
| msgid "errors.condition_has_no_content" | ||||
| msgstr "条件下方不能为空。" | ||||
| 
 | ||||
| msgid "errors.incomplete_expression" | ||||
| msgstr "不完整的表达式。" | ||||
| 
 | ||||
| msgid "errors.invalid_expression_for_value" | ||||
| msgstr "无效的赋值表达式。" | ||||
| 
 | ||||
| msgid "errors.file_not_found" | ||||
| msgstr "文件不存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_end_of_expression" | ||||
| msgstr "表达式 end 不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_function" | ||||
| msgstr "函数不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_bracket" | ||||
| msgstr "方括号不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_closing_bracket" | ||||
| msgstr "方括号不应存在。" | ||||
| 
 | ||||
| msgid "errors.missing_closing_bracket" | ||||
| msgstr "闭方括号不存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_operator" | ||||
| msgstr "操作符不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_comma" | ||||
| msgstr "逗号不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_colon" | ||||
| msgstr "冒号不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_dot" | ||||
| msgstr "句号不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_boolean" | ||||
| msgstr "布尔值不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_string" | ||||
| msgstr "字符串不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_number" | ||||
| msgstr "数字不应存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_variable" | ||||
| msgstr "标识符不应存在。" | ||||
| 
 | ||||
| msgid "errors.invalid_index" | ||||
| msgstr "索引无效。" | ||||
| 
 | ||||
| msgid "errors.unexpected_assignment" | ||||
| msgstr "不应在条件判断中使用 = ,应使用 == 。" | ||||
| 
 | ||||
| msgid "errors.unknown" | ||||
| msgstr "语法错误。" | ||||
| 
 | ||||
| msgid "update.available" | ||||
| msgstr "v{version} 更新可用。" | ||||
| 
 | ||||
| msgid "update.is_available_for_download" | ||||
| msgstr "v%s 已经可以下载。" | ||||
| 
 | ||||
| msgid "update.downloading" | ||||
| msgstr "正在下载更新……" | ||||
| 
 | ||||
| msgid "update.download_update" | ||||
| msgstr "下载" | ||||
| 
 | ||||
| msgid "update.needs_reload" | ||||
| msgstr "需要重新加载项目以应用更新。" | ||||
| 
 | ||||
| msgid "update.reload_ok_button" | ||||
| msgstr "重新加载" | ||||
| 
 | ||||
| msgid "update.reload_cancel_button" | ||||
| msgstr "暂不重新加载" | ||||
| 
 | ||||
| msgid "update.reload_project" | ||||
| msgstr "重新加载" | ||||
| 
 | ||||
| msgid "update.release_notes" | ||||
| msgstr "查看发行注记" | ||||
| 
 | ||||
| msgid "update.success" | ||||
| msgstr "v{version} 已成功安装并应用。" | ||||
| 
 | ||||
| msgid "update.failed" | ||||
| msgstr "更新失败。" | ||||
| 
 | ||||
| msgid "runtime.no_resource" | ||||
| msgstr "找不到资源。" | ||||
| 
 | ||||
| msgid "runtime.no_content" | ||||
| msgstr "资源“{file_path}”为空。" | ||||
| 
 | ||||
| msgid "runtime.errors" | ||||
| msgstr "文件中存在{errrors}个错误。" | ||||
| 
 | ||||
| msgid "runtime.error_detail" | ||||
| msgstr "第{index}行:{message}" | ||||
| 
 | ||||
| msgid "runtime.errors_see_details" | ||||
| msgstr "文件中存在{errrors}个错误。请查看调试输出。" | ||||
| 
 | ||||
| msgid "runtime.invalid_expression" | ||||
| msgstr "表达式“{expression}”无效:{error}" | ||||
| 
 | ||||
| msgid "runtime.array_index_out_of_bounds" | ||||
| msgstr "数组索引“{index}”越界。(数组名:“{array}”)" | ||||
| 
 | ||||
| msgid "runtime.left_hand_size_cannot_be_assigned_to" | ||||
| msgstr "表达式左侧的变量无法被赋值。" | ||||
| 
 | ||||
| msgid "runtime.key_not_found" | ||||
| msgstr "键“{key}”在字典“{dictionary}”中不存在。" | ||||
| 
 | ||||
| msgid "runtime.property_not_found" | ||||
| msgstr "“{property}”不存在。(全局变量:{states})" | ||||
| 
 | ||||
| msgid "runtime.property_not_found_missing_export" | ||||
| msgstr "“{property}”不存在。(全局变量:{states})你可能需要添加一个修饰词 [Export]。" | ||||
| 
 | ||||
| msgid "runtime.method_not_found" | ||||
| msgstr "“{method}”不存在。(全局变量:{states})" | ||||
| 
 | ||||
| msgid "runtime.signal_not_found" | ||||
| msgstr "“{sighal_name}”不存在。(全局变量:{states})" | ||||
| 
 | ||||
| msgid "runtime.method_not_callable" | ||||
| msgstr "{method}不是对象“{object}”上的函数。" | ||||
| 
 | ||||
| msgid "runtime.unknown_operator" | ||||
| msgstr "未知操作符。" | ||||
| 
 | ||||
| msgid "runtime.something_went_wrong" | ||||
| msgstr "有什么出错了。" | ||||
|  | @ -0,0 +1,408 @@ | |||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: Dialogue Manager\n" | ||||
| "POT-Creation-Date: \n" | ||||
| "PO-Revision-Date: \n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: 憨憨羊の宇航鴿鴿\n" | ||||
| "Language: zh_TW\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "X-Generator: Poedit 3.4\n" | ||||
| 
 | ||||
| msgid "start_a_new_file" | ||||
| msgstr "創建新檔案" | ||||
| 
 | ||||
| msgid "open_a_file" | ||||
| msgstr "開啟已有檔案" | ||||
| 
 | ||||
| msgid "open.open" | ||||
| msgstr "開啟……" | ||||
| 
 | ||||
| msgid "open.no_recent_files" | ||||
| msgstr "無歷史記錄" | ||||
| 
 | ||||
| msgid "open.clear_recent_files" | ||||
| msgstr "清空歷史記錄" | ||||
| 
 | ||||
| msgid "save_all_files" | ||||
| msgstr "儲存所有檔案" | ||||
| 
 | ||||
| msgid "test_dialogue" | ||||
| msgstr "測試對話" | ||||
| 
 | ||||
| msgid "search_for_text" | ||||
| msgstr "搜尋……" | ||||
| 
 | ||||
| msgid "insert" | ||||
| msgstr "插入" | ||||
| 
 | ||||
| msgid "translations" | ||||
| msgstr "翻譯" | ||||
| 
 | ||||
| msgid "settings" | ||||
| msgstr "設定" | ||||
| 
 | ||||
| msgid "show_support" | ||||
| msgstr "支援 Dialogue Manager" | ||||
| 
 | ||||
| msgid "docs" | ||||
| msgstr "文檔" | ||||
| 
 | ||||
| msgid "insert.wave_bbcode" | ||||
| msgstr "BBCode [lb]wave[rb]" | ||||
| 
 | ||||
| msgid "insert.shake_bbcode" | ||||
| msgstr "BBCode [lb]wave[rb]" | ||||
| 
 | ||||
| msgid "insert.typing_pause" | ||||
| msgstr "輸入間隔" | ||||
| 
 | ||||
| msgid "insert.typing_speed_change" | ||||
| msgstr "輸入速度變更" | ||||
| 
 | ||||
| msgid "insert.auto_advance" | ||||
| msgstr "自動切行" | ||||
| 
 | ||||
| msgid "insert.templates" | ||||
| msgstr "模板" | ||||
| 
 | ||||
| msgid "insert.title" | ||||
| msgstr "標題" | ||||
| 
 | ||||
| msgid "insert.dialogue" | ||||
| msgstr "對話" | ||||
| 
 | ||||
| msgid "insert.response" | ||||
| msgstr "回覆選項" | ||||
| 
 | ||||
| msgid "insert.random_lines" | ||||
| msgstr "隨機行" | ||||
| 
 | ||||
| msgid "insert.random_text" | ||||
| msgstr "隨機文本" | ||||
| 
 | ||||
| msgid "insert.actions" | ||||
| msgstr "操作" | ||||
| 
 | ||||
| msgid "insert.jump" | ||||
| msgstr "標題間跳轉" | ||||
| 
 | ||||
| msgid "insert.end_dialogue" | ||||
| msgstr "結束對話" | ||||
| 
 | ||||
| msgid "generate_line_ids" | ||||
| msgstr "生成行 ID" | ||||
| 
 | ||||
| msgid "save_to_csv" | ||||
| msgstr "生成 CSV" | ||||
| 
 | ||||
| msgid "import_from_csv" | ||||
| msgstr "從 CSV 匯入" | ||||
| 
 | ||||
| msgid "confirm_close" | ||||
| msgstr "是否要儲存到“{path}”?" | ||||
| 
 | ||||
| msgid "confirm_close.save" | ||||
| msgstr "儲存" | ||||
| 
 | ||||
| msgid "confirm_close.discard" | ||||
| msgstr "不儲存" | ||||
| 
 | ||||
| msgid "buffer.save" | ||||
| msgstr "儲存" | ||||
| 
 | ||||
| msgid "buffer.save_as" | ||||
| msgstr "儲存爲……" | ||||
| 
 | ||||
| msgid "buffer.close" | ||||
| msgstr "關閉" | ||||
| 
 | ||||
| msgid "buffer.close_all" | ||||
| msgstr "全部關閉" | ||||
| 
 | ||||
| msgid "buffer.close_other_files" | ||||
| msgstr "關閉其他檔案" | ||||
| 
 | ||||
| msgid "buffer.copy_file_path" | ||||
| msgstr "複製檔案位置" | ||||
| 
 | ||||
| msgid "buffer.show_in_filesystem" | ||||
| msgstr "在 Godot 側邊欄中顯示" | ||||
| 
 | ||||
| msgid "settings.revert_to_default_test_scene" | ||||
| msgstr "重置測試場景設定" | ||||
| 
 | ||||
| msgid "settings.autoload" | ||||
| msgstr "Autoload" | ||||
| 
 | ||||
| msgid "settings.path" | ||||
| msgstr "路徑" | ||||
| 
 | ||||
| msgid "settings.new_template" | ||||
| msgstr "新建檔案時自動插入模板" | ||||
| 
 | ||||
| msgid "settings.missing_keys" | ||||
| msgstr "將翻譯鍵缺失視爲錯誤" | ||||
| 
 | ||||
| msgid "settings.missing_keys_hint" | ||||
| msgstr "如果你使用靜態鍵,這將會幫助你尋找未添加至翻譯檔案的鍵。" | ||||
| 
 | ||||
| msgid "settings.wrap_long_lines" | ||||
| msgstr "自動折行" | ||||
| 
 | ||||
| msgid "settings.characters_translations" | ||||
| msgstr "在翻譯檔案中匯出角色名。" | ||||
| 
 | ||||
| msgid "settings.include_failed_responses" | ||||
| msgstr "在判斷條件失敗時仍顯示回復選項" | ||||
| 
 | ||||
| msgid "settings.ignore_missing_state_values" | ||||
| msgstr "忽略全局變量缺失錯誤(不建議)" | ||||
| 
 | ||||
| msgid "settings.custom_test_scene" | ||||
| msgstr "自訂測試場景(必須繼承自BaseDialogueTestScene)" | ||||
| 
 | ||||
| msgid "settings.default_csv_locale" | ||||
| msgstr "預設 CSV 區域格式" | ||||
| 
 | ||||
| msgid "settings.states_shortcuts" | ||||
| msgstr "全局變量映射" | ||||
| 
 | ||||
| msgid "settings.states_message" | ||||
| msgstr "當一個 Autoload 在這裏被勾選,他的所有成員會被映射爲全局變量。" | ||||
| 
 | ||||
| msgid "settings.states_hint" | ||||
| msgstr "比如,當你開啓對於“Foo”的映射時,你可以將“Foo.bar”簡寫成“bar”。" | ||||
| 
 | ||||
| msgid "n_of_n" | ||||
| msgstr "第{index}個,共{total}個" | ||||
| 
 | ||||
| msgid "search.previous" | ||||
| msgstr "搜尋上一個" | ||||
| 
 | ||||
| msgid "search.next" | ||||
| msgstr "搜尋下一個" | ||||
| 
 | ||||
| msgid "search.match_case" | ||||
| msgstr "大小寫敏感" | ||||
| 
 | ||||
| msgid "search.toggle_replace" | ||||
| msgstr "替換" | ||||
| 
 | ||||
| msgid "search.replace_with" | ||||
| msgstr "替換爲" | ||||
| 
 | ||||
| msgid "search.replace" | ||||
| msgstr "替換" | ||||
| 
 | ||||
| msgid "search.replace_all" | ||||
| msgstr "全部替換" | ||||
| 
 | ||||
| msgid "files_list.filter" | ||||
| msgstr "搜尋檔案" | ||||
| 
 | ||||
| msgid "titles_list.filter" | ||||
| msgstr "搜尋標題" | ||||
| 
 | ||||
| msgid "errors.key_not_found" | ||||
| msgstr "鍵“{key}”未找到" | ||||
| 
 | ||||
| msgid "errors.line_and_message" | ||||
| msgstr "第{line}行第{colume}列發生錯誤:{message}" | ||||
| 
 | ||||
| msgid "errors_in_script" | ||||
| msgstr "你的腳本中存在錯誤。請修復錯誤,然後重試。" | ||||
| 
 | ||||
| msgid "errors_with_build" | ||||
| msgstr "請先解決 Dialogue 中的錯誤。" | ||||
| 
 | ||||
| msgid "errors.import_errors" | ||||
| msgstr "被匯入的檔案存在問題。" | ||||
| 
 | ||||
| msgid "errors.already_imported" | ||||
| msgstr "檔案已被匯入。" | ||||
| 
 | ||||
| msgid "errors.duplicate_import" | ||||
| msgstr "匯入名不能重複。" | ||||
| 
 | ||||
| msgid "errors.empty_title" | ||||
| msgstr "標題名不能爲空。" | ||||
| 
 | ||||
| msgid "errors.duplicate_title" | ||||
| msgstr "標題名不能重複。" | ||||
| 
 | ||||
| msgid "errors.nested_title" | ||||
| msgstr "標題不能嵌套。" | ||||
| 
 | ||||
| msgid "errors.invalid_title_string" | ||||
| msgstr "標題名無效。" | ||||
| 
 | ||||
| msgid "errors.invalid_title_number" | ||||
| msgstr "標題不能以數字開始。" | ||||
| 
 | ||||
| msgid "errors.unknown_title" | ||||
| msgstr "標題未定義。" | ||||
| 
 | ||||
| msgid "errors.jump_to_invalid_title" | ||||
| msgstr "標題名無效。" | ||||
| 
 | ||||
| msgid "errors.title_has_no_content" | ||||
| msgstr "目標標題爲空。請替換爲“=> END”。" | ||||
| 
 | ||||
| msgid "errors.invalid_expression" | ||||
| msgstr "表達式無效。" | ||||
| 
 | ||||
| msgid "errors.unexpected_condition" | ||||
| msgstr "未知條件。" | ||||
| 
 | ||||
| msgid "errors.duplicate_id" | ||||
| msgstr "ID 重複。" | ||||
| 
 | ||||
| msgid "errors.missing_id" | ||||
| msgstr "ID 不存在。" | ||||
| 
 | ||||
| msgid "errors.invalid_indentation" | ||||
| msgstr "縮進無效。" | ||||
| 
 | ||||
| msgid "errors.condition_has_no_content" | ||||
| msgstr "條件下方不能爲空。" | ||||
| 
 | ||||
| msgid "errors.incomplete_expression" | ||||
| msgstr "不完整的表達式。" | ||||
| 
 | ||||
| msgid "errors.invalid_expression_for_value" | ||||
| msgstr "無效的賦值表達式。" | ||||
| 
 | ||||
| msgid "errors.file_not_found" | ||||
| msgstr "檔案不存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_end_of_expression" | ||||
| msgstr "表達式 end 不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_function" | ||||
| msgstr "函數不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_bracket" | ||||
| msgstr "方括號不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_closing_bracket" | ||||
| msgstr "方括號不應存在。" | ||||
| 
 | ||||
| msgid "errors.missing_closing_bracket" | ||||
| msgstr "閉方括號不存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_operator" | ||||
| msgstr "操作符不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_comma" | ||||
| msgstr "逗號不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_colon" | ||||
| msgstr "冒號不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_dot" | ||||
| msgstr "句號不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_boolean" | ||||
| msgstr "布爾值不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_string" | ||||
| msgstr "字符串不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_number" | ||||
| msgstr "數字不應存在。" | ||||
| 
 | ||||
| msgid "errors.unexpected_variable" | ||||
| msgstr "標識符不應存在。" | ||||
| 
 | ||||
| msgid "errors.invalid_index" | ||||
| msgstr "索引無效。" | ||||
| 
 | ||||
| msgid "errors.unexpected_assignment" | ||||
| msgstr "不應在條件判斷中使用 = ,應使用 == 。" | ||||
| 
 | ||||
| msgid "errors.unknown" | ||||
| msgstr "語法錯誤。" | ||||
| 
 | ||||
| msgid "update.available" | ||||
| msgstr "v{version} 更新可用。" | ||||
| 
 | ||||
| msgid "update.is_available_for_download" | ||||
| msgstr "v%s 已經可以下載。" | ||||
| 
 | ||||
| msgid "update.downloading" | ||||
| msgstr "正在下載更新……" | ||||
| 
 | ||||
| msgid "update.download_update" | ||||
| msgstr "下載" | ||||
| 
 | ||||
| msgid "update.needs_reload" | ||||
| msgstr "需要重新加載項目以套用更新。" | ||||
| 
 | ||||
| msgid "update.reload_ok_button" | ||||
| msgstr "重新加載" | ||||
| 
 | ||||
| msgid "update.reload_cancel_button" | ||||
| msgstr "暫不重新加載" | ||||
| 
 | ||||
| msgid "update.reload_project" | ||||
| msgstr "重新加載" | ||||
| 
 | ||||
| msgid "update.release_notes" | ||||
| msgstr "查看發行註記" | ||||
| 
 | ||||
| msgid "update.success" | ||||
| msgstr "v{version} 已成功安裝並套用。" | ||||
| 
 | ||||
| msgid "update.failed" | ||||
| msgstr "更新失敗。" | ||||
| 
 | ||||
| msgid "runtime.no_resource" | ||||
| msgstr "找不到資源。" | ||||
| 
 | ||||
| msgid "runtime.no_content" | ||||
| msgstr "資源“{file_path}”爲空。" | ||||
| 
 | ||||
| msgid "runtime.errors" | ||||
| msgstr "檔案中存在{errrors}個錯誤。" | ||||
| 
 | ||||
| msgid "runtime.error_detail" | ||||
| msgstr "第{index}行:{message}" | ||||
| 
 | ||||
| msgid "runtime.errors_see_details" | ||||
| msgstr "檔案中存在{errrors}個錯誤。請查看調試輸出。" | ||||
| 
 | ||||
| msgid "runtime.invalid_expression" | ||||
| msgstr "表達式“{expression}”無效:{error}" | ||||
| 
 | ||||
| msgid "runtime.array_index_out_of_bounds" | ||||
| msgstr "數組索引“{index}”越界。(數組名:“{array}”)" | ||||
| 
 | ||||
| msgid "runtime.left_hand_size_cannot_be_assigned_to" | ||||
| msgstr "表達式左側的變量無法被賦值。" | ||||
| 
 | ||||
| msgid "runtime.key_not_found" | ||||
| msgstr "鍵“{key}”在字典“{dictionary}”中不存在。" | ||||
| 
 | ||||
| msgid "runtime.property_not_found" | ||||
| msgstr "“{property}”不存在。(全局變量:{states})" | ||||
| 
 | ||||
| msgid "runtime.method_not_found" | ||||
| msgstr "“{method}”不存在。(全局變量:{states})" | ||||
| 
 | ||||
| msgid "runtime.signal_not_found" | ||||
| msgstr "“{sighal_name}”不存在。(全局變量:{states})" | ||||
| 
 | ||||
| msgid "runtime.property_not_found_missing_export" | ||||
| msgstr "“{property}”不存在。(全局變量:{states})你可能需要添加一個修飾詞 [Export]。" | ||||
| 
 | ||||
| msgid "runtime.method_not_callable" | ||||
| msgstr "{method}不是對象“{object}”上的函數。" | ||||
| 
 | ||||
| msgid "runtime.unknown_operator" | ||||
| msgstr "未知操作符。" | ||||
| 
 | ||||
| msgid "runtime.something_went_wrong" | ||||
| msgstr "有什麼出錯了。" | ||||
|  | @ -3,5 +3,5 @@ | |||
| name="Dialogue Manager" | ||||
| description="A simple but powerful branching dialogue system" | ||||
| author="Nathan Hoad" | ||||
| version="2.16.2" | ||||
| version="2.38.0" | ||||
| script="plugin.gd" | ||||
|  |  | |||
|  | @ -2,63 +2,82 @@ | |||
| extends EditorPlugin | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueImportPlugin = preload("res://addons/dialogue_manager/import_plugin.gd") | ||||
| const DialogueTranslationParserPlugin = preload("res://addons/dialogue_manager/editor_translation_parser_plugin.gd") | ||||
| const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd") | ||||
| const MainView = preload("res://addons/dialogue_manager/views/main_view.tscn") | ||||
| const DialogueConstants = preload("./constants.gd") | ||||
| const DialogueImportPlugin = preload("./import_plugin.gd") | ||||
| const DialogueTranslationParserPlugin = preload("./editor_translation_parser_plugin.gd") | ||||
| const DialogueSettings = preload("./settings.gd") | ||||
| const DialogueCache = preload("./components/dialogue_cache.gd") | ||||
| const MainView = preload("./views/main_view.tscn") | ||||
| 
 | ||||
| 
 | ||||
| var import_plugin: DialogueImportPlugin | ||||
| var translation_parser_plugin: DialogueTranslationParserPlugin | ||||
| var main_view | ||||
| 
 | ||||
| var dialogue_file_cache: Dictionary = {} | ||||
| var dialogue_cache: DialogueCache | ||||
| 
 | ||||
| 
 | ||||
| func _enter_tree() -> void: | ||||
| 	add_autoload_singleton("DialogueManager", "res://addons/dialogue_manager/dialogue_manager.gd") | ||||
| 	add_custom_type("DialogueLabel", "RichTextLabel", preload("res://addons/dialogue_manager/dialogue_label.gd"), _get_plugin_icon()) | ||||
| 	 | ||||
| 	add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd") | ||||
| 
 | ||||
| 	if Engine.is_editor_hint(): | ||||
| 		Engine.set_meta("DialogueManagerPlugin", self) | ||||
| 
 | ||||
| 		DialogueSettings.prepare() | ||||
| 		 | ||||
| 
 | ||||
| 		import_plugin = DialogueImportPlugin.new() | ||||
| 		import_plugin.editor_plugin = self | ||||
| 		add_import_plugin(import_plugin) | ||||
| 		 | ||||
| 
 | ||||
| 		translation_parser_plugin = DialogueTranslationParserPlugin.new() | ||||
| 		add_translation_parser_plugin(translation_parser_plugin) | ||||
| 		 | ||||
| 
 | ||||
| 		main_view = MainView.instantiate() | ||||
| 		main_view.editor_plugin = self | ||||
| 		get_editor_interface().get_editor_main_screen().add_child(main_view) | ||||
| 		_make_visible(false) | ||||
| 		 | ||||
| 		update_dialogue_file_cache() | ||||
| 		get_editor_interface().get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed) | ||||
| 
 | ||||
| 		dialogue_cache = DialogueCache.new() | ||||
| 		main_view.add_child(dialogue_cache) | ||||
| 		Engine.set_meta("DialogueCache", dialogue_cache) | ||||
| 
 | ||||
| 		_update_localization() | ||||
| 
 | ||||
| 		get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved) | ||||
| 		get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed) | ||||
| 		 | ||||
| 
 | ||||
| 		add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon) | ||||
| 
 | ||||
| 		# Prevent the project from showing as unsaved even though it was only just opened | ||||
| 		if Engine.get_physics_frames() == 0: | ||||
| 			var timer: Timer = Timer.new() | ||||
| 			var suppress_unsaved_marker: Callable | ||||
| 			suppress_unsaved_marker = func(): | ||||
| 				if Engine.get_frames_per_second() >= 10: | ||||
| 					timer.stop() | ||||
| 					get_editor_interface().save_all_scenes() | ||||
| 					timer.queue_free() | ||||
| 			timer.timeout.connect(suppress_unsaved_marker) | ||||
| 			add_child(timer) | ||||
| 			timer.start(0.1) | ||||
| 
 | ||||
| 
 | ||||
| func _exit_tree() -> void: | ||||
| 	remove_autoload_singleton("DialogueManager") | ||||
| 	remove_custom_type("DialogueLabel") | ||||
| 	 | ||||
| 
 | ||||
| 	remove_import_plugin(import_plugin) | ||||
| 	import_plugin = null | ||||
| 	 | ||||
| 
 | ||||
| 	remove_translation_parser_plugin(translation_parser_plugin) | ||||
| 	translation_parser_plugin = null | ||||
| 	 | ||||
| 
 | ||||
| 	if is_instance_valid(main_view): | ||||
| 		main_view.queue_free() | ||||
| 	 | ||||
| 	get_editor_interface().get_resource_filesystem().filesystem_changed.disconnect(_on_filesystem_changed) | ||||
| 
 | ||||
| 	Engine.remove_meta("DialogueManagerPlugin") | ||||
| 	Engine.remove_meta("DialogueCache") | ||||
| 
 | ||||
| 	get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved) | ||||
| 	 | ||||
| 	get_editor_interface().get_file_system_dock().file_removed.disconnect(_on_file_removed) | ||||
| 
 | ||||
| 	remove_tool_menu_item("Create copy of dialogue example balloon...") | ||||
| 
 | ||||
| 
 | ||||
|  | @ -76,10 +95,19 @@ func _get_plugin_name() -> String: | |||
| 
 | ||||
| 
 | ||||
| func _get_plugin_icon() -> Texture2D: | ||||
| 	return load("res://addons/dialogue_manager/assets/icon.svg") | ||||
| 	return load(get_plugin_path() + "/assets/icon.svg") | ||||
| 
 | ||||
| 
 | ||||
| func _handles(object) -> bool: | ||||
| 	var editor_settings: EditorSettings = get_editor_interface().get_editor_settings() | ||||
| 	var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path") | ||||
| 	var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != "" | ||||
| 	if object is DialogueResource and use_external_editor and DialogueSettings.get_user_value("open_in_external_editor", false): | ||||
| 		var project_path: String = ProjectSettings.globalize_path("res://") | ||||
| 		var file_path: String = ProjectSettings.globalize_path(object.resource_path) | ||||
| 		OS.create_process(external_editor, [project_path, file_path]) | ||||
| 		return false | ||||
| 
 | ||||
| 	return object is DialogueResource | ||||
| 
 | ||||
| 
 | ||||
|  | @ -91,71 +119,62 @@ func _edit(object) -> void: | |||
| func _apply_changes() -> void: | ||||
| 	if is_instance_valid(main_view): | ||||
| 		main_view.apply_changes() | ||||
| 		_update_localization() | ||||
| 
 | ||||
| 
 | ||||
| func _build() -> bool: | ||||
| 	# If this is the dotnet Godot then we need to check if the solution file exists | ||||
| 	if ProjectSettings.has_setting("dotnet/project/solution_directory"): | ||||
| 		var directory: String = ProjectSettings.get("dotnet/project/solution_directory") | ||||
| 		var file_name: String = ProjectSettings.get("dotnet/project/assembly_name") | ||||
| 		var has_dotnet_solution: bool = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name]) | ||||
| 		DialogueSettings.set_user_value("has_dotnet_solution", has_dotnet_solution) | ||||
| 
 | ||||
| 	# Ignore errors in other files if we are just running the test scene | ||||
| 	if DialogueSettings.get_user_value("is_running_test_scene", true): return true | ||||
| 	 | ||||
| 	var can_build: bool = true | ||||
| 	var is_first_file: bool = true | ||||
| 	for dialogue_file in dialogue_file_cache.values(): | ||||
| 		if dialogue_file.errors.size() > 0: | ||||
| 			# Open the first file | ||||
| 			if is_first_file: | ||||
| 				get_editor_interface().edit_resource(load(dialogue_file.path)) | ||||
| 				main_view.show_build_error_dialog() | ||||
| 				is_first_file = false | ||||
| 			push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path]) | ||||
| 			can_build = false | ||||
| 	return can_build | ||||
| 
 | ||||
| 	if dialogue_cache != null: | ||||
| 		var files_with_errors = dialogue_cache.get_files_with_errors() | ||||
| 		if files_with_errors.size() > 0: | ||||
| 			for dialogue_file in files_with_errors: | ||||
| 				push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path]) | ||||
| 			get_editor_interface().edit_resource(load(files_with_errors[0].path)) | ||||
| 			main_view.show_build_error_dialog() | ||||
| 			return false | ||||
| 
 | ||||
| 	return true | ||||
| 
 | ||||
| 
 | ||||
| ## Keep track of known files and their dependencies | ||||
| func add_to_dialogue_file_cache(path: String, resource_path: String, parse_results: DialogueManagerParseResult) -> void: | ||||
| 	dialogue_file_cache[path] = { | ||||
| 		path = path, | ||||
| 		resource_path = resource_path, | ||||
| 		dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path), | ||||
| 		errors = [] | ||||
| 	} | ||||
| 	 | ||||
| 	save_dialogue_cache() | ||||
| 	recompile_dependent_files(path) | ||||
| ## Get the current version | ||||
| func get_version() -> String: | ||||
| 	var config: ConfigFile = ConfigFile.new() | ||||
| 	config.load(get_plugin_path() + "/plugin.cfg") | ||||
| 	return config.get_value("plugin", "version") | ||||
| 
 | ||||
| 
 | ||||
| ## Keep track of compile errors | ||||
| func add_errors_to_dialogue_file_cache(path: String, errors: Array[Dictionary]) -> void: | ||||
| 	if dialogue_file_cache.has(path): | ||||
| 		dialogue_file_cache[path]["errors"] = errors | ||||
| 	else: | ||||
| 		dialogue_file_cache[path] = {  | ||||
| 			path = path, | ||||
| 			errors = errors  | ||||
| 		} | ||||
| 		 | ||||
| 	save_dialogue_cache() | ||||
| 	recompile_dependent_files(path) | ||||
| ## Get the current path of the plugin | ||||
| func get_plugin_path() -> String: | ||||
| 	return get_script().resource_path.get_base_dir() | ||||
| 
 | ||||
| 
 | ||||
| ## Update references to a moved file | ||||
| func update_import_paths(from_path: String, to_path: String) -> void: | ||||
| 	# Update its own reference in the cache | ||||
| 	if dialogue_file_cache.has(from_path): | ||||
| 		dialogue_file_cache[to_path] = dialogue_file_cache[from_path].duplicate() | ||||
| 		dialogue_file_cache.erase(from_path) | ||||
| 	 | ||||
| 	dialogue_cache.move_file_path(from_path, to_path) | ||||
| 
 | ||||
| 	# Reopen the file if it's already open | ||||
| 	if main_view.current_file_path == from_path: | ||||
| 		main_view.current_file_path = "" | ||||
| 		main_view.open_file(to_path) | ||||
| 	 | ||||
| 		if to_path == "": | ||||
| 			main_view.close_file(from_path) | ||||
| 		else: | ||||
| 			main_view.current_file_path = "" | ||||
| 			main_view.open_file(to_path) | ||||
| 
 | ||||
| 	# Update any other files that import the moved file | ||||
| 	var dependents = dialogue_file_cache.values().filter(func(d): return from_path in d.dependencies) | ||||
| 	var dependents = dialogue_cache.get_files_with_dependency(from_path) | ||||
| 	for dependent in dependents: | ||||
| 		dependent.dependencies.erase(from_path) | ||||
| 		dependent.dependencies.remove_at(dependent.dependencies.find(from_path)) | ||||
| 		dependent.dependencies.append(to_path) | ||||
| 		 | ||||
| 
 | ||||
| 		# Update the live buffer | ||||
| 		if main_view.current_file_path == dependent.path: | ||||
| 			main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path) | ||||
|  | @ -164,85 +183,35 @@ func update_import_paths(from_path: String, to_path: String) -> void: | |||
| 		# Open the file and update the path | ||||
| 		var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ) | ||||
| 		var text = file.get_as_text().replace(from_path, to_path) | ||||
| 		 | ||||
| 		file.close() | ||||
| 
 | ||||
| 		file = FileAccess.open(dependent.path, FileAccess.WRITE) | ||||
| 		file.store_string(text) | ||||
| 	 | ||||
| 	save_dialogue_cache() | ||||
| 		file.close() | ||||
| 
 | ||||
| 
 | ||||
| ## Rebuild any files that depend on this path | ||||
| func recompile_dependent_files(path: String) -> void: | ||||
| 	# Rebuild any files that depend on this one | ||||
| 	var dependents = dialogue_file_cache.values().filter(func(d): return path in d.dependencies) | ||||
| 	for dependent in dependents: | ||||
| 		if dependent.has("path") and dependent.has("resource_path"): | ||||
| 			import_plugin.compile_file(dependent.path, dependent.resource_path, false) | ||||
| func _update_localization() -> void: | ||||
| 	var dialogue_files = dialogue_cache.get_files() | ||||
| 
 | ||||
| 
 | ||||
| ## Make sure the cache points to real files | ||||
| func update_dialogue_file_cache() -> void: | ||||
| 	var cache: Dictionary = {} | ||||
| 	 | ||||
| 	# Open our cache file if it exists | ||||
| 	if FileAccess.file_exists(DialogueConstants.CACHE_PATH): | ||||
| 		var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.READ) | ||||
| 		cache = JSON.parse_string(file.get_as_text()) | ||||
| 	 | ||||
| 	# Scan for dialogue files | ||||
| 	var current_files: PackedStringArray = _get_dialogue_files_in_filesystem() | ||||
| 	 | ||||
| 	# Add any files to POT generation | ||||
| 	# Add any new files to POT generation | ||||
| 	var files_for_pot: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations_pot_files", []) | ||||
| 	var files_for_pot_changed: bool = false | ||||
| 	for path in current_files: | ||||
| 	for path in dialogue_files: | ||||
| 		if not files_for_pot.has(path): | ||||
| 			files_for_pot.append(path) | ||||
| 			files_for_pot_changed = true | ||||
| 	 | ||||
| 	# Remove any files that don't exist any more | ||||
| 	for path in cache.keys(): | ||||
| 		if not path in current_files: | ||||
| 			cache.erase(path) | ||||
| 			DialogueSettings.remove_recent_file(path) | ||||
| 			 | ||||
| 			# Remove missing files from POT generation | ||||
| 			if files_for_pot.has(path): | ||||
| 				files_for_pot.remove_at(files_for_pot.find(path)) | ||||
| 				files_for_pot_changed = true | ||||
| 	 | ||||
| 
 | ||||
| 	# Remove any POT references that don't exist any more | ||||
| 	for i in range(files_for_pot.size() - 1, -1, -1): | ||||
| 		var file_for_pot: String = files_for_pot[i] | ||||
| 		if file_for_pot.get_extension() == "dialogue" and not dialogue_files.has(file_for_pot): | ||||
| 			files_for_pot.remove_at(i) | ||||
| 			files_for_pot_changed = true | ||||
| 
 | ||||
| 	# Update project settings if POT changed | ||||
| 	if files_for_pot_changed: | ||||
| 		ProjectSettings.set_setting("internationalization/locale/translations_pot_files", files_for_pot) | ||||
| 		ProjectSettings.save() | ||||
| 	 | ||||
| 	dialogue_file_cache = cache | ||||
| 
 | ||||
| 
 | ||||
| ## Persist the cache | ||||
| func save_dialogue_cache() -> void: | ||||
| 	var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.WRITE) | ||||
| 	file.store_string(JSON.stringify(dialogue_file_cache)) | ||||
| 
 | ||||
| 
 | ||||
| ## Recursively find any dialogue files in a directory | ||||
| func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray: | ||||
| 	var files: PackedStringArray = [] | ||||
| 	 | ||||
| 	if DirAccess.dir_exists_absolute(path): | ||||
| 		var dir = DirAccess.open(path) | ||||
| 		dir.list_dir_begin() | ||||
| 		var file_name = dir.get_next() | ||||
| 		while file_name != "": | ||||
| 			var file_path: String = (path + "/" + file_name).simplify_path() | ||||
| 			if dir.current_is_dir(): | ||||
| 				if not file_name in [".godot", ".tmp"]: | ||||
| 					files.append_array(_get_dialogue_files_in_filesystem(file_path)) | ||||
| 			elif file_name.get_extension() == "dialogue": | ||||
| 				files.append(file_path) | ||||
| 			file_name = dir.get_next() | ||||
| 	 | ||||
| 	return files | ||||
| 
 | ||||
| 
 | ||||
| ### Callbacks | ||||
|  | @ -257,27 +226,36 @@ func _copy_dialogue_balloon() -> void: | |||
| 	directory_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR | ||||
| 	directory_dialog.min_size = Vector2(600, 500) * scale | ||||
| 	directory_dialog.dir_selected.connect(func(path): | ||||
| 		var file: FileAccess = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.tscn", FileAccess.READ) | ||||
| 		var file_contents: String = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd") | ||||
| 		file = FileAccess.open(path + "/balloon.tscn", FileAccess.WRITE) | ||||
| 		var plugin_path: String = get_plugin_path() | ||||
| 
 | ||||
| 		var is_dotnet: bool = DialogueSettings.has_dotnet_solution() | ||||
| 		var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn") | ||||
| 		var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd") | ||||
| 
 | ||||
| 		# Copy the balloon scene file and change the script reference | ||||
| 		var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 | ||||
| 		var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" | ||||
| 		var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd" | ||||
| 		var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_file_name, FileAccess.READ) | ||||
| 		var file_contents: String = file.get_as_text().replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path) | ||||
| 		file = FileAccess.open(balloon_path, FileAccess.WRITE) | ||||
| 		file.store_string(file_contents) | ||||
| 		file.close() | ||||
| 		 | ||||
| 		file = FileAccess.open("res://addons/dialogue_manager/example_balloon/small_example_balloon.tscn", FileAccess.READ) | ||||
| 		file_contents = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd") | ||||
| 		file = FileAccess.open(path + "/small_balloon.tscn", FileAccess.WRITE) | ||||
| 		file.store_string(file_contents) | ||||
| 		file.close() | ||||
| 		 | ||||
| 		file = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.gd", FileAccess.READ) | ||||
| 
 | ||||
| 		# Copy the script file | ||||
| 		file = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ) | ||||
| 		file_contents = file.get_as_text() | ||||
| 		file = FileAccess.open(path + "/balloon.gd", FileAccess.WRITE) | ||||
| 		if is_dotnet: | ||||
| 			file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon") | ||||
| 		file = FileAccess.open(balloon_script_path, FileAccess.WRITE) | ||||
| 		file.store_string(file_contents) | ||||
| 		file.close() | ||||
| 		 | ||||
| 
 | ||||
| 		get_editor_interface().get_resource_filesystem().scan() | ||||
| 		get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path + "/balloon.tscn") | ||||
| 		 | ||||
| 		get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", balloon_path) | ||||
| 
 | ||||
| 		DialogueSettings.set_setting("balloon_path", balloon_path) | ||||
| 
 | ||||
| 		directory_dialog.queue_free() | ||||
| 	) | ||||
| 	get_editor_interface().get_base_control().add_child(directory_dialog) | ||||
|  | @ -287,16 +265,12 @@ func _copy_dialogue_balloon() -> void: | |||
| ### Signals | ||||
| 
 | ||||
| 
 | ||||
| func _on_filesystem_changed() -> void: | ||||
| 	update_dialogue_file_cache() | ||||
| 
 | ||||
| 
 | ||||
| func _on_files_moved(old_file: String, new_file: String) -> void: | ||||
| 	update_import_paths(old_file, new_file) | ||||
| 	DialogueSettings.move_recent_file(old_file, new_file) | ||||
| 
 | ||||
| 
 | ||||
| func _on_file_removed(file: String) -> void: | ||||
| 	recompile_dependent_files(file) | ||||
| 	update_import_paths(file, "") | ||||
| 	if is_instance_valid(main_view): | ||||
| 		main_view.close_file(file) | ||||
|  |  | |||
|  | @ -2,40 +2,57 @@ | |||
| extends Node | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueConstants = preload("./constants.gd") | ||||
| 
 | ||||
| 
 | ||||
| ### Editor config | ||||
| 
 | ||||
| const DEFAULT_SETTINGS = { | ||||
| 	"states" = [], | ||||
| 	"missing_translations_are_errors" = false, | ||||
| 	"wrap_lines" = false, | ||||
| 	"new_with_template" = true, | ||||
| 	"include_all_responses" = false, | ||||
| 	"custom_test_scene_path" = "res://addons/dialogue_manager/test_scene.tscn" | ||||
| 	states = [], | ||||
| 	missing_translations_are_errors = false, | ||||
| 	export_characters_in_translation = true, | ||||
| 	wrap_lines = false, | ||||
| 	new_with_template = true, | ||||
| 	include_all_responses = false, | ||||
| 	ignore_missing_state_values = false, | ||||
| 	custom_test_scene_path = preload("./test_scene.tscn").resource_path, | ||||
| 	default_csv_locale = "en", | ||||
| 	balloon_path = "", | ||||
| 	create_lines_for_responses_with_characters = true, | ||||
| 	include_character_in_translation_exports = false, | ||||
| 	include_notes_in_translation_exports = false | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| static func prepare() -> void: | ||||
| 	# Migrate previous keys | ||||
| 	for key in [ | ||||
| 		"states",  | ||||
| 		"missing_translations_are_errors",  | ||||
| 		"wrap_lines",  | ||||
| 		"new_with_template",  | ||||
| 		"include_all_responses",  | ||||
| 		"states", | ||||
| 		"missing_translations_are_errors", | ||||
| 		"export_characters_in_translation", | ||||
| 		"wrap_lines", | ||||
| 		"new_with_template", | ||||
| 		"include_all_responses", | ||||
| 		"custom_test_scene_path" | ||||
| 	]: | ||||
| 		if ProjectSettings.has_setting("dialogue_manager/%s" % key): | ||||
| 			var value = ProjectSettings.get_setting("dialogue_manager/%s" % key) | ||||
| 			ProjectSettings.set_setting("dialogue_manager/%s" % key, null) | ||||
| 			set_setting(key, value) | ||||
| 	 | ||||
| 	# Set up defaults | ||||
| 
 | ||||
| 	# Set up initial settings | ||||
| 	for setting in DEFAULT_SETTINGS: | ||||
| 		if ProjectSettings.has_setting("dialogue_manager/general/%s" % setting): | ||||
| 			ProjectSettings.set_initial_value("dialogue_manager/general/%s" % setting, DEFAULT_SETTINGS[setting]) | ||||
| 		var setting_name: String = "dialogue_manager/general/%s" % setting | ||||
| 		if not ProjectSettings.has_setting(setting_name): | ||||
| 			set_setting(setting, DEFAULT_SETTINGS[setting]) | ||||
| 		ProjectSettings.set_initial_value(setting_name, DEFAULT_SETTINGS[setting]) | ||||
| 		if setting.ends_with("_path"): | ||||
| 			ProjectSettings.add_property_info({ | ||||
| 				"name": setting_name, | ||||
| 				"type": TYPE_STRING, | ||||
| 				"hint": PROPERTY_HINT_FILE, | ||||
| 			}) | ||||
| 
 | ||||
| 	ProjectSettings.save() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -52,23 +69,36 @@ static func get_setting(key: String, default): | |||
| 		return default | ||||
| 
 | ||||
| 
 | ||||
| static func get_settings(only_keys: PackedStringArray = []) -> Dictionary: | ||||
| 	var settings: Dictionary = {} | ||||
| 	for key in DEFAULT_SETTINGS.keys(): | ||||
| 		if only_keys.is_empty() or key in only_keys: | ||||
| 			settings[key] = get_setting(key, DEFAULT_SETTINGS[key]) | ||||
| 	return settings | ||||
| 
 | ||||
| 
 | ||||
| ### User config | ||||
| 
 | ||||
| 
 | ||||
| static func get_user_config() -> Dictionary: | ||||
| 	var user_config: Dictionary = { | ||||
| 		check_for_updates = true, | ||||
| 		just_refreshed = null, | ||||
| 		recent_files = [], | ||||
| 		reopen_files = [], | ||||
| 		most_recent_reopen_file = "", | ||||
| 		carets = {}, | ||||
| 		run_title = "", | ||||
| 		run_resource_path = "", | ||||
| 		is_running_test_scene = false | ||||
| 		is_running_test_scene = false, | ||||
| 		has_dotnet_solution = false, | ||||
| 		open_in_external_editor = false | ||||
| 	} | ||||
| 	 | ||||
| 
 | ||||
| 	if FileAccess.file_exists(DialogueConstants.USER_CONFIG_PATH): | ||||
| 		var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.READ) | ||||
| 		user_config.merge(JSON.parse_string(file.get_as_text()), true) | ||||
| 	 | ||||
| 
 | ||||
| 	return user_config | ||||
| 
 | ||||
| 
 | ||||
|  | @ -121,8 +151,8 @@ static func clear_recent_files() -> void: | |||
| 
 | ||||
| static func set_caret(path: String, cursor: Vector2) -> void: | ||||
| 	var carets: Dictionary = get_user_value("carets", {}) | ||||
| 	carets[path] = {  | ||||
| 		x = cursor.x,  | ||||
| 	carets[path] = { | ||||
| 		x = cursor.x, | ||||
| 		y = cursor.y | ||||
| 	} | ||||
| 	set_user_value("carets", carets) | ||||
|  | @ -135,3 +165,20 @@ static func get_caret(path: String) -> Vector2: | |||
| 		return Vector2(caret.x, caret.y) | ||||
| 	else: | ||||
| 		return Vector2.ZERO | ||||
| 
 | ||||
| 
 | ||||
| static func has_dotnet_solution() -> bool: | ||||
| 	if get_user_value("has_dotnet_solution", false): return true | ||||
| 
 | ||||
| 	if ProjectSettings.has_setting("dotnet/project/solution_directory"): | ||||
| 		var directory: String = ProjectSettings.get("dotnet/project/solution_directory") | ||||
| 		var file_name: String = ProjectSettings.get("dotnet/project/assembly_name") | ||||
| 		var has_dotnet_solution: bool = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name]) | ||||
| 		set_user_value("has_dotnet_solution", has_dotnet_solution) | ||||
| 		return has_dotnet_solution | ||||
| 	else: | ||||
| 		var plugin_path: String = new().get_script().resource_path.get_base_dir() | ||||
| 		if not ResourceLoader.exists(plugin_path + "/DialogueManager.cs"): return false | ||||
| 		if load(plugin_path + "/DialogueManager.cs") == null: return false | ||||
| 
 | ||||
| 	return true | ||||
|  | @ -1,7 +1,7 @@ | |||
| class_name BaseDialogueTestScene extends Node2D | ||||
| 
 | ||||
| 
 | ||||
| const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd") | ||||
| const DialogueSettings = preload("./settings.gd") | ||||
| 
 | ||||
| 
 | ||||
| @onready var title: String = DialogueSettings.get_user_value("run_title") | ||||
|  | @ -13,9 +13,11 @@ func _ready(): | |||
| 	DisplayServer.window_set_position(Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - DisplayServer.window_get_size()) * 0.5) | ||||
| 	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) | ||||
| 
 | ||||
| 	DialogueManager.dialogue_ended.connect(_on_dialogue_ended) | ||||
| 
 | ||||
| 	DialogueManager.show_example_dialogue_balloon(resource, title) | ||||
| 	# Normally you can just call DialogueManager directly but doing so before the plugin has been | ||||
| 	# enabled in settings will throw a compiler error here so I'm using get_singleton instead. | ||||
| 	var dialogue_manager = Engine.get_singleton("DialogueManager") | ||||
| 	dialogue_manager.dialogue_ended.connect(_on_dialogue_ended) | ||||
| 	dialogue_manager.show_dialogue_balloon(resource, title) | ||||
| 
 | ||||
| 
 | ||||
| func _enter_tree() -> void: | ||||
|  |  | |||
|  | @ -0,0 +1,468 @@ | |||
| extends Object | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| 
 | ||||
| const SUPPORTED_BUILTIN_TYPES = [ | ||||
| 	TYPE_ARRAY, | ||||
| 	TYPE_VECTOR2, | ||||
| 	TYPE_VECTOR3, | ||||
| 	TYPE_VECTOR4, | ||||
| 	TYPE_DICTIONARY, | ||||
| 	TYPE_QUATERNION, | ||||
| 	TYPE_COLOR, | ||||
| 	TYPE_SIGNAL | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| static var resolve_method_error: Error = OK | ||||
| 
 | ||||
| 
 | ||||
| static func is_supported(thing) -> bool: | ||||
| 	return typeof(thing) in SUPPORTED_BUILTIN_TYPES | ||||
| 
 | ||||
| 
 | ||||
| static func resolve_property(builtin, property: String): | ||||
| 	match typeof(builtin): | ||||
| 		TYPE_ARRAY, TYPE_DICTIONARY, TYPE_QUATERNION: | ||||
| 			return builtin[property] | ||||
| 
 | ||||
| 		# Some types have constants that we need to manually resolve | ||||
| 
 | ||||
| 		TYPE_VECTOR2: | ||||
| 			return resolve_vector2_property(builtin, property) | ||||
| 		TYPE_VECTOR3: | ||||
| 			return resolve_vector3_property(builtin, property) | ||||
| 		TYPE_VECTOR4: | ||||
| 			return resolve_vector4_property(builtin, property) | ||||
| 		TYPE_COLOR: | ||||
| 			return resolve_color_property(builtin, property) | ||||
| 
 | ||||
| 
 | ||||
| static func resolve_method(thing, method_name: String, args: Array): | ||||
| 	resolve_method_error = OK | ||||
| 
 | ||||
| 	# Resolve static methods manually | ||||
| 	match typeof(thing): | ||||
| 		TYPE_VECTOR2: | ||||
| 			match method_name: | ||||
| 				"from_angle": | ||||
| 					return Vector2.from_angle(args[0]) | ||||
| 
 | ||||
| 		TYPE_COLOR: | ||||
| 			match method_name: | ||||
| 				"from_hsv": | ||||
| 					return Color.from_hsv(args[0], args[1], args[2]) if args.size() == 3 else Color.from_hsv(args[0], args[1], args[2], args[3]) | ||||
| 				"from_ok_hsl": | ||||
| 					return Color.from_ok_hsl(args[0], args[1], args[2]) if args.size() == 3 else Color.from_ok_hsl(args[0], args[1], args[2], args[3]) | ||||
| 				"from_rgbe9995": | ||||
| 					return Color.from_rgbe9995(args[0]) | ||||
| 				"from_string": | ||||
| 					return Color.from_string(args[0], args[1]) | ||||
| 
 | ||||
| 		TYPE_QUATERNION: | ||||
| 			match method_name: | ||||
| 				"from_euler": | ||||
| 					return Quaternion.from_euler(args[0]) | ||||
| 
 | ||||
| 	# Anything else can be evaulatated automatically | ||||
| 	var references: Array = ["thing"] | ||||
| 	for i in range(0, args.size()): | ||||
| 		references.append("arg%d" % i) | ||||
| 	var expression = Expression.new() | ||||
| 	if expression.parse("thing.%s(%s)" % [method_name, ",".join(references.slice(1))], references) != OK: | ||||
| 		assert(false, expression.get_error_text()) | ||||
| 	var result = expression.execute([thing] + args, null, false) | ||||
| 	if expression.has_execute_failed(): | ||||
| 		resolve_method_error = ERR_CANT_RESOLVE | ||||
| 		return null | ||||
| 
 | ||||
| 	return result | ||||
| 
 | ||||
| 
 | ||||
| static func has_resolve_method_failed() -> bool: | ||||
| 	return resolve_method_error != OK | ||||
| 
 | ||||
| 
 | ||||
| static func resolve_color_property(color: Color, property: String): | ||||
| 	match property: | ||||
| 		"ALICE_BLUE": | ||||
| 			return Color.ALICE_BLUE | ||||
| 		"ANTIQUE_WHITE": | ||||
| 			return Color.ANTIQUE_WHITE | ||||
| 		"AQUA": | ||||
| 			return Color.AQUA | ||||
| 		"AQUAMARINE": | ||||
| 			return Color.AQUAMARINE | ||||
| 		"AZURE": | ||||
| 			return Color.AZURE | ||||
| 		"BEIGE": | ||||
| 			return Color.BEIGE | ||||
| 		"BISQUE": | ||||
| 			return Color.BISQUE | ||||
| 		"BLACK": | ||||
| 			return Color.BLACK | ||||
| 		"BLANCHED_ALMOND": | ||||
| 			return Color.BLANCHED_ALMOND | ||||
| 		"BLUE": | ||||
| 			return Color.BLUE | ||||
| 		"BLUE_VIOLET": | ||||
| 			return Color.BLUE_VIOLET | ||||
| 		"BROWN": | ||||
| 			return Color.BROWN | ||||
| 		"BURLYWOOD": | ||||
| 			return Color.BURLYWOOD | ||||
| 		"CADET_BLUE": | ||||
| 			return Color.CADET_BLUE | ||||
| 		"CHARTREUSE": | ||||
| 			return Color.CHARTREUSE | ||||
| 		"CHOCOLATE": | ||||
| 			return Color.CHOCOLATE | ||||
| 		"CORAL": | ||||
| 			return Color.CORAL | ||||
| 		"CORNFLOWER_BLUE": | ||||
| 			return Color.CORNFLOWER_BLUE | ||||
| 		"CORNSILK": | ||||
| 			return Color.CORNSILK | ||||
| 		"CRIMSON": | ||||
| 			return Color.CRIMSON | ||||
| 		"CYAN": | ||||
| 			return Color.CYAN | ||||
| 		"DARK_BLUE": | ||||
| 			return Color.DARK_BLUE | ||||
| 		"DARK_CYAN": | ||||
| 			return Color.DARK_CYAN | ||||
| 		"DARK_GOLDENROD": | ||||
| 			return Color.DARK_GOLDENROD | ||||
| 		"DARK_GRAY": | ||||
| 			return Color.DARK_GRAY | ||||
| 		"DARK_GREEN": | ||||
| 			return Color.DARK_GREEN | ||||
| 		"DARK_KHAKI": | ||||
| 			return Color.DARK_KHAKI | ||||
| 		"DARK_MAGENTA": | ||||
| 			return Color.DARK_MAGENTA | ||||
| 		"DARK_OLIVE_GREEN": | ||||
| 			return Color.DARK_OLIVE_GREEN | ||||
| 		"DARK_ORANGE": | ||||
| 			return Color.DARK_ORANGE | ||||
| 		"DARK_ORCHID": | ||||
| 			return Color.DARK_ORCHID | ||||
| 		"DARK_RED": | ||||
| 			return Color.DARK_RED | ||||
| 		"DARK_SALMON": | ||||
| 			return Color.DARK_SALMON | ||||
| 		"DARK_SEA_GREEN": | ||||
| 			return Color.DARK_SEA_GREEN | ||||
| 		"DARK_SLATE_BLUE": | ||||
| 			return Color.DARK_SLATE_BLUE | ||||
| 		"DARK_SLATE_GRAY": | ||||
| 			return Color.DARK_SLATE_GRAY | ||||
| 		"DARK_TURQUOISE": | ||||
| 			return Color.DARK_TURQUOISE | ||||
| 		"DARK_VIOLET": | ||||
| 			return Color.DARK_VIOLET | ||||
| 		"DEEP_PINK": | ||||
| 			return Color.DEEP_PINK | ||||
| 		"DEEP_SKY_BLUE": | ||||
| 			return Color.DEEP_SKY_BLUE | ||||
| 		"DIM_GRAY": | ||||
| 			return Color.DIM_GRAY | ||||
| 		"DODGER_BLUE": | ||||
| 			return Color.DODGER_BLUE | ||||
| 		"FIREBRICK": | ||||
| 			return Color.FIREBRICK | ||||
| 		"FLORAL_WHITE": | ||||
| 			return Color.FLORAL_WHITE | ||||
| 		"FOREST_GREEN": | ||||
| 			return Color.FOREST_GREEN | ||||
| 		"FUCHSIA": | ||||
| 			return Color.FUCHSIA | ||||
| 		"GAINSBORO": | ||||
| 			return Color.GAINSBORO | ||||
| 		"GHOST_WHITE": | ||||
| 			return Color.GHOST_WHITE | ||||
| 		"GOLD": | ||||
| 			return Color.GOLD | ||||
| 		"GOLDENROD": | ||||
| 			return Color.GOLDENROD | ||||
| 		"GRAY": | ||||
| 			return Color.GRAY | ||||
| 		"GREEN": | ||||
| 			return Color.GREEN | ||||
| 		"GREEN_YELLOW": | ||||
| 			return Color.GREEN_YELLOW | ||||
| 		"HONEYDEW": | ||||
| 			return Color.HONEYDEW | ||||
| 		"HOT_PINK": | ||||
| 			return Color.HOT_PINK | ||||
| 		"INDIAN_RED": | ||||
| 			return Color.INDIAN_RED | ||||
| 		"INDIGO": | ||||
| 			return Color.INDIGO | ||||
| 		"IVORY": | ||||
| 			return Color.IVORY | ||||
| 		"KHAKI": | ||||
| 			return Color.KHAKI | ||||
| 		"LAVENDER": | ||||
| 			return Color.LAVENDER | ||||
| 		"LAVENDER_BLUSH": | ||||
| 			return Color.LAVENDER_BLUSH | ||||
| 		"LAWN_GREEN": | ||||
| 			return Color.LAWN_GREEN | ||||
| 		"LEMON_CHIFFON": | ||||
| 			return Color.LEMON_CHIFFON | ||||
| 		"LIGHT_BLUE": | ||||
| 			return Color.LIGHT_BLUE | ||||
| 		"LIGHT_CORAL": | ||||
| 			return Color.LIGHT_CORAL | ||||
| 		"LIGHT_CYAN": | ||||
| 			return Color.LIGHT_CYAN | ||||
| 		"LIGHT_GOLDENROD": | ||||
| 			return Color.LIGHT_GOLDENROD | ||||
| 		"LIGHT_GRAY": | ||||
| 			return Color.LIGHT_GRAY | ||||
| 		"LIGHT_GREEN": | ||||
| 			return Color.LIGHT_GREEN | ||||
| 		"LIGHT_PINK": | ||||
| 			return Color.LIGHT_PINK | ||||
| 		"LIGHT_SALMON": | ||||
| 			return Color.LIGHT_SALMON | ||||
| 		"LIGHT_SEA_GREEN": | ||||
| 			return Color.LIGHT_SEA_GREEN | ||||
| 		"LIGHT_SKY_BLUE": | ||||
| 			return Color.LIGHT_SKY_BLUE | ||||
| 		"LIGHT_SLATE_GRAY": | ||||
| 			return Color.LIGHT_SLATE_GRAY | ||||
| 		"LIGHT_STEEL_BLUE": | ||||
| 			return Color.LIGHT_STEEL_BLUE | ||||
| 		"LIGHT_YELLOW": | ||||
| 			return Color.LIGHT_YELLOW | ||||
| 		"LIME": | ||||
| 			return Color.LIME | ||||
| 		"LIME_GREEN": | ||||
| 			return Color.LIME_GREEN | ||||
| 		"LINEN": | ||||
| 			return Color.LINEN | ||||
| 		"MAGENTA": | ||||
| 			return Color.MAGENTA | ||||
| 		"MAROON": | ||||
| 			return Color.MAROON | ||||
| 		"MEDIUM_AQUAMARINE": | ||||
| 			return Color.MEDIUM_AQUAMARINE | ||||
| 		"MEDIUM_BLUE": | ||||
| 			return Color.MEDIUM_BLUE | ||||
| 		"MEDIUM_ORCHID": | ||||
| 			return Color.MEDIUM_ORCHID | ||||
| 		"MEDIUM_PURPLE": | ||||
| 			return Color.MEDIUM_PURPLE | ||||
| 		"MEDIUM_SEA_GREEN": | ||||
| 			return Color.MEDIUM_SEA_GREEN | ||||
| 		"MEDIUM_SLATE_BLUE": | ||||
| 			return Color.MEDIUM_SLATE_BLUE | ||||
| 		"MEDIUM_SPRING_GREEN": | ||||
| 			return Color.MEDIUM_SPRING_GREEN | ||||
| 		"MEDIUM_TURQUOISE": | ||||
| 			return Color.MEDIUM_TURQUOISE | ||||
| 		"MEDIUM_VIOLET_RED": | ||||
| 			return Color.MEDIUM_VIOLET_RED | ||||
| 		"MIDNIGHT_BLUE": | ||||
| 			return Color.MIDNIGHT_BLUE | ||||
| 		"MINT_CREAM": | ||||
| 			return Color.MINT_CREAM | ||||
| 		"MISTY_ROSE": | ||||
| 			return Color.MISTY_ROSE | ||||
| 		"MOCCASIN": | ||||
| 			return Color.MOCCASIN | ||||
| 		"NAVAJO_WHITE": | ||||
| 			return Color.NAVAJO_WHITE | ||||
| 		"NAVY_BLUE": | ||||
| 			return Color.NAVY_BLUE | ||||
| 		"OLD_LACE": | ||||
| 			return Color.OLD_LACE | ||||
| 		"OLIVE": | ||||
| 			return Color.OLIVE | ||||
| 		"OLIVE_DRAB": | ||||
| 			return Color.OLIVE_DRAB | ||||
| 		"ORANGE": | ||||
| 			return Color.ORANGE | ||||
| 		"ORANGE_RED": | ||||
| 			return Color.ORANGE_RED | ||||
| 		"ORCHID": | ||||
| 			return Color.ORCHID | ||||
| 		"PALE_GOLDENROD": | ||||
| 			return Color.PALE_GOLDENROD | ||||
| 		"PALE_GREEN": | ||||
| 			return Color.PALE_GREEN | ||||
| 		"PALE_TURQUOISE": | ||||
| 			return Color.PALE_TURQUOISE | ||||
| 		"PALE_VIOLET_RED": | ||||
| 			return Color.PALE_VIOLET_RED | ||||
| 		"PAPAYA_WHIP": | ||||
| 			return Color.PAPAYA_WHIP | ||||
| 		"PEACH_PUFF": | ||||
| 			return Color.PEACH_PUFF | ||||
| 		"PERU": | ||||
| 			return Color.PERU | ||||
| 		"PINK": | ||||
| 			return Color.PINK | ||||
| 		"PLUM": | ||||
| 			return Color.PLUM | ||||
| 		"POWDER_BLUE": | ||||
| 			return Color.POWDER_BLUE | ||||
| 		"PURPLE": | ||||
| 			return Color.PURPLE | ||||
| 		"REBECCA_PURPLE": | ||||
| 			return Color.REBECCA_PURPLE | ||||
| 		"RED": | ||||
| 			return Color.RED | ||||
| 		"ROSY_BROWN": | ||||
| 			return Color.ROSY_BROWN | ||||
| 		"ROYAL_BLUE": | ||||
| 			return Color.ROYAL_BLUE | ||||
| 		"SADDLE_BROWN": | ||||
| 			return Color.SADDLE_BROWN | ||||
| 		"SALMON": | ||||
| 			return Color.SALMON | ||||
| 		"SANDY_BROWN": | ||||
| 			return Color.SANDY_BROWN | ||||
| 		"SEA_GREEN": | ||||
| 			return Color.SEA_GREEN | ||||
| 		"SEASHELL": | ||||
| 			return Color.SEASHELL | ||||
| 		"SIENNA": | ||||
| 			return Color.SIENNA | ||||
| 		"SILVER": | ||||
| 			return Color.SILVER | ||||
| 		"SKY_BLUE": | ||||
| 			return Color.SKY_BLUE | ||||
| 		"SLATE_BLUE": | ||||
| 			return Color.SLATE_BLUE | ||||
| 		"SLATE_GRAY": | ||||
| 			return Color.SLATE_GRAY | ||||
| 		"SNOW": | ||||
| 			return Color.SNOW | ||||
| 		"SPRING_GREEN": | ||||
| 			return Color.SPRING_GREEN | ||||
| 		"STEEL_BLUE": | ||||
| 			return Color.STEEL_BLUE | ||||
| 		"TAN": | ||||
| 			return Color.TAN | ||||
| 		"TEAL": | ||||
| 			return Color.TEAL | ||||
| 		"THISTLE": | ||||
| 			return Color.THISTLE | ||||
| 		"TOMATO": | ||||
| 			return Color.TOMATO | ||||
| 		"TRANSPARENT": | ||||
| 			return Color.TRANSPARENT | ||||
| 		"TURQUOISE": | ||||
| 			return Color.TURQUOISE | ||||
| 		"VIOLET": | ||||
| 			return Color.VIOLET | ||||
| 		"WEB_GRAY": | ||||
| 			return Color.WEB_GRAY | ||||
| 		"WEB_GREEN": | ||||
| 			return Color.WEB_GREEN | ||||
| 		"WEB_MAROON": | ||||
| 			return Color.WEB_MAROON | ||||
| 		"WEB_PURPLE": | ||||
| 			return Color.WEB_PURPLE | ||||
| 		"WHEAT": | ||||
| 			return Color.WHEAT | ||||
| 		"WHITE": | ||||
| 			return Color.WHITE | ||||
| 		"WHITE_SMOKE": | ||||
| 			return Color.WHITE_SMOKE | ||||
| 		"YELLOW": | ||||
| 			return Color.YELLOW | ||||
| 		"YELLOW_GREEN": | ||||
| 			return Color.YELLOW_GREEN | ||||
| 
 | ||||
| 	return color[property] | ||||
| 
 | ||||
| 
 | ||||
| static func resolve_vector2_property(vector: Vector2, property: String): | ||||
| 	match property: | ||||
| 		"AXIS_X": | ||||
| 			return Vector2.AXIS_X | ||||
| 		"AXIS_Y": | ||||
| 			return Vector2.AXIS_Y | ||||
| 		"ZERO": | ||||
| 			return Vector2.ZERO | ||||
| 		"ONE": | ||||
| 			return Vector2.ONE | ||||
| 		"INF": | ||||
| 			return Vector2.INF | ||||
| 		"LEFT": | ||||
| 			return Vector2.LEFT | ||||
| 		"RIGHT": | ||||
| 			return Vector2.RIGHT | ||||
| 		"UP": | ||||
| 			return Vector2.UP | ||||
| 		"DOWN": | ||||
| 			return Vector2.DOWN | ||||
| 
 | ||||
| 	return vector[property] | ||||
| 
 | ||||
| 
 | ||||
| static func resolve_vector3_property(vector: Vector3, property: String): | ||||
| 	match property: | ||||
| 		"AXIS_X": | ||||
| 			return Vector3.AXIS_X | ||||
| 		"AXIS_Y": | ||||
| 			return Vector3.AXIS_Y | ||||
| 		"AXIS_Z": | ||||
| 			return Vector3.AXIS_Z | ||||
| 		"ZERO": | ||||
| 			return Vector3.ZERO | ||||
| 		"ONE": | ||||
| 			return Vector3.ONE | ||||
| 		"INF": | ||||
| 			return Vector3.INF | ||||
| 		"LEFT": | ||||
| 			return Vector3.LEFT | ||||
| 		"RIGHT": | ||||
| 			return Vector3.RIGHT | ||||
| 		"UP": | ||||
| 			return Vector3.UP | ||||
| 		"DOWN": | ||||
| 			return Vector3.DOWN | ||||
| 		"FORWARD": | ||||
| 			return Vector3.FORWARD | ||||
| 		"BACK": | ||||
| 			return Vector3.BACK | ||||
| 		"MODEL_LEFT": | ||||
| 			return Vector3(1, 0, 0) | ||||
| 		"MODEL_RIGHT": | ||||
| 			return Vector3(-1, 0, 0) | ||||
| 		"MODEL_TOP": | ||||
| 			return Vector3(0, 1, 0) | ||||
| 		"MODEL_BOTTOM": | ||||
| 			return Vector3(0, -1, 0) | ||||
| 		"MODEL_FRONT": | ||||
| 			return Vector3(0, 0, 1) | ||||
| 		"MODEL_REAR": | ||||
| 			return Vector3(0, 0, -1) | ||||
| 
 | ||||
| 	return vector[property] | ||||
| 
 | ||||
| 
 | ||||
| static func resolve_vector4_property(vector: Vector4, property: String): | ||||
| 	match property: | ||||
| 		"AXIS_X": | ||||
| 			return Vector4.AXIS_X | ||||
| 		"AXIS_Y": | ||||
| 			return Vector4.AXIS_Y | ||||
| 		"AXIS_Z": | ||||
| 			return Vector4.AXIS_Z | ||||
| 		"AXIS_W": | ||||
| 			return Vector4.AXIS_W | ||||
| 		"ZERO": | ||||
| 			return Vector4.ZERO | ||||
| 		"ONE": | ||||
| 			return Vector4.ONE | ||||
| 		"INF": | ||||
| 			return Vector4.INF | ||||
| 
 | ||||
| 	return vector[property] | ||||
|  | @ -2,8 +2,8 @@ | |||
| extends Control | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| const DialogueSettings = preload("../settings.gd") | ||||
| 
 | ||||
| const OPEN_OPEN = 100 | ||||
| const OPEN_CLEAR = 101 | ||||
|  | @ -41,16 +41,20 @@ enum TranslationSource { | |||
| @onready var build_error_dialog: AcceptDialog = $BuildErrorDialog | ||||
| @onready var close_confirmation_dialog: ConfirmationDialog = $CloseConfirmationDialog | ||||
| @onready var updated_dialog: AcceptDialog = $UpdatedDialog | ||||
| @onready var find_in_files_dialog: AcceptDialog = $FindInFilesDialog | ||||
| @onready var find_in_files: Control = $FindInFilesDialog/FindInFiles | ||||
| 
 | ||||
| # Toolbar | ||||
| @onready var new_button: Button = %NewButton | ||||
| @onready var open_button: MenuButton = %OpenButton | ||||
| @onready var save_all_button: Button = %SaveAllButton | ||||
| @onready var find_in_files_button: Button = %FindInFilesButton | ||||
| @onready var test_button: Button = %TestButton | ||||
| @onready var search_button: Button = %SearchButton | ||||
| @onready var insert_button: MenuButton = %InsertButton | ||||
| @onready var translations_button: MenuButton = %TranslationsButton | ||||
| @onready var settings_button: Button = %SettingsButton | ||||
| @onready var support_button: Button = %SupportButton | ||||
| @onready var docs_button: Button = %DocsButton | ||||
| @onready var version_label: Label = %VersionLabel | ||||
| @onready var update_button: Button = %UpdateButton | ||||
|  | @ -83,6 +87,7 @@ var current_file_path: String = "": | |||
| 			files_list.hide() | ||||
| 			title_list.hide() | ||||
| 			code_edit.hide() | ||||
| 			errors_panel.hide() | ||||
| 		else: | ||||
| 			test_button.disabled = false | ||||
| 			search_button.disabled = false | ||||
|  | @ -120,7 +125,7 @@ func _ready() -> void: | |||
| 	self.current_file_path = "" | ||||
| 
 | ||||
| 	# Set up the update checker | ||||
| 	version_label.text = "v%s" % update_button.get_version() | ||||
| 	version_label.text = "v%s" % editor_plugin.get_version() | ||||
| 	update_button.editor_plugin = editor_plugin | ||||
| 	update_button.on_before_refresh = func on_before_refresh(): | ||||
| 		# Save everything | ||||
|  | @ -149,14 +154,27 @@ func _ready() -> void: | |||
| 	editor_settings.settings_changed.connect(_on_editor_settings_changed) | ||||
| 	_on_editor_settings_changed() | ||||
| 
 | ||||
| 	# Reopen any files that were open when Godot was closed | ||||
| 	if editor_settings.get_setting("text_editor/behavior/files/restore_scripts_on_load"): | ||||
| 		var reopen_files: Array = DialogueSettings.get_user_value("reopen_files", []) | ||||
| 		for reopen_file in reopen_files: | ||||
| 			open_file(reopen_file) | ||||
| 
 | ||||
| 		self.current_file_path = DialogueSettings.get_user_value("most_recent_reopen_file", "") | ||||
| 
 | ||||
| 	save_all_button.disabled = true | ||||
| 
 | ||||
| 	close_confirmation_dialog.ok_button_text = DialogueConstants.translate("confirm_close.save") | ||||
| 	close_confirmation_dialog.add_button(DialogueConstants.translate("confirm_close.discard"), true, "discard") | ||||
| 	close_confirmation_dialog.ok_button_text = DialogueConstants.translate(&"confirm_close.save") | ||||
| 	close_confirmation_dialog.add_button(DialogueConstants.translate(&"confirm_close.discard"), true, "discard") | ||||
| 
 | ||||
| 	settings_view.editor_plugin = editor_plugin | ||||
| 
 | ||||
| 	errors_dialog.dialog_text = DialogueConstants.translate("errors_in_script") | ||||
| 	errors_dialog.dialog_text = DialogueConstants.translate(&"errors_in_script") | ||||
| 
 | ||||
| 
 | ||||
| func _exit_tree() -> void: | ||||
| 	DialogueSettings.set_user_value("reopen_files", open_buffers.keys()) | ||||
| 	DialogueSettings.set_user_value("most_recent_reopen_file", self.current_file_path) | ||||
| 
 | ||||
| 
 | ||||
| func _unhandled_input(event: InputEvent) -> void: | ||||
|  | @ -164,13 +182,18 @@ func _unhandled_input(event: InputEvent) -> void: | |||
| 
 | ||||
| 	if event is InputEventKey and event.is_pressed(): | ||||
| 		match event.as_text(): | ||||
| 			"Ctrl+Alt+S": | ||||
| 			"Ctrl+Alt+S", "Command+Alt+S": | ||||
| 				get_viewport().set_input_as_handled() | ||||
| 				save_file(current_file_path) | ||||
| 			"Ctrl+W": | ||||
| 			"Ctrl+W", "Command+W": | ||||
| 				get_viewport().set_input_as_handled() | ||||
| 				close_file(current_file_path) | ||||
| 			"Ctrl+F5": | ||||
| 			"Ctrl+F5", "Command+F5": | ||||
| 				get_viewport().set_input_as_handled() | ||||
| 				_on_test_button_pressed() | ||||
| 			"Ctrl+Shift+F", "Command+Shift+F": | ||||
| 				get_viewport().set_input_as_handled() | ||||
| 				_on_find_in_files_button_pressed() | ||||
| 
 | ||||
| 
 | ||||
| func apply_changes() -> void: | ||||
|  | @ -195,7 +218,7 @@ func load_from_version_refresh(just_refreshed: Dictionary) -> void: | |||
| 	else: | ||||
| 		editor_plugin.get_editor_interface().set_main_screen_editor("Dialogue") | ||||
| 
 | ||||
| 	updated_dialog.dialog_text = DialogueConstants.translate("update.success").format({ version = update_button.get_version() }) | ||||
| 	updated_dialog.dialog_text = DialogueConstants.translate(&"update.success").format({ version = update_button.get_version() }) | ||||
| 	updated_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -253,26 +276,29 @@ func open_file(path: String) -> void: | |||
| 
 | ||||
| 
 | ||||
| func show_file_in_filesystem(path: String) -> void: | ||||
| 	var file_system = editor_plugin.get_editor_interface().get_file_system_dock() | ||||
| 	file_system.navigate_to_path(path) | ||||
| 	var file_system_dock: FileSystemDock = Engine.get_meta("DialogueManagerPlugin") \ | ||||
| 		.get_editor_interface() \ | ||||
| 		.get_file_system_dock() | ||||
| 
 | ||||
| 	file_system_dock.navigate_to_path(path) | ||||
| 
 | ||||
| 
 | ||||
| # Save any open files | ||||
| func save_files() -> void: | ||||
| 	save_all_button.disabled = true | ||||
| 
 | ||||
| 	var saved_files: PackedStringArray = [] | ||||
| 	for path in open_buffers: | ||||
| 		if open_buffers[path].text != open_buffers[path].pristine_text: | ||||
| 			saved_files.append(path) | ||||
| 		save_file(path) | ||||
| 		save_file(path, false) | ||||
| 
 | ||||
| 	# Make sure we reimport/recompile the changes | ||||
| 	if saved_files.size() > 0: | ||||
| 		editor_plugin.get_editor_interface().get_resource_filesystem().reimport_files(saved_files) | ||||
| 	save_all_button.disabled = true | ||||
| 		Engine.get_meta("DialogueCache").reimport_files(saved_files) | ||||
| 
 | ||||
| 
 | ||||
| # Save a file | ||||
| func save_file(path: String) -> void: | ||||
| func save_file(path: String, rescan_file_system: bool = true) -> void: | ||||
| 	var buffer = open_buffers[path] | ||||
| 
 | ||||
| 	files_list.mark_file_as_unsaved(path, false) | ||||
|  | @ -289,6 +315,12 @@ func save_file(path: String) -> void: | |||
| 	file.store_string(buffer.text) | ||||
| 	file.close() | ||||
| 
 | ||||
| 	if rescan_file_system: | ||||
| 		Engine.get_meta("DialogueManagerPlugin") \ | ||||
| 			.get_editor_interface() \ | ||||
| 			.get_resource_filesystem()\ | ||||
| 			.scan() | ||||
| 
 | ||||
| 
 | ||||
| func close_file(file: String) -> void: | ||||
| 	if not file in open_buffers.keys(): return | ||||
|  | @ -298,7 +330,7 @@ func close_file(file: String) -> void: | |||
| 	if buffer.text == buffer.pristine_text: | ||||
| 		remove_file_from_open_buffers(file) | ||||
| 	else: | ||||
| 		close_confirmation_dialog.dialog_text = DialogueConstants.translate("confirm_close").format({ path = file.get_file() }) | ||||
| 		close_confirmation_dialog.dialog_text = DialogueConstants.translate(&"confirm_close").format({ path = file.get_file() }) | ||||
| 		close_confirmation_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -328,6 +360,9 @@ func apply_theme() -> void: | |||
| 			current_line_color = editor_settings.get_setting("text_editor/theme/highlighting/current_line_color"), | ||||
| 			error_line_color = editor_settings.get_setting("text_editor/theme/highlighting/mark_color"), | ||||
| 
 | ||||
| 			critical_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/critical_color"), | ||||
| 			notice_color = editor_settings.get_setting("text_editor/theme/highlighting/comment_markers/notice_color"), | ||||
| 
 | ||||
| 			titles_color = editor_settings.get_setting("text_editor/theme/highlighting/control_flow_keyword_color"), | ||||
| 			text_color = editor_settings.get_setting("text_editor/theme/highlighting/text_color"), | ||||
| 			conditions_color = editor_settings.get_setting("text_editor/theme/highlighting/keyword_color"), | ||||
|  | @ -343,69 +378,78 @@ func apply_theme() -> void: | |||
| 		} | ||||
| 
 | ||||
| 		new_button.icon = get_theme_icon("New", "EditorIcons") | ||||
| 		new_button.tooltip_text = DialogueConstants.translate("start_a_new_file") | ||||
| 		new_button.tooltip_text = DialogueConstants.translate(&"start_a_new_file") | ||||
| 
 | ||||
| 		open_button.icon = get_theme_icon("Load", "EditorIcons") | ||||
| 		open_button.tooltip_text = DialogueConstants.translate("open_a_file") | ||||
| 		open_button.tooltip_text = DialogueConstants.translate(&"open_a_file") | ||||
| 
 | ||||
| 		save_all_button.icon = get_theme_icon("Save", "EditorIcons") | ||||
| 		save_all_button.tooltip_text = DialogueConstants.translate("start_all_files") | ||||
| 		save_all_button.tooltip_text = DialogueConstants.translate(&"start_all_files") | ||||
| 
 | ||||
| 		find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons") | ||||
| 		find_in_files_button.tooltip_text = DialogueConstants.translate(&"find_in_files") | ||||
| 
 | ||||
| 		test_button.icon = get_theme_icon("PlayScene", "EditorIcons") | ||||
| 		test_button.tooltip_text = DialogueConstants.translate("test_dialogue") | ||||
| 		test_button.tooltip_text = DialogueConstants.translate(&"test_dialogue") | ||||
| 
 | ||||
| 		search_button.icon = get_theme_icon("Search", "EditorIcons") | ||||
| 		search_button.tooltip_text = DialogueConstants.translate("search_for_text") | ||||
| 		search_button.tooltip_text = DialogueConstants.translate(&"search_for_text") | ||||
| 
 | ||||
| 		insert_button.icon = get_theme_icon("RichTextEffect", "EditorIcons") | ||||
| 		insert_button.text = DialogueConstants.translate("insert") | ||||
| 		insert_button.text = DialogueConstants.translate(&"insert") | ||||
| 
 | ||||
| 		translations_button.icon = get_theme_icon("Translation", "EditorIcons") | ||||
| 		translations_button.text = DialogueConstants.translate("translations") | ||||
| 		translations_button.text = DialogueConstants.translate(&"translations") | ||||
| 
 | ||||
| 		settings_button.icon = get_theme_icon("Tools", "EditorIcons") | ||||
| 		settings_button.tooltip_text = DialogueConstants.translate("settings") | ||||
| 		settings_button.tooltip_text = DialogueConstants.translate(&"settings") | ||||
| 
 | ||||
| 		support_button.icon = get_theme_icon("Heart", "EditorIcons") | ||||
| 		support_button.text = DialogueConstants.translate(&"sponsor") | ||||
| 		support_button.tooltip_text = DialogueConstants.translate(&"show_support") | ||||
| 
 | ||||
| 		docs_button.icon = get_theme_icon("Help", "EditorIcons") | ||||
| 		docs_button.text = DialogueConstants.translate("docs") | ||||
| 		docs_button.text = DialogueConstants.translate(&"docs") | ||||
| 
 | ||||
| 		update_button.apply_theme() | ||||
| 
 | ||||
| 		# Set up the effect menu | ||||
| 		var popup: PopupMenu = insert_button.get_popup() | ||||
| 		popup.clear() | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.wave_bbcode"), 0) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.shake_bbcode"), 1) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.wave_bbcode"), 0) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.shake_bbcode"), 1) | ||||
| 		popup.add_separator() | ||||
| 		popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DialogueConstants.translate("insert.typing_pause"), 3) | ||||
| 		popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DialogueConstants.translate("insert.typing_speed_change"), 4) | ||||
| 		popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DialogueConstants.translate("insert.auto_advance"), 5) | ||||
| 		popup.add_separator(DialogueConstants.translate("insert.templates")) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.title"), 6) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.dialogue"), 7) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.response"), 8) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.random_lines"), 9) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.random_text"), 10) | ||||
| 		popup.add_separator(DialogueConstants.translate("insert.actions")) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.jump"), 11) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate("insert.end_dialogue"), 12) | ||||
| 		popup.add_icon_item(get_theme_icon("Time", "EditorIcons"), DialogueConstants.translate(&"insert.typing_pause"), 3) | ||||
| 		popup.add_icon_item(get_theme_icon("ViewportSpeed", "EditorIcons"), DialogueConstants.translate(&"insert.typing_speed_change"), 4) | ||||
| 		popup.add_icon_item(get_theme_icon("DebugNext", "EditorIcons"), DialogueConstants.translate(&"insert.auto_advance"), 5) | ||||
| 		popup.add_separator(DialogueConstants.translate(&"insert.templates")) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.title"), 6) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.dialogue"), 7) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.response"), 8) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_lines"), 9) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.random_text"), 10) | ||||
| 		popup.add_separator(DialogueConstants.translate(&"insert.actions")) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.jump"), 11) | ||||
| 		popup.add_icon_item(get_theme_icon("RichTextEffect", "EditorIcons"), DialogueConstants.translate(&"insert.end_dialogue"), 12) | ||||
| 
 | ||||
| 		# Set up the translations menu | ||||
| 		popup = translations_button.get_popup() | ||||
| 		popup.clear() | ||||
| 		popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DialogueConstants.translate("generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS) | ||||
| 		popup.add_icon_item(get_theme_icon("Translation", "EditorIcons"), DialogueConstants.translate(&"generate_line_ids"), TRANSLATIONS_GENERATE_LINE_IDS) | ||||
| 		popup.add_separator() | ||||
| 		popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate("save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV) | ||||
| 		popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate("save_to_csv"), TRANSLATIONS_SAVE_TO_CSV) | ||||
| 		popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DialogueConstants.translate("import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV) | ||||
| 		popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_characters_to_csv"), TRANSLATIONS_SAVE_CHARACTERS_TO_CSV) | ||||
| 		popup.add_icon_item(get_theme_icon("FileList", "EditorIcons"), DialogueConstants.translate(&"save_to_csv"), TRANSLATIONS_SAVE_TO_CSV) | ||||
| 		popup.add_icon_item(get_theme_icon("AssetLib", "EditorIcons"), DialogueConstants.translate(&"import_from_csv"), TRANSLATIONS_IMPORT_FROM_CSV) | ||||
| 
 | ||||
| 		# Dialog sizes | ||||
| 		new_dialog.min_size = Vector2(600, 500) * scale | ||||
| 		save_dialog.min_size = Vector2(600, 500) * scale | ||||
| 		open_dialog.min_size = Vector2(600, 500) * scale | ||||
| 		export_dialog.min_size = Vector2(600, 500) * scale | ||||
| 		export_dialog.min_size = Vector2(600, 500) * scale | ||||
| 		settings_dialog.min_size = Vector2(600, 600) * scale | ||||
| 		import_dialog.min_size = Vector2(600, 500) * scale | ||||
| 		settings_dialog.min_size = Vector2(1000, 600) * scale | ||||
| 		settings_dialog.max_size = Vector2(1000, 600) * scale | ||||
| 		find_in_files_dialog.min_size = Vector2(800, 600) * scale | ||||
| 
 | ||||
| 
 | ||||
| ### Helpers | ||||
|  | @ -415,19 +459,20 @@ func apply_theme() -> void: | |||
| func build_open_menu() -> void: | ||||
| 	var menu = open_button.get_popup() | ||||
| 	menu.clear() | ||||
| 	menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DialogueConstants.translate("open.open"), OPEN_OPEN) | ||||
| 	menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), DialogueConstants.translate(&"open.open"), OPEN_OPEN) | ||||
| 	menu.add_separator() | ||||
| 
 | ||||
| 	var recent_files = DialogueSettings.get_recent_files() | ||||
| 	if recent_files.size() == 0: | ||||
| 		menu.add_item(DialogueConstants.translate("open.no_recent_files")) | ||||
| 		menu.add_item(DialogueConstants.translate(&"open.no_recent_files")) | ||||
| 		menu.set_item_disabled(2, true) | ||||
| 	else: | ||||
| 		for path in recent_files: | ||||
| 			menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path) | ||||
| 			if FileAccess.file_exists(path): | ||||
| 				menu.add_icon_item(get_theme_icon("File", "EditorIcons"), path) | ||||
| 
 | ||||
| 	menu.add_separator() | ||||
| 	menu.add_item(DialogueConstants.translate("open.clear_recent_files"), OPEN_CLEAR) | ||||
| 	menu.add_item(DialogueConstants.translate(&"open.clear_recent_files"), OPEN_CLEAR) | ||||
| 	if menu.id_pressed.is_connected(_on_open_menu_id_pressed): | ||||
| 		menu.id_pressed.disconnect(_on_open_menu_id_pressed) | ||||
| 	menu.id_pressed.connect(_on_open_menu_id_pressed) | ||||
|  | @ -454,7 +499,7 @@ func parse() -> void: | |||
| 
 | ||||
| 
 | ||||
| func show_build_error_dialog() -> void: | ||||
| 	build_error_dialog.dialog_text = DialogueConstants.translate("errors_with_build") | ||||
| 	build_error_dialog.dialog_text = DialogueConstants.translate(&"errors_with_build") | ||||
| 	build_error_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -531,11 +576,16 @@ func add_path_to_project_translations(path: String) -> void: | |||
| 
 | ||||
| # Export dialogue and responses to CSV | ||||
| func export_translations_to_csv(path: String) -> void: | ||||
| 	var default_locale: String = DialogueSettings.get_setting("default_csv_locale", "en") | ||||
| 
 | ||||
| 	var file: FileAccess | ||||
| 
 | ||||
| 	# If the file exists, open it first and work out which keys are already in it | ||||
| 	var existing_csv = {} | ||||
| 	var commas = [] | ||||
| 	var existing_csv: Dictionary = {} | ||||
| 	var column_count: int = 2 | ||||
| 	var default_locale_column: int = 1 | ||||
| 	var character_column: int = -1 | ||||
| 	var notes_column: int = -1 | ||||
| 	if FileAccess.file_exists(path): | ||||
| 		file = FileAccess.open(path, FileAccess.READ) | ||||
| 		var is_first_line = true | ||||
|  | @ -544,17 +594,44 @@ func export_translations_to_csv(path: String) -> void: | |||
| 			line = file.get_csv_line() | ||||
| 			if is_first_line: | ||||
| 				is_first_line = false | ||||
| 				for i in range(2, line.size()): | ||||
| 					commas.append("") | ||||
| 				column_count = line.size() | ||||
| 				for i in range(1, line.size()): | ||||
| 					if line[i] == default_locale: | ||||
| 						default_locale_column = i | ||||
| 					elif line[i] == "_character": | ||||
| 						character_column = i | ||||
| 					elif line[i] == "_notes": | ||||
| 						notes_column = i | ||||
| 
 | ||||
| 			# Make sure the line isn't empty before adding it | ||||
| 			if line.size() > 0 and line[0].strip_edges() != "": | ||||
| 				existing_csv[line[0]] = line | ||||
| 
 | ||||
| 		# The character column wasn't found in the existing file but the setting is turned on | ||||
| 		if character_column == -1 and DialogueSettings.get_setting("include_character_in_translation_exports", false): | ||||
| 			character_column = column_count | ||||
| 			column_count += 1 | ||||
| 			existing_csv["keys"].append("_character") | ||||
| 
 | ||||
| 		# The notes column wasn't found in the existing file but the setting is turned on | ||||
| 		if notes_column == -1 and DialogueSettings.get_setting("include_notes_in_translation_exports", false): | ||||
| 			notes_column = column_count | ||||
| 			column_count += 1 | ||||
| 			existing_csv["keys"].append("_notes") | ||||
| 
 | ||||
| 	# Start a new file | ||||
| 	file = FileAccess.open(path, FileAccess.WRITE) | ||||
| 
 | ||||
| 	if not file.file_exists(path): | ||||
| 		file.store_csv_line(["keys", "en"]) | ||||
| 	if not FileAccess.file_exists(path): | ||||
| 		var headings: PackedStringArray = ["keys", default_locale] | ||||
| 		if DialogueSettings.get_setting("include_character_in_translation_exports", false): | ||||
| 			character_column = headings.size() | ||||
| 			headings.append("_character") | ||||
| 		if DialogueSettings.get_setting("include_notes_in_translation_exports", false): | ||||
| 			notes_column = headings.size() | ||||
| 			headings.append("_notes") | ||||
| 		file.store_csv_line(headings) | ||||
| 		column_count = headings.size() | ||||
| 
 | ||||
| 	# Write our translations to file | ||||
| 	var known_keys: PackedStringArray = [] | ||||
|  | @ -571,13 +648,22 @@ func export_translations_to_csv(path: String) -> void: | |||
| 
 | ||||
| 		known_keys.append(line.translation_key) | ||||
| 
 | ||||
| 		var line_to_save: PackedStringArray = [] | ||||
| 		if existing_csv.has(line.translation_key): | ||||
| 			var existing_line = existing_csv.get(line.translation_key) | ||||
| 			existing_line[1] = line.text | ||||
| 			lines_to_save.append(existing_line) | ||||
| 			line_to_save = existing_csv.get(line.translation_key) | ||||
| 			line_to_save.resize(column_count) | ||||
| 			existing_csv.erase(line.translation_key) | ||||
| 		else: | ||||
| 			lines_to_save.append(PackedStringArray([line.translation_key, line.text] + commas)) | ||||
| 			line_to_save.resize(column_count) | ||||
| 			line_to_save[0] = line.translation_key | ||||
| 
 | ||||
| 		line_to_save[default_locale_column] = line.text | ||||
| 		if character_column > -1: | ||||
| 			line_to_save[character_column] = "(response)" if line.type == DialogueConstants.TYPE_RESPONSE else line.character | ||||
| 		if notes_column > -1: | ||||
| 			line_to_save[notes_column] = line.notes | ||||
| 
 | ||||
| 		lines_to_save.append(line_to_save) | ||||
| 
 | ||||
| 	# Store lines in the file, starting with anything that already exists that hasn't been touched | ||||
| 	for line in existing_csv.values(): | ||||
|  | @ -591,7 +677,8 @@ func export_translations_to_csv(path: String) -> void: | |||
| 	editor_plugin.get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path) | ||||
| 
 | ||||
| 	# Add it to the project l10n settings if it's not already there | ||||
| 	var translation_path: String = path.replace(".csv", ".en.translation") | ||||
| 	var language_code: RegExMatch = RegEx.create_from_string("^[a-z]{2,3}").search(default_locale) | ||||
| 	var translation_path: String = path.replace(".csv", ".%s.translation" % language_code.get_string()) | ||||
| 	call_deferred("add_path_to_project_translations", translation_path) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -619,7 +706,7 @@ func export_character_names_to_csv(path: String) -> void: | |||
| 	file = FileAccess.open(path, FileAccess.WRITE) | ||||
| 
 | ||||
| 	if not file.file_exists(path): | ||||
| 		file.store_csv_line(["keys", "en"]) | ||||
| 		file.store_csv_line(["keys", DialogueSettings.get_setting("default_csv_locale", "en")]) | ||||
| 
 | ||||
| 	# Write our translations to file | ||||
| 	var known_keys: PackedStringArray = [] | ||||
|  | @ -709,6 +796,15 @@ func import_translations_from_csv(path: String) -> void: | |||
| 	parser.free() | ||||
| 
 | ||||
| 
 | ||||
| func show_search_form(is_enabled: bool) -> void: | ||||
| 	if code_edit.last_selected_text: | ||||
| 		search_and_replace.input.text = code_edit.last_selected_text | ||||
| 
 | ||||
| 	search_and_replace.visible = is_enabled | ||||
| 	search_button.set_pressed_no_signal(is_enabled) | ||||
| 	search_and_replace.focus_line_edit() | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
| 
 | ||||
| 
 | ||||
|  | @ -716,6 +812,7 @@ func _on_editor_settings_changed() -> void: | |||
| 	var editor_settings: EditorSettings = editor_plugin.get_editor_interface().get_editor_settings() | ||||
| 	code_edit.minimap_draw = editor_settings.get_setting("text_editor/appearance/minimap/show_minimap") | ||||
| 	code_edit.minimap_width = editor_settings.get_setting("text_editor/appearance/minimap/minimap_width") | ||||
| 	code_edit.scroll_smooth = editor_settings.get_setting("text_editor/behavior/navigation/smooth_scrolling") | ||||
| 
 | ||||
| 
 | ||||
| func _on_open_menu_id_pressed(id: int) -> void: | ||||
|  | @ -837,11 +934,17 @@ func _on_save_all_button_pressed() -> void: | |||
| 	save_files() | ||||
| 
 | ||||
| 
 | ||||
| func _on_find_in_files_button_pressed() -> void: | ||||
| 	find_in_files_dialog.popup_centered() | ||||
| 	find_in_files.prepare() | ||||
| 
 | ||||
| 
 | ||||
| func _on_code_edit_text_changed() -> void: | ||||
| 	title_list.titles = code_edit.get_titles() | ||||
| 
 | ||||
| 	var buffer = open_buffers[current_file_path] | ||||
| 	buffer.text = code_edit.text | ||||
| 
 | ||||
| 	files_list.mark_file_as_unsaved(current_file_path, buffer.text != buffer.pristine_text) | ||||
| 	save_all_button.disabled = open_buffers.values().filter(func(d): return d.text != d.pristine_text).size() == 0 | ||||
| 
 | ||||
|  | @ -878,15 +981,11 @@ func _on_errors_panel_error_pressed(line_number: int, column_number: int) -> voi | |||
| 
 | ||||
| 
 | ||||
| func _on_search_button_toggled(button_pressed: bool) -> void: | ||||
| 	if code_edit.last_selected_text: | ||||
| 		search_and_replace.input.text = code_edit.last_selected_text | ||||
| 
 | ||||
| 	search_and_replace.visible = button_pressed | ||||
| 	show_search_form(button_pressed) | ||||
| 
 | ||||
| 
 | ||||
| func _on_search_and_replace_open_requested() -> void: | ||||
| 	search_button.set_pressed_no_signal(true) | ||||
| 	search_and_replace.visible = true | ||||
| 	show_search_form(true) | ||||
| 
 | ||||
| 
 | ||||
| func _on_search_and_replace_close_requested() -> void: | ||||
|  | @ -896,6 +995,7 @@ func _on_search_and_replace_close_requested() -> void: | |||
| 
 | ||||
| 
 | ||||
| func _on_settings_button_pressed() -> void: | ||||
| 	settings_view.prepare() | ||||
| 	settings_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -905,7 +1005,7 @@ func _on_settings_view_script_button_pressed(path: String) -> void: | |||
| 
 | ||||
| 
 | ||||
| func _on_test_button_pressed() -> void: | ||||
| 	apply_changes() | ||||
| 	save_file(current_file_path) | ||||
| 
 | ||||
| 	if errors_panel.errors.size() > 0: | ||||
| 		errors_dialog.popup_centered() | ||||
|  | @ -918,11 +1018,16 @@ func _on_test_button_pressed() -> void: | |||
| 
 | ||||
| 
 | ||||
| func _on_settings_dialog_confirmed() -> void: | ||||
| 	settings_view.apply_settings_changes() | ||||
| 	parse() | ||||
| 	code_edit.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY if DialogueSettings.get_setting("wrap_lines", false) else TextEdit.LINE_WRAPPING_NONE | ||||
| 	code_edit.grab_focus() | ||||
| 
 | ||||
| 
 | ||||
| func _on_support_button_pressed() -> void: | ||||
| 	OS.shell_open("https://patreon.com/nathanhoad") | ||||
| 
 | ||||
| 
 | ||||
| func _on_docs_button_pressed() -> void: | ||||
| 	OS.shell_open("https://github.com/nathanhoad/godot_dialogue_manager") | ||||
| 
 | ||||
|  | @ -932,17 +1037,21 @@ func _on_files_list_file_popup_menu_requested(at_position: Vector2) -> void: | |||
| 	files_popup_menu.popup() | ||||
| 
 | ||||
| 
 | ||||
| func _on_files_list_file_middle_clicked(path: String): | ||||
| 	close_file(path) | ||||
| 
 | ||||
| 
 | ||||
| func _on_files_popup_menu_about_to_popup() -> void: | ||||
| 	files_popup_menu.clear() | ||||
| 
 | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.save"), ITEM_SAVE, KEY_MASK_CTRL | KEY_MASK_ALT | KEY_S) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.save_as"), ITEM_SAVE_AS) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.close"), ITEM_CLOSE, KEY_MASK_CTRL | KEY_W) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.close_all"), ITEM_CLOSE_ALL) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.close_other_files"), ITEM_CLOSE_OTHERS) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save"), ITEM_SAVE, KEY_MASK_CTRL | KEY_MASK_ALT | KEY_S) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.save_as"), ITEM_SAVE_AS) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close"), ITEM_CLOSE, KEY_MASK_CTRL | KEY_W) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_all"), ITEM_CLOSE_ALL) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.close_other_files"), ITEM_CLOSE_OTHERS) | ||||
| 	files_popup_menu.add_separator() | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.copy_file_path"), ITEM_COPY_PATH) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate("buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.copy_file_path"), ITEM_COPY_PATH) | ||||
| 	files_popup_menu.add_item(DialogueConstants.translate(&"buffer.show_in_filesystem"), ITEM_SHOW_IN_FILESYSTEM) | ||||
| 
 | ||||
| 
 | ||||
| func _on_files_popup_menu_id_pressed(id: int) -> void: | ||||
|  | @ -984,3 +1093,8 @@ func _on_close_confirmation_dialog_custom_action(action: StringName) -> void: | |||
| 	if action == "discard": | ||||
| 		remove_file_from_open_buffers(current_file_path) | ||||
| 	close_confirmation_dialog.hide() | ||||
| 
 | ||||
| 
 | ||||
| func _on_find_in_files_result_selected(path: String, cursor: Vector2, length: int) -> void: | ||||
| 	open_file(path) | ||||
| 	code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length) | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| [gd_scene load_steps=13 format=3 uid="uid://cbuf1q3xsse3q"] | ||||
| [gd_scene load_steps=14 format=3 uid="uid://cbuf1q3xsse3q"] | ||||
| 
 | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/views/main_view.gd" id="1_h6qfq"] | ||||
| [ext_resource type="PackedScene" uid="uid://civ6shmka5e8u" path="res://addons/dialogue_manager/components/code_edit.tscn" id="2_f73fm"] | ||||
|  | @ -7,9 +7,11 @@ | |||
| [ext_resource type="PackedScene" uid="uid://co8yl23idiwbi" path="res://addons/dialogue_manager/components/update_button.tscn" id="2_ph3vs"] | ||||
| [ext_resource type="PackedScene" uid="uid://gr8nakpbrhby" path="res://addons/dialogue_manager/components/search_and_replace.tscn" id="6_ylh0t"] | ||||
| [ext_resource type="PackedScene" uid="uid://cs8pwrxr5vxix" path="res://addons/dialogue_manager/components/errors_panel.tscn" id="7_5cvl4"] | ||||
| [ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="7_necsa"] | ||||
| [ext_resource type="PackedScene" uid="uid://cpg4lg1r3ff6m" path="res://addons/dialogue_manager/views/settings_view.tscn" id="9_8bf36"] | ||||
| [ext_resource type="PackedScene" uid="uid://0n7hwviyyly4" path="res://addons/dialogue_manager/components/find_in_files.tscn" id="10_yold3"] | ||||
| 
 | ||||
| [sub_resource type="Image" id="Image_0jq4a"] | ||||
| [sub_resource type="Image" id="Image_xvtti"] | ||||
| data = { | ||||
| "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), | ||||
| "format": "RGBA8", | ||||
|  | @ -19,19 +21,10 @@ data = { | |||
| } | ||||
| 
 | ||||
| [sub_resource type="ImageTexture" id="ImageTexture_fguub"] | ||||
| image = SubResource("Image_0jq4a") | ||||
| image = SubResource("Image_xvtti") | ||||
| 
 | ||||
| [sub_resource type="Image" id="Image_vtxr5"] | ||||
| data = { | ||||
| "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), | ||||
| "format": "RGBA8", | ||||
| "height": 16, | ||||
| "mipmaps": false, | ||||
| "width": 16 | ||||
| } | ||||
| 
 | ||||
| [sub_resource type="ImageTexture" id="ImageTexture_wbkwf"] | ||||
| image = SubResource("Image_vtxr5") | ||||
| [sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_dvnxt"] | ||||
| script = ExtResource("7_necsa") | ||||
| 
 | ||||
| [node name="MainView" type="Control"] | ||||
| layout_mode = 3 | ||||
|  | @ -77,29 +70,12 @@ layout_mode = 2 | |||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| tooltip_text = "Start a new file" | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| flat = true | ||||
| 
 | ||||
| [node name="OpenButton" type="MenuButton" parent="Margin/Content/SidePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| tooltip_text = "Open a file" | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| item_count = 5 | ||||
| popup/item_0/text = "Open..." | ||||
| popup/item_0/icon = SubResource("ImageTexture_wbkwf") | ||||
| popup/item_0/id = 0 | ||||
| popup/item_1/text = "" | ||||
| popup/item_1/id = -1 | ||||
| popup/item_1/separator = true | ||||
| popup/item_2/text = "res://examples/dialogue.dialogue" | ||||
| popup/item_2/icon = SubResource("ImageTexture_wbkwf") | ||||
| popup/item_2/id = 2 | ||||
| popup/item_3/text = "" | ||||
| popup/item_3/id = -1 | ||||
| popup/item_3/separator = true | ||||
| popup/item_4/text = "Clear recent files" | ||||
| popup/item_4/id = 4 | ||||
| 
 | ||||
| [node name="SaveAllButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
|  | @ -107,6 +83,11 @@ layout_mode = 2 | |||
| disabled = true | ||||
| flat = true | ||||
| 
 | ||||
| [node name="FindInFilesButton" type="Button" parent="Margin/Content/SidePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| flat = true | ||||
| 
 | ||||
| [node name="Bookmarks" type="VSplitContainer" parent="Margin/Content/SidePanel"] | ||||
| layout_mode = 2 | ||||
| size_flags_vertical = 3 | ||||
|  | @ -138,33 +119,12 @@ unique_name_in_owner = true | |||
| layout_mode = 2 | ||||
| disabled = true | ||||
| text = "Insert" | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| item_count = 6 | ||||
| popup/item_0/text = "Wave BBCode" | ||||
| popup/item_0/icon = SubResource("ImageTexture_fguub") | ||||
| popup/item_0/id = 0 | ||||
| popup/item_1/text = "Shake BBCode" | ||||
| popup/item_1/icon = SubResource("ImageTexture_fguub") | ||||
| popup/item_1/id = 1 | ||||
| popup/item_2/text = "" | ||||
| popup/item_2/id = -1 | ||||
| popup/item_2/separator = true | ||||
| popup/item_3/text = "Typing pause" | ||||
| popup/item_3/icon = SubResource("ImageTexture_fguub") | ||||
| popup/item_3/id = 3 | ||||
| popup/item_4/text = "Typing speed change" | ||||
| popup/item_4/icon = SubResource("ImageTexture_fguub") | ||||
| popup/item_4/id = 4 | ||||
| popup/item_5/text = "Auto advance" | ||||
| popup/item_5/icon = SubResource("ImageTexture_fguub") | ||||
| popup/item_5/id = 5 | ||||
| 
 | ||||
| [node name="TranslationsButton" type="MenuButton" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| disabled = true | ||||
| text = "Translations" | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| item_count = 6 | ||||
| popup/item_0/text = "Generate line IDs" | ||||
| popup/item_0/icon = SubResource("ImageTexture_fguub") | ||||
|  | @ -194,7 +154,6 @@ layout_mode = 2 | |||
| tooltip_text = "Search for text" | ||||
| disabled = true | ||||
| toggle_mode = true | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| flat = true | ||||
| 
 | ||||
| [node name="TestButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] | ||||
|  | @ -202,35 +161,40 @@ unique_name_in_owner = true | |||
| layout_mode = 2 | ||||
| tooltip_text = "Test dialogue" | ||||
| disabled = true | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| flat = true | ||||
| 
 | ||||
| [node name="Separator3" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="SettingsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| tooltip_text = "Settings" | ||||
| flat = true | ||||
| 
 | ||||
| [node name="Spacer2" type="Control" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| layout_mode = 2 | ||||
| size_flags_horizontal = 3 | ||||
| 
 | ||||
| [node name="SettingsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| [node name="SupportButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| tooltip_text = "Settings" | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| flat = true | ||||
| 
 | ||||
| [node name="Separator3" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| [node name="Separator4" type="VSeparator" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="DocsButton" type="Button" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| layout_mode = 2 | ||||
| text = "Docs" | ||||
| icon = SubResource("ImageTexture_fguub") | ||||
| flat = true | ||||
| 
 | ||||
| [node name="VersionLabel" type="Label" parent="Margin/Content/CodePanel/Toolbar"] | ||||
| unique_name_in_owner = true | ||||
| modulate = Color(1, 1, 1, 0.490196) | ||||
| layout_mode = 2 | ||||
| text = "v2.15.2" | ||||
| text = "v2.19.0" | ||||
| vertical_alignment = 1 | ||||
| 
 | ||||
| [node name="UpdateButton" parent="Margin/Content/CodePanel/Toolbar" instance=ExtResource("2_ph3vs")] | ||||
|  | @ -239,7 +203,6 @@ layout_mode = 2 | |||
| 
 | ||||
| [node name="SearchAndReplace" parent="Margin/Content/CodePanel" instance=ExtResource("6_ylh0t")] | ||||
| unique_name_in_owner = true | ||||
| visible = false | ||||
| layout_mode = 2 | ||||
| 
 | ||||
| [node name="CodeEdit" parent="Margin/Content/CodePanel" instance=ExtResource("2_f73fm")] | ||||
|  | @ -250,6 +213,7 @@ size_flags_horizontal = 3 | |||
| size_flags_vertical = 3 | ||||
| theme_override_colors/bookmark_color = Color(1, 0.333333, 0.333333, 1) | ||||
| text = "" | ||||
| syntax_highlighter = SubResource("SyntaxHighlighter_dvnxt") | ||||
| 
 | ||||
| [node name="ErrorsPanel" parent="Margin/Content/CodePanel" instance=ExtResource("7_5cvl4")] | ||||
| unique_name_in_owner = true | ||||
|  | @ -281,8 +245,11 @@ size = Vector2i(600, 500) | |||
| min_size = Vector2i(600, 500) | ||||
| 
 | ||||
| [node name="ImportDialog" type="FileDialog" parent="."] | ||||
| title = "Open a File" | ||||
| size = Vector2i(600, 500) | ||||
| min_size = Vector2i(600, 500) | ||||
| ok_button_text = "Open" | ||||
| file_mode = 0 | ||||
| filters = PackedStringArray("*.csv ; Translation CSV") | ||||
| 
 | ||||
| [node name="ErrorsDialog" type="AcceptDialog" parent="."] | ||||
|  | @ -292,6 +259,7 @@ dialog_text = "You have errors in your script. Fix them and then try again." | |||
| [node name="SettingsDialog" type="AcceptDialog" parent="."] | ||||
| title = "Settings" | ||||
| size = Vector2i(834, 600) | ||||
| unresizable = true | ||||
| min_size = Vector2i(600, 600) | ||||
| ok_button_text = "Done" | ||||
| 
 | ||||
|  | @ -314,12 +282,28 @@ title = "Updated" | |||
| size = Vector2i(191, 100) | ||||
| dialog_text = "You're now up to date!" | ||||
| 
 | ||||
| [node name="FindInFilesDialog" type="AcceptDialog" parent="."] | ||||
| title = "Find in files" | ||||
| size = Vector2i(416, 457) | ||||
| ok_button_text = "Done" | ||||
| 
 | ||||
| [node name="FindInFiles" parent="FindInFilesDialog" node_paths=PackedStringArray("main_view", "code_edit") instance=ExtResource("10_yold3")] | ||||
| custom_minimum_size = Vector2(400, 400) | ||||
| offset_left = 8.0 | ||||
| offset_top = 8.0 | ||||
| offset_right = -8.0 | ||||
| offset_bottom = -49.0 | ||||
| main_view = NodePath("../..") | ||||
| code_edit = NodePath("../../Margin/Content/CodePanel/CodeEdit") | ||||
| 
 | ||||
| [connection signal="theme_changed" from="." to="." method="_on_main_view_theme_changed"] | ||||
| [connection signal="visibility_changed" from="." to="." method="_on_main_view_visibility_changed"] | ||||
| [connection signal="timeout" from="ParseTimer" to="." method="_on_parse_timer_timeout"] | ||||
| [connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/NewButton" to="." method="_on_new_button_pressed"] | ||||
| [connection signal="about_to_popup" from="Margin/Content/SidePanel/Toolbar/OpenButton" to="." method="_on_open_button_about_to_popup"] | ||||
| [connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/SaveAllButton" to="." method="_on_save_all_button_pressed"] | ||||
| [connection signal="pressed" from="Margin/Content/SidePanel/Toolbar/FindInFilesButton" to="." method="_on_find_in_files_button_pressed"] | ||||
| [connection signal="file_middle_clicked" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_middle_clicked"] | ||||
| [connection signal="file_popup_menu_requested" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_popup_menu_requested"] | ||||
| [connection signal="file_selected" from="Margin/Content/SidePanel/Bookmarks/FilesList" to="." method="_on_files_list_file_selected"] | ||||
| [connection signal="about_to_popup" from="Margin/Content/SidePanel/Bookmarks/FilesList/FilesPopupMenu" to="." method="_on_files_popup_menu_about_to_popup"] | ||||
|  | @ -328,6 +312,7 @@ dialog_text = "You're now up to date!" | |||
| [connection signal="toggled" from="Margin/Content/CodePanel/Toolbar/SearchButton" to="." method="_on_search_button_toggled"] | ||||
| [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/TestButton" to="." method="_on_test_button_pressed"] | ||||
| [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SettingsButton" to="." method="_on_settings_button_pressed"] | ||||
| [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/SupportButton" to="." method="_on_support_button_pressed"] | ||||
| [connection signal="pressed" from="Margin/Content/CodePanel/Toolbar/DocsButton" to="." method="_on_docs_button_pressed"] | ||||
| [connection signal="close_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_close_requested"] | ||||
| [connection signal="open_requested" from="Margin/Content/CodePanel/SearchAndReplace" to="." method="_on_search_and_replace_open_requested"] | ||||
|  | @ -346,3 +331,4 @@ dialog_text = "You're now up to date!" | |||
| [connection signal="script_button_pressed" from="SettingsDialog/SettingsView" to="." method="_on_settings_view_script_button_pressed"] | ||||
| [connection signal="confirmed" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_confirmed"] | ||||
| [connection signal="custom_action" from="CloseConfirmationDialog" to="." method="_on_close_confirmation_dialog_custom_action"] | ||||
| [connection signal="result_selected" from="FindInFilesDialog/FindInFiles" to="." method="_on_find_in_files_result_selected"] | ||||
|  |  | |||
|  | @ -1,73 +1,142 @@ | |||
| @tool | ||||
| extends VBoxContainer | ||||
| extends TabContainer | ||||
| 
 | ||||
| 
 | ||||
| signal script_button_pressed(path: String) | ||||
| 
 | ||||
| 
 | ||||
| const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") | ||||
| const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd") | ||||
| const DialogueConstants = preload("../constants.gd") | ||||
| const DialogueSettings = preload("../settings.gd") | ||||
| 
 | ||||
| 
 | ||||
| const DEFAULT_TEST_SCENE_PATH = "res://addons/dialogue_manager/test_scene.tscn" | ||||
| enum PathTarget { | ||||
| 	CustomTestScene, | ||||
| 	Balloon | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @onready var new_template_button: CheckBox = $NewTemplateButton | ||||
| @onready var missing_translations_button: CheckBox = $MissingTranslationsButton | ||||
| @onready var wrap_lines_button: Button = $WrapLinesButton | ||||
| @onready var test_scene_path_input: LineEdit = $CustomTestScene/TestScenePath | ||||
| @onready var revert_test_scene_button: Button = $CustomTestScene/RevertTestScene | ||||
| @onready var load_test_scene_button: Button = $CustomTestScene/LoadTestScene | ||||
| # Editor | ||||
| @onready var new_template_button: CheckBox = $Editor/NewTemplateButton | ||||
| @onready var characters_translations_button: CheckBox = $Editor/CharactersTranslationsButton | ||||
| @onready var wrap_lines_button: Button = $Editor/WrapLinesButton | ||||
| @onready var default_csv_locale: LineEdit = $Editor/DefaultCSVLocale | ||||
| 
 | ||||
| # Runtime | ||||
| @onready var include_all_responses_button: CheckBox = $Runtime/IncludeAllResponsesButton | ||||
| @onready var ignore_missing_state_values: CheckBox = $Runtime/IgnoreMissingStateValues | ||||
| @onready var balloon_path_input: LineEdit = $Runtime/CustomBalloon/BalloonPath | ||||
| @onready var revert_balloon_button: Button = $Runtime/CustomBalloon/RevertBalloonPath | ||||
| @onready var load_balloon_button: Button = $Runtime/CustomBalloon/LoadBalloonPath | ||||
| @onready var states_title: Label = $Runtime/StatesTitle | ||||
| @onready var globals_list: Tree = $Runtime/GlobalsList | ||||
| 
 | ||||
| # Advanced | ||||
| @onready var check_for_updates: CheckBox = $Advanced/CheckForUpdates | ||||
| @onready var include_characters_in_translations: CheckBox = $Advanced/IncludeCharactersInTranslations | ||||
| @onready var include_notes_in_translations: CheckBox = $Advanced/IncludeNotesInTranslations | ||||
| @onready var open_in_external_editor_button: CheckBox = $Advanced/OpenInExternalEditorButton | ||||
| @onready var test_scene_path_input: LineEdit = $Advanced/CustomTestScene/TestScenePath | ||||
| @onready var revert_test_scene_button: Button = $Advanced/CustomTestScene/RevertTestScene | ||||
| @onready var load_test_scene_button: Button = $Advanced/CustomTestScene/LoadTestScene | ||||
| @onready var custom_test_scene_file_dialog: FileDialog = $CustomTestSceneFileDialog | ||||
| @onready var include_all_responses_button: Button = $IncludeAllResponsesButton | ||||
| @onready var states_title: Label = $StatesTitle | ||||
| @onready var globals_list: Tree = $GlobalsList | ||||
| @onready var create_lines_for_response_characters: CheckBox = $Advanced/CreateLinesForResponseCharacters | ||||
| @onready var missing_translations_button: CheckBox = $Advanced/MissingTranslationsButton | ||||
| 
 | ||||
| var editor_plugin: EditorPlugin | ||||
| var all_globals: Dictionary = {} | ||||
| var enabled_globals: Array = [] | ||||
| var path_target: PathTarget = PathTarget.CustomTestScene | ||||
| 
 | ||||
| var _default_test_scene_path: String = preload("../test_scene.tscn").resource_path | ||||
| 
 | ||||
| var _recompile_if_changed_settings: Dictionary | ||||
| 
 | ||||
| 
 | ||||
| func _ready() -> void: | ||||
| 	$NewTemplateButton.text = DialogueConstants.translate("settings.new_template") | ||||
| 	$MissingTranslationsButton.text = DialogueConstants.translate("settings.missing_keys") | ||||
| 	$MissingTranslationsHint.text = DialogueConstants.translate("settings.missing_keys_hint") | ||||
| 	$WrapLinesButton.text = DialogueConstants.translate("settings.wrap_long_lines") | ||||
| 	$IncludeAllResponsesButton.text = DialogueConstants.translate("settings.include_failed_responses") | ||||
| 	$CustomTestSceneLabel.text = DialogueConstants.translate("settings.custom_test_scene") | ||||
| 	$StatesTitle.text = DialogueConstants.translate("settings.states_shortcuts") | ||||
| 	$StatesMessage.text = DialogueConstants.translate("settings.states_message") | ||||
| 	$StatesHint.text = DialogueConstants.translate("settings.states_hint") | ||||
| 	new_template_button.text = DialogueConstants.translate(&"settings.new_template") | ||||
| 	$Editor/MissingTranslationsHint.text = DialogueConstants.translate(&"settings.missing_keys_hint") | ||||
| 	characters_translations_button.text = DialogueConstants.translate(&"settings.characters_translations") | ||||
| 	wrap_lines_button.text = DialogueConstants.translate(&"settings.wrap_long_lines") | ||||
| 	$Editor/DefaultCSVLocaleLabel.text = DialogueConstants.translate(&"settings.default_csv_locale") | ||||
| 
 | ||||
| 	include_all_responses_button.text = DialogueConstants.translate(&"settings.include_failed_responses") | ||||
| 	ignore_missing_state_values.text = DialogueConstants.translate(&"settings.ignore_missing_state_values") | ||||
| 	$Runtime/CustomBalloonLabel.text = DialogueConstants.translate(&"settings.default_balloon_hint") | ||||
| 	states_title.text = DialogueConstants.translate(&"settings.states_shortcuts") | ||||
| 	$Runtime/StatesMessage.text = DialogueConstants.translate(&"settings.states_message") | ||||
| 	$Runtime/StatesHint.text = DialogueConstants.translate(&"settings.states_hint") | ||||
| 
 | ||||
| 	check_for_updates.text = DialogueConstants.translate(&"settings.check_for_updates") | ||||
| 	include_characters_in_translations.text = DialogueConstants.translate(&"settings.include_characters_in_translations") | ||||
| 	include_notes_in_translations.text = DialogueConstants.translate(&"settings.include_notes_in_translations") | ||||
| 	open_in_external_editor_button.text = DialogueConstants.translate(&"settings.open_in_external_editor") | ||||
| 	$Advanced/ExternalWarning.text = DialogueConstants.translate(&"settings.external_editor_warning") | ||||
| 	$Advanced/CustomTestSceneLabel.text = DialogueConstants.translate(&"settings.custom_test_scene") | ||||
| 	$Advanced/RecompileWarning.text = DialogueConstants.translate(&"settings.recompile_warning") | ||||
| 	missing_translations_button.text = DialogueConstants.translate(&"settings.missing_keys") | ||||
| 	create_lines_for_response_characters.text = DialogueConstants.translate(&"settings.create_lines_for_responses_with_characters") | ||||
| 
 | ||||
| 	current_tab = 0 | ||||
| 
 | ||||
| 
 | ||||
| func prepare() -> void: | ||||
| 	test_scene_path_input.placeholder_text = DialogueSettings.get_setting("custom_test_scene_path", DEFAULT_TEST_SCENE_PATH) | ||||
| 	revert_test_scene_button.visible = test_scene_path_input.placeholder_text != DEFAULT_TEST_SCENE_PATH | ||||
| 	_recompile_if_changed_settings = _get_settings_that_require_recompilation() | ||||
| 
 | ||||
| 	test_scene_path_input.placeholder_text = DialogueSettings.get_setting("custom_test_scene_path", _default_test_scene_path) | ||||
| 	revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path | ||||
| 	revert_test_scene_button.icon = get_theme_icon("RotateLeft", "EditorIcons") | ||||
| 	revert_test_scene_button.tooltip_text = DialogueConstants.translate("settings.revert_to_default_test_scene") | ||||
| 	revert_test_scene_button.tooltip_text = DialogueConstants.translate(&"settings.revert_to_default_test_scene") | ||||
| 	load_test_scene_button.icon = get_theme_icon("Load", "EditorIcons") | ||||
| 	 | ||||
| 
 | ||||
| 	var balloon_path: String = DialogueSettings.get_setting("balloon_path", "") | ||||
| 	if not FileAccess.file_exists(balloon_path): | ||||
| 		DialogueSettings.set_setting("balloon_path", "") | ||||
| 		balloon_path = "" | ||||
| 	balloon_path_input.placeholder_text = balloon_path if balloon_path != "" else DialogueConstants.translate(&"settings.default_balloon_path") | ||||
| 	revert_balloon_button.visible = balloon_path != "" | ||||
| 	revert_balloon_button.icon = get_theme_icon("RotateLeft", "EditorIcons") | ||||
| 	revert_balloon_button.tooltip_text = DialogueConstants.translate(&"settings.revert_to_default_balloon") | ||||
| 	load_balloon_button.icon = get_theme_icon("Load", "EditorIcons") | ||||
| 
 | ||||
| 	var scale: float = editor_plugin.get_editor_interface().get_editor_scale() | ||||
| 	custom_test_scene_file_dialog.min_size = Vector2(600, 500) * scale | ||||
| 	 | ||||
| 
 | ||||
| 	states_title.add_theme_font_override("font", get_theme_font("bold", "EditorFonts")) | ||||
| 	 | ||||
| 	missing_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("missing_translations_are_errors", false)) | ||||
| 
 | ||||
| 	check_for_updates.set_pressed_no_signal(DialogueSettings.get_user_value("check_for_updates", true)) | ||||
| 	characters_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("export_characters_in_translation", true)) | ||||
| 	wrap_lines_button.set_pressed_no_signal(DialogueSettings.get_setting("wrap_lines", false)) | ||||
| 	include_all_responses_button.set_pressed_no_signal(DialogueSettings.get_setting("include_all_responses", false)) | ||||
| 	ignore_missing_state_values.set_pressed_no_signal(DialogueSettings.get_setting("ignore_missing_state_values", false)) | ||||
| 	new_template_button.set_pressed_no_signal(DialogueSettings.get_setting("new_with_template", true)) | ||||
| 	 | ||||
| 	default_csv_locale.text = DialogueSettings.get_setting("default_csv_locale", "en") | ||||
| 
 | ||||
| 	missing_translations_button.set_pressed_no_signal(DialogueSettings.get_setting("missing_translations_are_errors", false)) | ||||
| 	create_lines_for_response_characters.set_pressed_no_signal(DialogueSettings.get_setting("create_lines_for_responses_with_characters", true)) | ||||
| 
 | ||||
| 	include_characters_in_translations.set_pressed_no_signal(DialogueSettings.get_setting("include_character_in_translation_exports", false)) | ||||
| 	include_notes_in_translations.set_pressed_no_signal(DialogueSettings.get_setting("include_notes_in_translation_exports", false)) | ||||
| 	open_in_external_editor_button.set_pressed_no_signal(DialogueSettings.get_user_value("open_in_external_editor", false)) | ||||
| 
 | ||||
| 	var editor_settings: EditorSettings = editor_plugin.get_editor_interface().get_editor_settings() | ||||
| 	var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path") | ||||
| 	var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != "" | ||||
| 	if not use_external_editor: | ||||
| 		open_in_external_editor_button.hide() | ||||
| 		$Advanced/ExternalWarning.hide() | ||||
| 		$Advanced/ExternalSeparator.hide() | ||||
| 
 | ||||
| 	var project = ConfigFile.new() | ||||
| 	var err = project.load("res://project.godot") | ||||
| 	assert(err == OK, "Could not find the project file") | ||||
| 	 | ||||
| 
 | ||||
| 	all_globals.clear() | ||||
| 	if project.has_section("autoload"): | ||||
| 		for key in project.get_section_keys("autoload"): | ||||
| 			if key != "DialogueManager": | ||||
| 				all_globals[key] = project.get_value("autoload", key) | ||||
| 	 | ||||
| 	enabled_globals = DialogueSettings.get_setting("states", []) | ||||
| 
 | ||||
| 	enabled_globals = DialogueSettings.get_setting("states", []).duplicate() | ||||
| 	globals_list.clear() | ||||
| 	var root = globals_list.create_item() | ||||
| 	for name in all_globals.keys(): | ||||
|  | @ -77,46 +146,58 @@ func prepare() -> void: | |||
| 		item.set_text(0, name) | ||||
| 		item.add_button(1, get_theme_icon("Edit", "EditorIcons")) | ||||
| 		item.set_text(2, all_globals.get(name, "").replace("*res://", "res://")) | ||||
| 	 | ||||
| 
 | ||||
| 	globals_list.set_column_expand(0, false) | ||||
| 	globals_list.set_column_custom_minimum_width(0, 250) | ||||
| 	globals_list.set_column_expand(1, false) | ||||
| 	globals_list.set_column_custom_minimum_width(1, 40) | ||||
| 	globals_list.set_column_titles_visible(true) | ||||
| 	globals_list.set_column_title(0, DialogueConstants.translate("settings.autoload")) | ||||
| 	globals_list.set_column_title(0, DialogueConstants.translate(&"settings.autoload")) | ||||
| 	globals_list.set_column_title(1, "") | ||||
| 	globals_list.set_column_title(2, DialogueConstants.translate("settings.path")) | ||||
| 	globals_list.set_column_title(2, DialogueConstants.translate(&"settings.path")) | ||||
| 
 | ||||
| 
 | ||||
| func apply_settings_changes() -> void: | ||||
| 	if _recompile_if_changed_settings != _get_settings_that_require_recompilation(): | ||||
| 		Engine.get_meta("DialogueCache").reimport_files() | ||||
| 
 | ||||
| 
 | ||||
| func _get_settings_that_require_recompilation() -> Dictionary: | ||||
| 	return DialogueSettings.get_settings([ | ||||
| 		"missing_translations_are_errors", | ||||
| 		"create_lines_for_responses_with_characters" | ||||
| 	]) | ||||
| 
 | ||||
| 
 | ||||
| ### Signals | ||||
| 
 | ||||
| 
 | ||||
| func _on_settings_view_visibility_changed() -> void: | ||||
| 	prepare() | ||||
| func _on_missing_translations_button_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("missing_translations_are_errors", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_missing_translations_button_toggled(button_pressed: bool) -> void: | ||||
| 	DialogueSettings.set_setting("missing_translations_are_errors", button_pressed) | ||||
| func _on_characters_translations_button_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("export_characters_in_translation", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_wrap_lines_button_toggled(button_pressed: bool) -> void: | ||||
| 	DialogueSettings.set_setting("wrap_lines", button_pressed) | ||||
| func _on_wrap_lines_button_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("wrap_lines", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_include_all_responses_button_toggled(button_pressed: bool) -> void: | ||||
| 	DialogueSettings.set_setting("include_all_responses", button_pressed) | ||||
| func _on_include_all_responses_button_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("include_all_responses", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_globals_list_item_selected() -> void: | ||||
| 	var item = globals_list.get_selected() | ||||
| 	var is_checked = not item.is_checked(0)	 | ||||
| 	var is_checked = not item.is_checked(0) | ||||
| 	item.set_checked(0, is_checked) | ||||
| 	 | ||||
| 
 | ||||
| 	if is_checked: | ||||
| 		enabled_globals.append(item.get_text(0)) | ||||
| 	else: | ||||
| 		enabled_globals.erase(item.get_text(0)) | ||||
| 	 | ||||
| 
 | ||||
| 	DialogueSettings.set_setting("states", enabled_globals) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -124,21 +205,76 @@ func _on_globals_list_button_clicked(item: TreeItem, column: int, id: int, mouse | |||
| 	emit_signal("script_button_pressed", item.get_text(2)) | ||||
| 
 | ||||
| 
 | ||||
| func _on_sample_template_toggled(button_pressed): | ||||
| 	DialogueSettings.set_setting("new_with_template", button_pressed) | ||||
| func _on_sample_template_toggled(toggled_on): | ||||
| 	DialogueSettings.set_setting("new_with_template", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_revert_test_scene_pressed() -> void: | ||||
| 	DialogueSettings.set_setting("custom_test_scene_path", DEFAULT_TEST_SCENE_PATH) | ||||
| 	test_scene_path_input.placeholder_text = DEFAULT_TEST_SCENE_PATH | ||||
| 	revert_test_scene_button.visible = test_scene_path_input.placeholder_text != DEFAULT_TEST_SCENE_PATH | ||||
| 	DialogueSettings.set_setting("custom_test_scene_path", _default_test_scene_path) | ||||
| 	test_scene_path_input.placeholder_text = _default_test_scene_path | ||||
| 	revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path | ||||
| 
 | ||||
| 
 | ||||
| func _on_load_test_scene_pressed() -> void: | ||||
| 	path_target = PathTarget.CustomTestScene | ||||
| 	custom_test_scene_file_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
| func _on_custom_test_scene_file_dialog_file_selected(path: String) -> void: | ||||
| 	DialogueSettings.set_setting("custom_test_scene_path", path) | ||||
| 	test_scene_path_input.placeholder_text = path | ||||
| 	revert_test_scene_button.visible = test_scene_path_input.placeholder_text != DEFAULT_TEST_SCENE_PATH | ||||
| 	match path_target: | ||||
| 		PathTarget.CustomTestScene: | ||||
| 			# Check that the test scene is a subclass of BaseDialogueTestScene | ||||
| 			var test_scene: PackedScene = load(path) | ||||
| 			if test_scene and test_scene.instantiate() is BaseDialogueTestScene: | ||||
| 				DialogueSettings.set_setting("custom_test_scene_path", path) | ||||
| 				test_scene_path_input.placeholder_text = path | ||||
| 				revert_test_scene_button.visible = test_scene_path_input.placeholder_text != _default_test_scene_path | ||||
| 			else: | ||||
| 				var accept: AcceptDialog = AcceptDialog.new() | ||||
| 				accept.dialog_text = DialogueConstants.translate(&"settings.invalid_test_scene").format({ path = path }) | ||||
| 				add_child(accept) | ||||
| 				accept.popup_centered.call_deferred() | ||||
| 
 | ||||
| 		PathTarget.Balloon: | ||||
| 			DialogueSettings.set_setting("balloon_path", path) | ||||
| 			balloon_path_input.placeholder_text = path | ||||
| 			revert_balloon_button.visible = balloon_path_input.placeholder_text != "" | ||||
| 
 | ||||
| 
 | ||||
| func _on_ignore_missing_state_values_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("ignore_missing_state_values", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_default_csv_locale_text_changed(new_text: String) -> void: | ||||
| 	DialogueSettings.set_setting("default_csv_locale", new_text) | ||||
| 
 | ||||
| 
 | ||||
| func _on_revert_balloon_path_pressed() -> void: | ||||
| 	DialogueSettings.set_setting("balloon_path", "") | ||||
| 	balloon_path_input.placeholder_text = DialogueConstants.translate(&"settings.default_balloon_path") | ||||
| 	revert_balloon_button.visible = DialogueSettings.get_setting("balloon_path", "") != "" | ||||
| 
 | ||||
| 
 | ||||
| func _on_load_balloon_path_pressed() -> void: | ||||
| 	path_target = PathTarget.Balloon | ||||
| 	custom_test_scene_file_dialog.popup_centered() | ||||
| 
 | ||||
| 
 | ||||
| func _on_create_lines_for_response_characters_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("create_lines_for_responses_with_characters", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_open_in_external_editor_button_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_user_value("open_in_external_editor", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_include_characters_in_translations_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("include_character_in_translation_exports", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_include_notes_in_translations_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_setting("include_notes_in_translation_exports", toggled_on) | ||||
| 
 | ||||
| 
 | ||||
| func _on_keep_up_to_date_toggled(toggled_on: bool) -> void: | ||||
| 	DialogueSettings.set_user_value("check_for_updates", toggled_on) | ||||
|  |  | |||
											
												
													File diff suppressed because one or more lines are too long
												
											
										
									
								
		Loading…
	
		Reference in New Issue