From 39f791e2b4de21f554906ea360f644ae4b4923d4 Mon Sep 17 00:00:00 2001 From: Zenny Date: Fri, 27 Jun 2025 18:53:08 -0700 Subject: [PATCH] Fix dialogue manager plugin, lower resolution --- Zennysoft.Game.Ma/addons/.gdignore | 0 .../dialogue_manager/DialogueManager.cs | 953 ++++++++++-------- .../addons/dialogue_manager/assets/banner.png | Bin 0 -> 38619 bytes .../dialogue_manager/assets/banner.png.import | 34 + .../dialogue_manager/compiler/compilation.gd | 66 +- .../compiler/compiled_line.gd | 4 + .../compiler/compiler_regex.gd | 1 + .../compiler/expression_parser.gd | 72 +- .../dialogue_manager/compiler/tree_line.gd | 2 + .../dialogue_manager/components/code_edit.gd | 170 +++- .../code_edit_syntax_highlighter.gd | 28 +- .../components/find_in_files.gd | 2 +- .../components/search_and_replace.gd | 10 +- .../addons/dialogue_manager/constants.gd | 28 +- .../dialogue_manager/dialogue_manager.gd | 119 ++- .../dialogue_manager/dialogue_response.gd | 4 + .../dialogue_responses_menu.gd | 4 + .../example_balloon/ExampleBalloon.cs | 336 +++--- .../example_balloon/example_balloon.tscn | 67 +- .../small_example_balloon.tscn | 66 +- .../addons/dialogue_manager/export_plugin.gd | 10 +- .../addons/dialogue_manager/import_plugin.gd | 11 +- .../addons/dialogue_manager/l10n/en.po | 17 +- .../addons/dialogue_manager/l10n/es.po | 9 + .../dialogue_manager/l10n/translations.pot | 15 + .../addons/dialogue_manager/l10n/uk.po | 15 + .../addons/dialogue_manager/l10n/zh.po | 9 + .../addons/dialogue_manager/l10n/zh_TW.po | 9 + .../addons/dialogue_manager/plugin.cfg | 2 +- .../addons/dialogue_manager/plugin.gd | 18 +- .../addons/dialogue_manager/settings.gd | 83 +- .../utilities/dialogue_cache.gd | 1 + .../dialogue_manager/views/main_view.gd | 129 ++- .../dialogue_manager/views/main_view.tscn | 88 +- Zennysoft.Game.Ma/project.godot | 4 +- .../src/dialog/Dialogue.dialogue.import | 3 +- .../src/enemy/state/EnemyLogic.g.puml | 4 +- Zennysoft.Game.Ma/src/game/Game.tscn | 5 +- .../src/npc/Ran/ran.dialogue.import | 3 +- .../src/npc/Rat/ratdialogue.dialogue.import | 3 +- .../src/ui/in_game_ui/InGameUI.tscn | 2 +- .../src/ui/player_ui/PlayerInfoUI.tscn | 1 + .../ui/teleport_prompt/UseTeleportPrompt.tscn | 14 +- 43 files changed, 1565 insertions(+), 856 deletions(-) delete mode 100644 Zennysoft.Game.Ma/addons/.gdignore create mode 100644 Zennysoft.Game.Ma/addons/dialogue_manager/assets/banner.png create mode 100644 Zennysoft.Game.Ma/addons/dialogue_manager/assets/banner.png.import diff --git a/Zennysoft.Game.Ma/addons/.gdignore b/Zennysoft.Game.Ma/addons/.gdignore deleted file mode 100644 index e69de29b..00000000 diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs b/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs index b3189c77..04e29440 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/DialogueManager.cs @@ -1,6 +1,7 @@ using Godot; using Godot.Collections; using System; +using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -8,458 +9,568 @@ using System.Threading.Tasks; namespace DialogueManagerRuntime { - public enum TranslationSource - { - None, - Guess, - CSV, - PO - } - - public partial class DialogueManager : RefCounted - { - public delegate void DialogueStartedEventHandler(Resource dialogueResource); - public delegate void PassedTitleEventHandler(string title); - public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); - public delegate void MutatedEventHandler(Dictionary mutation); - public delegate void DialogueEndedEventHandler(Resource dialogueResource); - - public static DialogueStartedEventHandler? DialogueStarted; - 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 + public enum TranslationSource { - get - { - if (instance == null) + None, + Guess, + CSV, + PO + } + + public partial class DialogueManager : RefCounted + { + public delegate void DialogueStartedEventHandler(Resource dialogueResource); + public delegate void PassedTitleEventHandler(string title); + public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); + public delegate void MutatedEventHandler(Dictionary mutation); + public delegate void DialogueEndedEventHandler(Resource dialogueResource); + + public static DialogueStartedEventHandler? DialogueStarted; + 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 { - instance = Engine.GetSingleton("DialogueManager"); - instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); + get + { + if (instance == null) + { + instance = Engine.GetSingleton("DialogueManager"); + instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); + } + return instance; + } } - 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 GetCurrentScene - { - set => Instance.Set("get_current_scene", Callable.From(value)); - } - - - public static void Prepare(GodotObject instance) - { - 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 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 Resource CreateResourceFromText(string text) - { - return (Resource)Instance.Call("create_resource_from_text", text); - } - - public static async Task GetNextDialogueLine(Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - var instance = (Node)Instance.Call("_bridge_get_new_instance"); - Prepare(instance); - instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array()); - var result = await instance.ToSignal(instance, "bridge_get_next_dialogue_line_completed"); - instance.QueueFree(); - - if ((RefCounted)result[0] == null) - return null; - - return new DialogueLine((RefCounted)result[0]); - } - - - public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); - } - - - public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); - } - - public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); - } - - public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); - } - - - public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) - { - return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); - } - - - public static async void Mutate(Dictionary mutation, Array? extraGameStates = null, bool isInlineMutation = false) - { - Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array(), isInlineMutation); - await Instance.ToSignal(Instance, "bridge_mutated"); - } - - - public bool ThingHasMethod(GodotObject thing, string method, Array args) - { - var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); - foreach (var methodInfo in methodInfos) - { - if (methodInfo.Name == method && args.Count == methodInfo.GetParameters().Length) + public static Godot.Collections.Array GameStates { - return true; + get => (Godot.Collections.Array)Instance.Get("game_states"); + set => Instance.Set("game_states", value); } - } - - return false; - } - public async void ResolveThingMethod(GodotObject thing, string method, Array args) - { - MethodInfo? info = null; - var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); - foreach (var methodInfo in methodInfos) - { - if (methodInfo.Name == method && args.Count == methodInfo.GetParameters().Length) + public static bool IncludeSingletons { - info = methodInfo; + get => (bool)Instance.Get("include_singletons"); + set => Instance.Set("include_singletons", value); } - } - if (info == null) - return; + + 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 GetCurrentScene + { + set => Instance.Set("get_current_scene", Callable.From(value)); + } + + + public static void Prepare(GodotObject instance) + { + 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 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 Resource CreateResourceFromText(string text) + { + return (Resource)Instance.Call("create_resource_from_text", text); + } + + public static async Task GetNextDialogueLine(Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + var instance = (Node)Instance.Call("_bridge_get_new_instance"); + Prepare(instance); + instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array()); + var result = await instance.ToSignal(instance, "bridge_get_next_dialogue_line_completed"); + instance.QueueFree(); + + if ((RefCounted)result[0] == null) return null; + + return new DialogueLine((RefCounted)result[0]); + } + + + public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); + } + + + public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); + } + + public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); + } + + public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); + } + + + public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) + { + return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); + } + + + public static Array StaticIdToLineIds(Resource dialogueResource, string staticId) + { + return (Array)Instance.Call("static_id_to_line_ids", dialogueResource, staticId); + } + + + public static string StaticIdToLineId(Resource dialogueResource, string staticId) + { + return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId); + } + + + public static async void Mutate(Dictionary mutation, Array? extraGameStates = null, bool isInlineMutation = false) + { + Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array(), isInlineMutation); + await Instance.ToSignal(Instance, "bridge_mutated"); + } + + + public static Array GetMembersForAutoload(Script script) + { + Array members = new Array(); + + string typeName = script.ResourcePath.GetFile().GetBaseName(); + var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName); + foreach (var matchingType in matchingTypes) + { + var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var memberInfo in memberInfos) + { + string type; + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + FieldInfo fieldInfo = memberInfo as FieldInfo; + + if (fieldInfo.FieldType.ToString().Contains("EventHandler")) + { + type = "signal"; + } + else if (fieldInfo.IsLiteral) + { + type = "constant"; + } + else + { + type = "property"; + } + break; + case MemberTypes.Method: + type = "method"; + break; + + default: + continue; + } + + members.Add(new Dictionary() { + { "name", memberInfo.Name }, + { "type", type } + }); + } + } + + return members; + } + + + public bool ThingHasConstant(GodotObject thing, string property) + { + var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var fieldInfo in fieldInfos) + { + if (fieldInfo.Name == property && fieldInfo.IsLiteral) + { + return true; + } + } + + return false; + } + + + public Variant ResolveThingConstant(GodotObject thing, string property) + { + var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var fieldInfo in fieldInfos) + { + if (fieldInfo.Name == property && fieldInfo.IsLiteral) + { + try + { + Variant value = fieldInfo.GetValue(thing) switch + { + int v => Variant.From((long)v), + float v => Variant.From((double)v), + System.String v => Variant.From((string)v), + _ => Variant.From(fieldInfo.GetValue(thing)) + }; + return value; + } + catch (Exception) + { + throw new Exception($"Constant {property} of type ${fieldInfo.GetValue(thing).GetType()} is not supported by Variant."); + } + } + } + + throw new Exception($"{property} is not a public constant on {thing}"); + } + + + public bool ThingHasMethod(GodotObject thing, string method, Array args) + { + var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var methodInfo in methodInfos) + { + if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count()) + { + return true; + } + } + + return false; + } + + + public async void ResolveThingMethod(GodotObject thing, string method, Array args) + { + MethodInfo? info = null; + var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var methodInfo in methodInfos) + { + if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count()) + { + info = methodInfo; + } + } + + 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++) - { - // check if args is assignable from derived type - if (i < args.Count && args[i].Obj != null) - { - if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType())) - { - _args[i] = args[i].Obj; - } - // fallback to assigning primitive types - else - { - _args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType); - } - } - else if (argTypes[i].DefaultValue != null) - { - _args[i] = argTypes[i].DefaultValue; - } - } + // 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++) + { + // check if args is assignable from derived type + if (i < args.Count && args[i].Obj != null) + { + if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType())) + { + _args[i] = args[i].Obj; + } + // fallback to assigning primitive types + else + { + _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); + // Add a single frame wait in case the method returns before signals can listen + await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame); - // invoke method and handle the result based on return type - object result = info.Invoke(thing, _args); + // invoke method and handle the result based on return type + object result = info.Invoke(thing, _args); - if (result is Task taskResult) - { - await taskResult; - try - { - Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); - EmitSignal(SignalName.Resolved, value); + if (result is Task taskResult) + { + await taskResult; + try + { + Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); + EmitSignal(SignalName.Resolved, value); + } + catch (Exception) + { + EmitSignal(SignalName.Resolved); + } + } + else + { + EmitSignal(SignalName.Resolved, (Variant)result); + } } - catch (Exception) - { - EmitSignal(SignalName.Resolved); - } - } - else - { - EmitSignal(SignalName.Resolved, (Variant)result); - } - } #nullable enable - } - - - public partial class DialogueLine : RefCounted - { - private string id = ""; - public string Id - { - get => id; - set => id = value; - } - - 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 responses = new Array(); - public Array 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 inline_mutations = new Array(); - public Array InlineMutations - { - get => inline_mutations; - } - - private Array concurrent_lines = new Array(); - public Array ConcurrentLines - { - get => concurrent_lines; - } - - private Array extra_game_states = new Array(); - public Array ExtraGameStates - { - get => extra_game_states; - } - - private Array tags = new Array(); - public Array 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)data.Get("inline_mutations"); - time = (string)data.Get("time"); - tags = (Array)data.Get("tags"); - - foreach (var concurrent_line_data in (Array)data.Get("concurrent_lines")) - { - concurrent_lines.Add(new DialogueLine(concurrent_line_data)); - } - - foreach (var response in (Array)data.Get("responses")) - { - responses.Add(new DialogueResponse(response)); - } } - public string GetTagValue(string tagName) + public partial class DialogueLine : RefCounted { - string wrapped = $"{tagName}="; - foreach (var tag in tags) - { - if (tag.StartsWith(wrapped)) + private string id = ""; + public string Id { - return tag.Substring(wrapped.Length); + get => id; + set => id = value; } - } - return ""; - } - public override string ToString() - { - switch (type) - { - case "dialogue": - return $""; - case "mutation": - return ""; - default: - return ""; - } - } - } - - - 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; - } - - private Array tags = new Array(); - public Array 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)data.Get("tags"); - } - - public string GetTagValue(string tagName) - { - string wrapped = $"{tagName}="; - foreach (var tag in tags) - { - if (tag.StartsWith(wrapped)) + private string type = "dialogue"; + public string Type { - return tag.Substring(wrapped.Length); + 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 responses = new Array(); + public Array 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 inline_mutations = new Array(); + public Array InlineMutations + { + get => inline_mutations; + } + + private Array concurrent_lines = new Array(); + public Array ConcurrentLines + { + get => concurrent_lines; + } + + private Array extra_game_states = new Array(); + public Array ExtraGameStates + { + get => extra_game_states; + } + + private Array tags = new Array(); + public Array Tags + { + get => tags; + } + + public DialogueLine(RefCounted data) + { + id = (string)data.Get("id"); + 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)data.Get("inline_mutations"); + time = (string)data.Get("time"); + tags = (Array)data.Get("tags"); + + foreach (var concurrent_line_data in (Array)data.Get("concurrent_lines")) + { + concurrent_lines.Add(new DialogueLine(concurrent_line_data)); + } + + foreach (var response in (Array)data.Get("responses")) + { + responses.Add(new DialogueResponse(response)); + } + } + + + 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() + { + switch (type) + { + case "dialogue": + return $""; + case "mutation": + return ""; + default: + return ""; + } } - } - return ""; } - public override string ToString() + + public partial class DialogueResponse : RefCounted { - return $" next_id; + set => next_id = value; + } + + private bool is_allowed = true; + public bool IsAllowed + { + get => is_allowed; + set => is_allowed = value; + } + + private string condition_as_text = ""; + public string ConditionAsText + { + get => condition_as_text; + set => condition_as_text = 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 tags = new Array(); + public Array 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)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 $"Rzg-CcsaySuwAci!)L?vL4Jhuxm) z?yBn3r%s0}DM+JyBK!ma0f7SgCZPfW0i_B2J&XVcymDn_H3Dyl4&St#As|ru{<|QD z?2Ao+n*=VBnl7sLW-jiAPNopcGk|uh7P7I_D<$$7lMQk5abXb3DNH!>1XTi zKHBrk-PfBP8l#DVMqgs$`NHec&(9zYFRP_TZRqR1(*JCnn!(I2zk&KW!|{oEs#ZFM z<}3MEC=i0lWj`o=={jOp$fV-Sh6whky5RnB9COy?YfcsAx+Ar$Mr@|@gmeBi)*(Iu zq`EV)y0cGx8U@08%zEN4CVC8!s!D49*E4m(5TkgGB)5GSW%0t zWFaPmX{v#v#meSV;?Gha6G7xIUwtlMMG<&03`hpigV$hlHvbsi)7FzCC{sa|_Yv%f zF(OQ3bdd4pMk=9)IACcc^noaYX%OD!nHG02LiAXry4)lKT=DyUO?TCk-#Jac?}bCi zqK8~zLaH-Tc4LNsrJ{N;Lyp4d?R9}iNSRc_h@!(##2MXs1KkM2s6&7SK8y0HP<9KX z|Ehw?*~HKd2_g?kL#ReI&X#UN1iBSOe&-1MI|?6ot}^C4Q{1otqL@qng?lCNrM%Ps z%$X*aydH{1sDhyoFyb$w04)C{dJ%-qOgV-T0{)!X<@`Szr;K0FSP;alcd%H2PK}Sb zDq=yZYfJFvkW1*enBNRsNa~?`)oJR10n==!1_sp8UvSu4B++xJLq)tOyRCxlejz84 zOQ_VJDuo+D2BDMGye+?=td2AUc=J+SvW))%75MXgLD3tKoJUyAojig1_tZDMv$HG zLcU=E6C?VI8TfTPMG%wd5iPQK?6LlI&{Ub^7vJ?@iGX=j`CG~V21_E~i5(aYtWgoW z|L(3&<*vhs9*N{WF{(3Nu^>!SLzVkU&q@LYzX$#|jCa3~)kX48|C`+xXyrsn^pH$) zsB*WX&_Ie>%^>t!iN+u3xwN5Jnv~rqze*$Z8-vg>lv{@hLzCG7SHd2pQ2`zUi2<}^ zgB7m@L(xi&VQ{aQ`#qSvpGO``Bo~GvwV(XaPllHZ`5H+B-6-)hTr-l`_i&b8KRFyN zCedG7srYb!v(a#9z9%cf0FR94(v3cjY$Lyet$8EBhE%8J8poTphY&rgl5qQh?gdHj z)}=2QFpmfgt@bF@MgaIqN@@6H1G$9m*`Nks>CZCDaQk}7vNzH1z|Q2wQLgk z7LOQMST@&EN>{QbYdNp?3H2b(-2f@{sel>5upqoa3YOx;h`$-?_n=}r*1b6$h+|p2 zh|~MbGKxqb{%?rrw4F+o@@1)^Eclc9rJk6Ut7v^16@rjhoC$Gb{;%}WB2VZPGg0*1 zAvTv2-T7%dsn~F@0B*k}CKkbyBxHFKl5w*;uTaBk`VVbkt#?e6ixmrAY4l!(I7s!p zBE7jT%n-e=yj+Aj+!#XUUS5Q8)XM7W8@q=QKk#y>LFv4gpA}NP=;Np*%Lsf!sX=l? zo`S}3in|R}WQEy2=JjdR2);3eWt7c8GnAORArX|ErX$n`WI{<;3Vfj;tRYoR&9P%7 zmXm8|Y36PL?+4d(f%966jLu1?rMTvY`q!t)B(&-#8|(i%NAv{=*L)N^V_g?XGdZ+!!rZreaHx^LbcKT6-RP?OlFTkQD5$ zO)lSy66BdFiUz6H9>h1?s@{w?O19Ah(f-WETKaiV4^gaYCVG2OJk;cB(3+LaVye5_ zHzJqK`vDm@-H}02@R=#g7mU;9cAnGN7}K=j@ux(UmeczoijAk;nv&GOv_P@nEGG8O z(s+R%u>DxkF80}rQY`9px~U3-?uZdV>~ebfaUq3W~x)xfU7UU~0o@ihwHzVBGH%{*c_9b!1UKN)J=Ic8>Xi3G`ZeIMPvGPiamWo>fvG?zWeV# zEbo)!2717ZA3Ixs85(so-{gcA``^mO<-SbzuZ@n6wam%&c2~mxh%hp{+oBg*k`X=9 z+P);`a(E^j5Xb~FV17~2^~$N@?NFQDe*J7Le0Kd@R(S0XX1P?pobmU@%VCtYOg=|q z2-zg&!%!l7J7s0xb_yzLAi=nLFfz4SlmAeP!arMIq+Ih|GnHy+L_pqT;^Hs8Ay{oSZI&9k}(l61y<+sI7%2FD?y zi1~wVCq)>K__Q0J%vaLJ%?4v5*Vqbv!D(Krds8beo}y&rqGzqSp_|rr7q`eH#d2F^ zBUHmczV#%~(J>(WdCW@s7_4a0om8xPQ=WRSl)|H-frr57G^2*_cDu~0ncn;`D z3Fn{K#96}7LNp~#4DWyXZk2%}y}~7WSN-9nP z4c5FI_8UwpDsaCOxYK=KKmEvXe}5VOuUGv81MG%AJlaj?TcKI{1M<9jNKeq*l9phF zkCRh-t@fv~Y-5X5(rbnb^kd{inw;Bcuc<4`ieN^FNL{0q7BlsZ6B-rk9#s4l&r2I| zQwzvV2=7*e>JY!7DQ+EM+SH9(>qtZ~|7^NX<{7@9py3OzE|WH-7F{QV59_}=G`Jkg zG}^C9J~U6KJM>Ni@dH_HI*U3X@_~#kxm~0I{&jc9bwI`$_V@2z_O4>8U`$*bbh%y~ zYBOp8!L0KNa&_vUI^Bye4RUWfGJ14~I@KChc}PS24rP@x)&C_UtN})fmrkdhYKo9u7Q9 zX{*EKlGNO(%=~%ccVqy)#+7+8GjiGtA3M#^RUmQ1(KrvknjFwY@&6FDo1Pi83TDnK z;VWweLuu~NFg?Tsr;6_G{esnO>}P?|EXa8e+}*!`tqz)6#1D-bI>gjXEgYZH;l#Ezf5B`tb zups$5a3S%CfN$%OEX0@@)+6}1$>(`~_3dj)=%c#i1YA z)4b*_P0{mXUE>E1C2X}OlboG$hzof7w$(MWPZF3^F~Rj=)Di3@VodSRjqlAgdX%2O zL^K7y>ynj37LooXNlB787Gv7G1qK2n)qgtpNlepaKjT8RgHNcz!$)D7h7N(`o19#? z5u$>{o?ON^pY|?@!;tW0(O6Sc6zJ~r2c_@dKXEOVqzW#RMfpCi{{UZZ7eNy+#{ZL0 zn1Jzfn;&SuUZ@*OJ!fsGHtg_+%UZViWKx<6KsQ7Pt;7_5PI%~#)SxZamTI2?22%eW z0zapHlrfL=mj`H&M*~4|GYH!)uqQl>bi6bG1yB^?{E;zU~-v96nPa5~t zrKhvGt5;>^uPa`s#X9lNks^6>fA(^EVplP#3p0&(lC8(any2(Je8KZ(aIr~kqz0}k z0u(xO2NU{{O0*InDU(|JAapyD4(InkH4?k5y47F}*qkm+hXasL=N_}3@B49fwqgIT zvF>yIH6OL9t~9&DOGad$!A6@KcufKVXaYVqoB*_qkH2-3?Qz<6osoy&>rd7TPr-RW zn)QF$t@=$J?s}REUoe@%$y{uEy1<>Jm>kDR%BI91jk^$HQboRkbA!SF$qZSN0Jl|xRMYj6gJBo6K|=Ia1@d~OJM zr#d#>(89xqB|URRJTT#j4M9v}Zef9H6jMv9ulZnQ6zUO;h=;I~e}GZrOY? zg>)CuP>(s}p zFP~^{sq@&m4ucY%LmVQ5?#7c%gfeIv;L@o!Tv- zIHMZS4$nAvaVf|8AMV5GA4`R%qn+Z5D1*^teXRB^Z~=SN{uoz=k+<6RRUzPOwkwddt`Ea5 z%*K+HP0VBAwz5866u0=tQgPCKPh);fP|k_#5RGZs8id<_u#$to{yerlrYkxW8as_SYE@I)3F?7q_CRX`uOT^vl2mIpl+1`ftZlzMw=7 z^rG;B|F+=?k8JgTAx=BoMRJ;p|1$bHVc_XIv26Oi`jUTRU}i=l7N6yEa=sn@m(raN z0*{>ag`#YeO^O6f7S+0*DsXVX!NE0my>3bd2Aq*ic88lA@fkervy?#saX{(8^S zbzmEA0XJIKD!$TL(|T(wHLixwC(y3gr|-`A@=pka3mK2DjjY>OH8m93Ec}-q=ka54bFCXVSp%Kf>X1Ua0RM7dOkznp^rp|n?O*sur~X!m9`aAkig4ApRe>H zuSYkM2^7dx8WJ*1sfwFOokU?6ay_c_=`FUaLtZzdw%p)k#6>m-k7LS}L|$VW1?POq zmi<)`opN_xV`INJa-ttt{`V*Snb6fVH?Qe&Mmz{lGS%> z-^SnKOxDs&K2(MNk~N#_Jo8Rr^Imv9?#Ok z*P25VWw|*`2?W`9-9k=+cYk&#S2mh(!)g|op0P6RoVyEe(Q4?7m21tmoXtdfyhndP zRh!B>W6W8q7FAzegej>dkbNBaSXo(lKCZ{2+8$O_Bmj#Gu#Ilx@~#oWK&f7%Ft5&i zjO4M+kNPJQ%bbC=*@`XANHIy-zllP`sBc)CzxC^M6>?!4@+bC5cdMQZuS*o}$8l&( zxrm4@5NBW3eL8 z_d7RV_e!-Pl3i2SPOi$yDG}6axn@l&4x_qz3o||azqj2M>0v_t^6{a_2pP?reU(nl z8*T-2D&Z`&x@9>}Psy+zoUaQA{sC&Ls-tgthjA_zf&|^?UGFrT6+gy-03oLP@=Z;R zo1PvznT;at!}pEgaqX3jjP>O|gkM~K-blrk#>UNEa0#O+%w4Yq70o@3RBZyw zF-VVXi~4a!Gk~1f9Mh#Ds6bVJt?{+oSN=FO)bPkxfUd;db#2o3$=N!?@NY1}5LWju%%3ZRkXOu zJ?^&w{MN$i1@hR2bzBo~*7fcn2e3zprxFW(m7>G1K!~4D9TIrJasDpf^WHA>~j+>v$3N; zqPFX;w(z5g6((;_HR*0A;c}T=fojjv_j`3ia1N-$+pViWv7QW|q2gZtGQbR1PPL za@vXX%WD$4p*u~<0)!Sofnd?bST{^gtgWYyLQ#Xn7F`!ASBK+h zWS_l`n?zSl%}{&HZWqnHZpYm{!TKMFF2@5yL+oDPar2~O_u?X14enE2nRHvzfmOHt zb|Osr&Zmu`mVinB=x6`QXda#cWR$Q>kBzDT6CNZuRn7Q-(Ly+Qr-B4t`k;AY^3*YO zW<=cfs884OT$!6}VQLZK5=X)V2dd^%Ao<;|nHdgYum|E{rnkVRUtOv6#C2U6%V*~$ zbmYFRtjVqs4gE?0H?#S^n3P&uIkp51=@W(aRIUk{jg47yc3Y+XgLiloLc0dTN3IB7 zhFp)!f_?6;%|&+$2#IvxZZx`f$tDG3N=u39goa;G(s~m!P9lckG`)sYc~)nXaAyp4R$!oB?pn!yLOpz$%F$v09tobv3 z0F@!Hr<|_axlKJ}>}liyp+|6NUTo$-@6Xt3H~Cf9iqr)Tn_~By7wb#-6GLXb_WJR2 zNVkhSc+F+wp=>%CrGop4RktlS@9O39D2wR~S`g;$V01xFPGD{>=I6%Z#!h#8zcS~& z!%OoXEfLh?=TKFNDl6e}ET<-rujJ-SoC;HhM$qMm>=u%3?>`-vO=`{;ODSFq-fSuYc=@8%x_8)Sx2%>_u!5 z$(J`8asX52p`m=Y-?@EW@+mIwO<(5Zd#{i8xB|C5nH^IKdt9oKdhhu`grX369ci`% zYQ=PbOc+~;1cc=WY)-tgV(VtQ*rlx=SNuq4jwttewDLcx}^g;#s^ zVbG7(sS0|TBH6F2n_0WNv3R%ZrDUxid)`X@z)@zg)4p`uPfT*pLrW)nwYJO&@Y!mh% z>0(ZMj}(q26oP6K>KuQ!wmNxQPH4K>)BhMi+pHRz&s`H23(5czRe%V~o7$A{fMmhk z+*8r0VhTseJiGssq3)KOsVNSb?;U;9rX%kVu{#{nr6)o_P>@(6i#F*|+?g$<9z;)3 z=*If2k|nxG_AXL&<+HU0e;M-0C<@1*B4~Wl)2?GD zH|xqn_DRKy95xzY`mek-nas}^Mr8Ly&mYF2^j`wK?9!i{snURJTyD4gGDISR7I5nC z2OA!an)!ArGVMx*Zt$;mEdSrG=6hc#&*ZL^%k=>f*%fdBq!X2t35W3@c4g;-3P3{e zz29MZys=KgCQh;=2Uln}Ighd54yVird};I=_o{3IYkXH%|I=oznEhE_AM={7{}iQz zt99Y$R=p*%%e8vO$lHDY;#6mv4OlX}p%utcRVA3?Bhn^f+O;|)o0jbD6}4`f+{V&O?! zu>iRKaNY)mv*GNta{1W?G|TmH!MwHZj&}|A&Vm3#F!yXDtk~`5+(Yi}YMT!t_ZjTF zi&S&EyJsO+k`w-y>PiGPd>8d}#^K?e-rs{mNjpe6v)WG2nKw$^e=LYzX}B)B;7G=( z%r9S{gHl-HB>|u5wR#I-e|vZ!`a>NKP-}wEi~h>W$^-gY?Xmg!5`KPtipwp=Xq&!G ziP1!9i^t9aL^Z3s{QG~636c|gNt#5y+S!rDe~zZ2H5Y3;j5nQI$~gFcEkFtqi_TY5 zN$wT^INRbnIem8eS8xak_R=mWFcp1mzkcywZuev);<39(!J`OA^IVtmJ``t8pbC}_NiJT1PAK+2cj6CuIoGv zT?DXQaT+>Dt3I5P>(cgX`F;5DOy}sxP_CfN{5bM>^)z0`|+suXN3SC?t$p;tSVpb2VYB%J@8Xck&! z3Ue;q9?|j|MSJ%o76K&a)(3ZG1Mr|Bo~h z0dG$R+J>T$jR08(w%ab&d@Q+~XR^gsn*}J)ATfPL0UKp~ydF8TblnwZYX!@{SS{*n zND7h|xH#K3bck3W<9}|_escbRW2UPv8D1ptb+H(rKOI5-#9JDbp2y~q8%3iarn
<`x0GUBnr_BlRQ-L5fP{Y-jyGba%fH#hs{(zNon}U)_6alYnKB#Y7zp2W> zA5=CZqE#OPu%O5RT6KjteUGQhjm$|}bwwP0y_(dZ1A9eqWKa;L-{Y%(Hr@E9H$Okt za)Y&fnK2JfYxB8=d;H(O!&?-`Tmrh-cw`6+3vgUE%)D)UbYh|Rw_F9Da6Qq(}@J85-5xM%?X-dfYuFeS0iN9zqvl|YWI0g#>6Zl7Kn}lVW^k=E})6u)O;Se{|mm zhLPH9Jvr~u`QP86i9AJZzP%{&WsL9Y70TKA6XSlhwI%uDerEUq-a!p!-1@a>Jw8G> zffu=Hd|h+Y1ItItl3iH~$>X`0#`Pw`wnZCaYCwA@Gx(DvhkmQ(- z68^w`O2NW~1kX@7`q^Om&G&5nbK`Htva>e3&4^Qy8nts9!NJNq&bCDww* za*c)Zz74FL5)qX|Hmj*f-Q~-(Kjy1yHJOHMKR6E^U*TCU*J^05o&aPB)zqcm5Tn25 zLOVa-0MWdjqXA_R>lngc&TcG)fp=K-)&JiBUPvCu5a*gjOp^ab)r%;arDEQlT*G`$ zus}92auW7}#qHeoRm^mZ{gXH?H`(eg*HVYhDch64+STzVDl>*;+wq~LI+Z*djmK8y zG-0-yiBo4|p5+$_w{MBqUzzTy5X4LXOz{`+224Ej-T$^qfIBc=Ev# z(!RM)D!glmQS4q0e(8~2uHvO4P6wAm40U-9hHDbbb`cKPlYKg!)XD{(;BeBHzd#5!{XUt7coV z4b)un_)1Q$-3dY&SU^TkQ4eh~gJT+R#^Yi(D(&;6PTDUFRVTehwGS^TUY`e|Kt+Mq66S(5KcYz(c_k|~u1;)|P$Bj<^N>Nw| z$HR&%Q40U8h_Ta3J$(m5cf{L|kabOl!*6qBT*WJ!j`_LPN;SMrvmfx$3189^?ZT73 zeM5R*9W*;7BVf9MKOxoMq~pUicO*Vxz+N%_wIzez%i?}W>gV@PQm)^`|9fr9HRDpf za{HvrZ~dZ{_fK|M$=*aF^SGqSkZCN+D0?5x4gu5r;<)QW%-0Qu=)D}Lwx2SO6$`Ch zg7Un{ZPodiYtfnCRo&fA`r1q3>;6T|jAk`a|68g+1VmkiZN>eB;()3tM|$u4-(exq zrO8(r4p2Da--HBsGkhtvjpp;;++9rCZs8dey@S@gzX;M8CL78svA$u_s+ZC4t zxYrK|rX?=u*MP=N1o+1z58a$5+1VF;H;Gv_!OlTpWuNbVM-bTUINcncPf(P*S# zL@f%#jyW%HpDipL;(l*^RmLVpg}=Z5qqtWO3|x8Z;E{aJcOmdrp+Se#KOP~59-M&6 z-Uk|nd-(UC^4bE;(zNbcKs0u2Of7mw!IEZ`O(IkBGA;r+=H@0{dzvhGP!m@p9L#w0WaEgnL492|qcQ#trnAdS&i%&Hm&)%FzdEopR6d?h|K$zcjoAXWL-jRwf8y2zC}Hkbg=vU8=&DREoSXM z#p+t~7L#;6l@xl#$~qZ1Jtfi-1jYSo;`_b7k>wZ};t)NA{*o0jV)R))wZG$WV^mE; zcOWL?OY>E)^YHpT^v{7W%2Yap-;UCAtfkxK6l6i5@W9n=rckeZNy62WahZ1oQuy?67@v@UbF4a49}GWQd`k?YJc*gK z7)}jJ#+I#5o1xcI`PKA!>Zs&$QB=xa<`l^{((ni6ccUETP%sBf+y}nL&S6cxOFKp`BqVf{Tu=bN)L@PG{QUg=```HF-x5AP z9M{+XsA*A$gAdVc3A1rZWd*Digpc;k2lzp0(#N5yyiRa}E!OMY!I@g@sz9flr+;Cyc_+d#a6go1#`D=qf6qm}E&) z$Kck=>8)}|a(0S_S4E!CkX!iqWOgd(rpNPBUE}sV%uqaa`~B`f6%LMmxmbtgv@Sff z20*68Y6OfI?S#>g&579Y8=a~N7OFL6^adQ4&`lYMM4BN!eke<;?9bl-ZSg;HBA?J7 zJ+E9Z`n%dsuj>JW2R!{uVPAnZ_sh@1!gpStAP^|sbwTUDOYpiMtNcepgn6cCBAUos zz~8^faOP11MvZXiyQ&@E)f2ZK=9G1H6^x81Qww*TP|TFP7BECEI~7oNx$Oq;N&;6` z8_$tf9s4L4H0q>E(g9#7wL8atywwwY#yTn)|1!9g_sz*Oqq%*e8nyV0Uf#I&MeOSm zmW#Lx)~B5C*jVvI96}-@pn?bCIY~NJ)rCr`7-`$58ue717qio_sAqaOkujF>C0Mu3 zFT1lxe^Yt(dxVVBa_v14T;b~TeU<;e2%J%RdisN!A=18cT#=w21Zz0!hjfC#RU#gn zPybnF1-US>{>59qVZPIDJawM006N2dN%8qRuY!hc)-W<4$w(gnB)W5#BYYusEHUV> znp#Bp;)5PE5j6s|H>?$qYu?2Ux;%t4G7|-m_3THisjORmgfaOQ(`r?fA1_E~~xn zt*{4Hz@jgR@|RxdT*h9gw+G{c0GAJdKo}vjI<1zgpgQOe@LoJ?@4}!%_z$uWMXPL4 z7Q=y?{bpUem=u4x`qUZPlG2e}NitP^eZ>S%>H4(UA^fNByrm~SHnj#k%U-_j>B}Yq zCbe6t>mEKA$?3L7WJ!h)wyZ%c2=ZqSPqjU4L*7Q zDOs-h?CU{@FQxphPOrg(1&HI$SGf6zGSWTP5H-swdoQDAmxpPym~-=+lf7D!rH&2i z)8=Ri@IBPf^yY(^(yEi0lCc?`STp{8rZ8k5FjTkkP{Y&9YEwksxMYBame)`=nU1Ec zpERn0`6LUqw2QUW?#k(9L@DJZpkhjMqGcX9=b7ESS;&B62OX|qM8EsC_2dk!lyyA(P z7CzyA+D=Vw$`z+c1D24T-b^k6Hs0tsp(l|Vq&a@AiOp!&z+Q+Ot2ue-4Rg%Z&s?ZC&*FeVZ2}qT)7^a1Kbw+9WnucYI~e&qs#HZf^ej(fO(j%!`@K zN_UQbgc-Dt3RU!tDDeyW_pFIbA@AY!2CUzJ_@Lhg7f)MFZMo5xF;YZ-Y@tG1$(Mn_ zPG*cziKZq>ycwvuls;dT+WDly%P;n@{iZ3<&?Tn<4VG&H7~MrTHr!4e6vvh(fvati zHgAP^LJITYU6_*t*G3g%Jjq`)3Lv9#v%JE>&un}vhqnNs(X-LvO(5Vxx4`4|9Vp+F z0BPsgMFsF>d3hAOCZg9I(FZ9hI2BRRY!(e;H=ZQ$W*_oo`mg1+S0u7W>exw~8O>4s z4IkfZqO0e_4|#wKqb2r7c3%URbvJkb#m*XUMZMVi$8Yx$fwXodsqY5`$TNjlRcN_| zg`uXKO#5m3qr>=I$)dq8=l?rv7XYe$88A_*GYz`RC8qf2_kr*#;t=xJV{# z1q?FsBH1#&#)i9{a*M7m>(OZbcp16LfVla?OWaUSlE<*FfBVL&gF~M5le{WB+wXop zrW_R1u#WLp`Olm*G&z&`6C)#(jEs>H5uZLGA<;81^zL3~p9++&c$H6GthX)Kmz@6q zN``Mpy@$lZcKKU;F%dr8QE1W;vaFoHTc4~3p z;ZfS0`YP{=l7NE~X!}FOQMF=ErR*%aq=wh`XW$3a^bIA&=uM8nXcQ%5Sgp=BS}U^n z5L;9?KAiucjO3TUEfEQi;uqNFI2;gu8#KOvmEBCV`PjN z^T|7jgvorX`@5dkwIwFzTc8ynH+x+F_@>CkZQ~^$FrP2Vk}JycS&!^nt}t<1W}w^` zJng=t8zQiSr}rbNVL-v{{Gz_A_XkTBJap`L`RLjtxR!6_%L}7VoH}|wT=4yI)Ks#c zYRj{S!|Y@^zBwfgx*WGgDBk7}F{Bn6ZlP7aAgSF(K*%Ccf>g>Xdp>hFBpjuVTn{x> z#?O()n&Vw|JR-ZV-+!i|0iT?tq@?@@Leky!UISP9!?Dnqy>~AU;!FwR|Ah<~SvDBd z3p;;)-0=H?7=umxu*cHGto~Ca6A~z6sHuOyIT@Xd&%DSAnOZv9NoraXb#;~3T*bHR zVClZ$wD^lQLq`Tf&BRxI0$ok;FFA_OB+pYPktyX*)KnYRC)tM_OTvhm4$2sz`=3R5 z+X<&vDzbSxqIo)@p_))dtnA1n<5SZvWVX60CLl2@W5FcD0E```G|hY~y=i$mn3{p* zl@9gA1$A~BT3QP;LvNkRXl%UA9$*qj&zrzcEoLuif`T+?tmJr^!s7MRpiQ|pjXo*u^!4WrFM zMy*UeYMr?BbN6n8#0@a?@OU})_ISJ2pkuD+wQ6vh;6J?T$5Lb~mSkoLP|lMw#NTvT zY1Aw_+4vogxjSSIZ^sL(0o0+}+kFMWOTmFQ>e>@R=79*+xh2X1!-FV9tt18%Ags7yl0JZm+vpAq}Qru$I6(kl^j8Ou*kFK zm@sZlWWf-7#f&HIS8U{|OWQ=KS292h(G=C)-tzG9G`3Eju8)?g9bWY!*^-g7vStmD zzA~c;KZ$J#M-J>InCu@5=Go8%zJ=@3_%53kz#!Grg zWiNZ@>4qwfN7C%FV>VB($-a$VW%JqMbCT`<5fL#wN49n4qpq#3{jsjEas#hk?l2>D9~uxV;uriA$v<+O z2l5Kusw3yGESIP6IwS4f#w(fgE{aB8DZ}dnPBrbN5_*FFE>H%G6zdm@FC*{9CHojQj0C*t7;E zO}6fprm7HVUB%57we{T9&~RytPuLgczIO44DyPPgo0}VaWIGg-LHFLg_e)!-cGBL| zR4ogoGCK=i6GCl-g`sRj>tVds?p(z3Nf0OArDqu!{ZKV@xdA2j_}+d}>~=xYh2c*BB_vjA z;q&Pf0i3Wt0mSWLpCJ}(@Y~an0Y`yB(a10};MUee+d%8ez%+Z( zZ|V#FHLJ0?<>dgTmSXcg`IjvBZZE$srTCt6a{jI{hv$8PqT@v4X3Pd3!BAXLW!!(Y zO@T*F^2LNOrNnqitrqeUqfin!uUL4@U>4u7iH3W@8W)GDggUC{ifh7u24Z@?F=lB! zqMVsosfwLbt7S6gNrV0X#f1#SNqW67N8z(ab54G*(HsFOJa9M{rhK_1{+FP!F?_ZC zF$k3F?Y-Xb7q#lVgg~HUVkXx7+S1=IFEVnUk#L=D@W}2d}_8`6K$XEo8TW2gV4wFH34ozqK!ZdEiy8;518Dh zP=D#bg_sO$0zv9%Kw5^GIoPZ-JTQ<=cO)JrhQ~0_#O3`!{OKmrQNTJ`(3HbG%a>74IM_slLQn4p^J(NCs_r_P$`35e|`3J{D;GL*8@ldg5G?1Y*ql74GtU} zYFplHD=~yw{Lkx@y5ui@kamjD18SZfg`dO7@6=jzC_vz!4L_HtNB?6b!Qe)5F~s_~ zxEak3Gyc9%Qx4OSzf^XywO-tXvBF{-^@yI2c6VA8NcxS3tM*;{m;N6_K&HO!jbwE= zt!UzKK9Exoyp{k|@M-p4)Z8?8DW|@HtBV1J_v?Yj+tk3Au@hV)&zPZC!LzQ>%W^=& z7sRt$pdTLxe^Zd5q-RGA>%(X!KMGZ_9Plf(LRDE*lGc@nF1y8boJt~!E}}$Wm+G~X z#THeM0=6*yHkLx_C3%!2VemiTM?NjrIHQ~{R@v7js?p__GK@_MBc+DA4H8&5AG%qJ z=q{#knxkrIX$jtraaNF4Ra5{yqaRCllNpO1zj-2*T+YSqPdH&Ko%>c}348tWHST`8 z|CHd{FksAbAoO1nKJ@wyCPuz9H)^fkW0%HMl@qn^=mW*D6v)UrnOIDqp3P{Pk9%0;qAF8$v?+hKvw1lg7*<0-TI z@0kFP08|M)fbH1;b?vp^I=^~y9|(kxw4p20w+IeHZur5@QYv8!Yz z-NoXvD(geKsq0HtQFO5!A^|&nXh&RjQbzUR+Nh3wqX~I&1_6Y6WN|b(FAKt;X>5z= z$;^p|4C@qrXIubmvD_X`UOH)W$?v{ic9{?Vrr0|;aJtU|OX%njGfs$PQHY1Sw0^j6 z`VRH}MDpBE(D-F-9-hT*yd6@NsD{Aa+y zfIE(z$W5-_nEfm{%Hyk8-%r`fL0azYnO_=CJMq0wg;+3<5Rvo>OnF#Tn3yD*t3+S@ zG=HmU>3y9KT(@WqrJ_j4p(>iu|OdNh^8q|^M{cD3;u;4azi*7$IlG`j)9boHPUaEQYw&WSBNUcJc`95O6>ck(9o zTUUKQVY?=@vhhGB?LU8Euw@zIYzH1R-{Tv@-wRjKF`jgVU`2M!>K9>td*c$6PZ;6dSfv8Ig^ z{_U`8I@O4?uqikDxR~EF5%|@VyrikA=ySscheEJ;z@k@`HUs@i&oTimxr5gAA+)ID z%#DbcV=f{qBeT@vObg&<6La$>uQx!A0-Q6e+)dQB+aBXoFr4?tb9j~4^RYTTG`85f z53Ce*t>~{5O!@FJF*E;1CzlwQF64~kzA@wXPS*#Kgk^O??RJ*vx$lqbmSAQWK{S0K zyb(&${^&uGCuIq-_FkMY$vY;>@f?}`lJ4#C`llm`q^!X^iZ-+9`X#RB?XG_PZKw3X zd^ioDZ5$ojHo)Dz@2s(68pN9Iuy4nqgcE{l16H=EvMwa3q<<}K{^E1X?8c(4CUYq4 z=GKXmefu~pVrs8*f(pe>DNjD4#0E}J0&xlnU8ZGH`$DOe7#@p;`Wx-?!ulHGB9!oq zhcgfEC33=y$AjK=!C*@<@ySPTgi)rXtSY~?6d5`L00>UolDxNLXajG@C3r25VEE=2 za}B5KSpWWZ&)ZhPM0+3tZ2ft%qyJwE;D&*VgX6fU>wf)@HtXsrFV^W{*}TsB5cfM( z%E^iZCgvyhwA5gX*hSGi-e^8;SRQL=5Hxaat&0M`^8R+$p>vXeaTRf=i1}4P|sssFxzy(%$`p`A&1@At*#f76v1PeGAPoI=j1trcTnh~)REPJX_P-9E9l+XJH0 zbgo!A&^C5mt6wowTTZef7O(R)&j0<@1``XD|Nd`0-@{Q}GLWd3mzS4pGVmbopM9A0 zJJWeLJc8XfJj1XQ1^>{%~#|za& z^!Lj^m=IKU$?bRPCtUXpN_A?L#|zO1hsPuRA5B*olx5d-MMAoyyFpUAq+7cCrn{xP zq)R}$LAtw3LK=YwknS$&hVS^zynobTkho%>y<)Eo{Wzy_|EETW+V1a1$><7dJHt|c z16ccx=~0^~S#bAdufIDz$90Hk7b0wN!cmjt>JY*v02SqtGw)XUG#5Fxq&>Q-5BXw_VlL;__1c*5)HxA<*!2Ei%7bsZ=5_8Nfm;-` z434VXV^GT-j~vbGTG>hkc5gilHC42jNP`g`mL`{y+77uGm(yNW_s&lw7AoG>lg+GO zF_YVs28`eW8izYyp8EpdI@k zp_!rS%1g`*{Gi&K90qOKz-TV+`IHnXtJ7wGuCp03$snu6+SsTpuCv*b!l1mR-`Ec4 z-HZ+WLBzn*_3cM9ol6r0nHNknvkAFV-^w6DW;&D2ZJb9hj#H0S87~!q?$@+5)k#Y~ z##Q@o@zcU)gq`MfIwMb>z54!jZ|C**cQ)onS+&`}jh8YmDotLTyu0sfM4iz4-BHQ3 zDkdU;HD8$5KT!Kd+;7Ia=y#vPeea^wPx#*Fyvlc>$9*Ipd05 zT#TAA5G!-gULX4a4JeKAIj@lDo41w1dFC>b+{k5Fkgh*Ep`5HCng-tS?0_ZsE7o|h zf1Fp*1zZ~*-mnJtOTyRd@gBd3u5k<+i5f(A>y3;EHwxT` z2+7;sbzifOa?m{M3g+^sRk?3u=dn3fqKiuDnBbm|x(NL0aFFo7;FfP@$KFtY+i~lmuw@M!_m$)p}QIwrcsiRs2!LU@YNKMv(VPqM-hYng=I+ zW>;@Eauz4gm3C~8$+-W#+L03$b`1KoiLr@^j>+N)q{w1tGSe!MXJ(&_hDWHEmLZy+V)8#a=Q|>jhx3%W zWX_H2s*B9MjSnF^?KnhM6gW1$uVch&q%3B;RN_qg;UU2Lut&!aS05j@XT6=AdT%Jg zyVCydcT+G*BGtM|A02&+=}sAiilVj>YhGST_-_uVF!X_Zy8+}wyL|6zr1(akiJa%M zG(*k;{P8=Xx9Y03yd$gxa;Y(ag6u8n=kucIFp5gv^2chvca%DBm2|WRmhB{>N&Wd9 zI-j}BFI$t?qTuU3I&We>c`-)0YZles51H!)vZ_gM(DlZb7+r5wa8OC{_Qu*WHWAUh z7ND85QGL6cO9Z|@OuYT2@zyUppCO*-_>ivl)qChlV?@t&b6jG+|8$Z}2YTYK(9zY@ z)TiR9R$j`W|N9iGpWiIKvnQpjrW0X3b;ht%kLTNnB>!Go2vfFrf=hfV>l_y+&&hQS zbB?40hV_jMM=pAQuN=En9Nzv+wcFFS2K~z7_qrq7w={W?PV+4$eG{fIbw+V0w7vEV z$nwJK*D#W^Fvx1gzis6@rsTEiWz~va9lt-RK*=K&R#3d5yFNDg_*1CbmDTG<6y*V^ zVA{r8ZqOn|`i^D16)T2)@eUTdh}dD@0%RC&hch_jb5&wCJ00rry_>#gdH;7=mieDt zP5M-{`BP4itQ!Rb8;AR&P&waR8@xaSAJ=?z9N^>#%+GXj+T6%K;@KXg0Pa65s#vgxsqf0GgUVN0YP|% zBU+LYXvh#4oB1kzxwH$u$J?`FyK>W9>E=%Lx{=Ma#$+7zcse?33BTBo$z9W2nM!o@ zk9g6LG!JqSg2`2NxoYMzT0->P2FBP*UGd{D=%IY9*6D{oRKJigC8{AJQh4lL+OGOA z2mSf;Rxzjc)v@-ACf8t(n-*iBWx8Y%QC|=?ugd{Wz`&*(G)0d-cZ;-lJDyC5sjvgA zM3Pn~0)3K_T+thGW${GLL(Ndx&)@H_9|2O0A=vOLz_{5HCnpe7Dj{?2j@sx8auqoQ zeprZ0{Lf3=58ttNYW-X>2%GVF)D6uqF`&lLQG@U~p8b@VifIC(Lrg-9F^e&GWA{`%Kx4*zhN1Dmw?$)wRC&4M!KqWpJIiCziK` zBOz^P_v*J;)bLxD^qUt~j0~4PAMtd$Fi^5E@5N6ZwaE#|8yVS@s{eSd3uf z;?}zysC3=$>MCUND2)ENL2lb79f~z3(|7+AMj1fwO=a%Zl{|7p;>)U}36nOHD zxV=)TN?KnBV+!=zn(8f<91WYZ(Dsr7YCG*SHitHBucA{fGEZw_2) zMNJJ+3v~!#M8wfyI85k{R3;sF)zD8^8ay9v&MMI@=?o2Vamn)t7PL1QwNwM?n?x|n zB@CRrb+12?g^SRo2SIUTuu10{3BPBw{G;7o5P5Vv;M6++_ZB+p2u;cNg?>KPsg75l zs`DeVJaVrkM{oh6d{5LLtGC(xxbHSBC6P8gW2bCsw8kwZ*Ja z;Z&WQHVod#B*4R$B<9V-G~N&l)kQ(SYj%EpzIFaZ?EaKCL&ex5sud8z7U?88q%}@} zpv)F|+_O_6UZ!CHm}G~p`%nSzn;M5TK?+Kf@CXIdzdHp2h$nSur6rqh(;A3Tf9B^$ zD1I(7n-+vortAtC!FAxndlrUUz>W`SPF6EBL#L^KUy!HqmYnU0iaj}fxcsvyI}F8V zP8iWXM$_r)=B4S`QWFHiZuO6NSBVM8j(e8;4wK_zNuo|8ZdmGhW7t0r$Jy-oq^0w{ zk?SS4CXR?)x_ZWQ-Of0_sp5oNPRZK+x2KteP4Va+X7Uk#-!VR|=I`5RxQ`LcO0plV zJdsam>mAi`G&TCC>U=SKMRvZz}J$0<10*_NXW;1w=utcjIa5x@mwNVpm# zcIS6!IDZpm2X)~Ulyunk`k=@%%wq+@vRwnET6}*-7!fkE--nN|nrp{OGkSYxGZwKF zG88Nwe?=4HO3Ua=OG^Q_awM0@gVDa?r{HWqdyhD=t@9^~q^Ipac;ke2BLw8??7Gm= z9pl-u>ypTj(Rcb%YpZ>hSw6}II}RfZ*fjFZtOw6T=02{O({(yis^ItT&B%hcj+mqt z_AHH2{q^3}ZcaF+bdrbFDkMBW5Pdtu^15sk zE3>KobSOOZ@>&l?BrZJ|<;V4=aUPhP)%|@GZZ27|hbI14ck`ns7X-K4Mm8fdQ@LWzr zz#E1Hx_m1gJ4r)*%XG#3LiJY$x-l?h$SSX5=-#Ra{t=8y%;yLIx{8X*&%5(2-$UJ2 zJ?6NKYPiVFF5DnWO#JH{?B9p1I>;W`8)#$Dxn*UMfWlK#FO=PTX|P@h3<$^_WD#9) zW)oNb;YMK=2CHjUXz#7k5qezaVo>bmU{Q?BzK8WKv4B=vMuuLMHi?|RaZ$BSAj+oN zQP)3~B-->W_RVBB<)g_|gN_OoMk!l`J8zJVHxpHG(sG@!+sPb%tMQiFm`04rujn6H zSQyddZ6|MY>AZeYe^xTCPteBii}oxe;`@^}v%uPM5gzs8YldkIg9R%p%4tth`mJu} zr*pk2#N(BR-GXSdXVucQwW%Sdi~v2uIQk+*!ii++n8H?)J^fq=K_W>xlg-UmIv*<7 zw4V$=zW-9S_}CxHydI%I`8C~2y0X(NxN5qd?Cikdm_P9fO&USbrfsK(zDJb7tFCDoI zSehxqp|0AAogq);JxLU>yuL6vm>~ z6Q!$9*>)W6as2tydMiIuf#Cy#S>{*IUmTv+uydkVD@k6Qaz99LJ-4<@sG8LdmPX~@ zj55G%AIiGUG%|M?oNRm8V|hEr5REWdEX6bKD$bNt9f~REwKE%K1-JdGCJinOELl_< z5N_xmO8(!uKPjechNV8ZQT7G55jzHt3Od137z^27=4sHMA6yLM+Pg@rcOCUGN4Acy z2A$)kbs!vId!-sxqe^d>sd-rxBp+!t+2Whmw@#hOp~EoZM1`3~Hl^y-TNB#Bh{5Esj8 zuaV9-Y8sMt4CBJNo!ss4$X{}b^YIq^Kf=g6=H$@f zele0m7|-{F-$1uri0lMEjn7D42D zS%jwZ*ASubd||_H%{XbVIukEZpGf(eaKq}qKR@1@G#)~iVM}aW&l^~be4j@^MK$XU z@^m@cO_t)$7{^17SwhE>5K&hLPw-y-=+4DA4*412v`Lkt0dK=ZChwEQ#f-}#n8==_zw4RQss2G`$AlZ3(Ew*m5x254>Tt(6&Gu@Y%SA!nw^b3Pxzr2 zBmNr{`ha}o5`YHbYu4fp&?Q0KUa>gt@3-d34AdH1O+b3HgUxDcA$k7Pj(VG#4X{pje(Ok&Wyn3iRUPtg>fBuQ~T_8?%=X|Qy$zb2Q#OrW}d)-yRM6H!n= zg$ss~ICq13OIIdpC4pzbw&Dtx`m)nIciHt64Lx`ipM}Lw>|0X`f zBq$AEBNoC!ADJSODD`+-f;?rx#3+dOdUu$eM34~Y-o{NconX;Xa&AiSvKHtq4-yAQz z786zDk6R9|5fhXZ1s3Js83;q4rnsPNI9jkHoHo%ov&1jfoDt-29yy@ERGND=#?z;3 zu-NZ3i(tmU=!g;=o7DcN6Z_N(MfimU@8o30%`4# znCwnTvIq)l>H+ZMc*N|W@$&NeJf1W^U5v5-o?S&-TSiU}8Qi|@x>076{%{Pz;4Noo zXAGEOXMxV}A4RoB-uMHpW^LvLP*hOIvmIlnUyU<*(tH9%GqA(ZLusU++8#NAf|Wq8&c%6?REWVK1Q)x*;B$Bn@Ja^E4HUlleqB(o}?)~YoXth3&1+1{Qxc=^@D zE23&M9BKMd`4ryh(85AGQl+#6b7%-nxM3e5CUh{*#BKF)NT6vrz+m$oNW;0V89VqC zg4jMJWsuoxiq4bGaUMV60L3$bZtL`|r zgCk5M{JJOXTqDEM1U9(_vr#YPVk}RG_$JvC5PyfOLTJO;^Ckgc1UC- zTP7+YsxE?L@Yh~4b;mE;{V&3JV`*uv#RYCHk8qKLv7*Pnl9VsmGl28kIWdeP}`;>yT`1 zuha_U`ose3awsekPK{yjTb*Wm6k%&?dgZA`6$G=wfy@aW$=4U(yW6eAf4)BBTF1lx zKCJq`Li^pDA?0mMX)(SmYrV}UqxejbU@HDT=vx*lmio=i8cJ|)P&9_No3c(q-wSk@ z^ykkPz5rP8@}`YvaMzoU6I6A&aVWD!gbR+z<&)*{oB*RVdHb@LYQAYjasj%JhJ=z@ zOadY|IVv);l@hD|3UmnnL$Pl@r7 z;8!&y)zwSsSR|E?;e*1d)Mt;#J;s%>LaaHxF>HjK#Nx$f>CdzlO|KmBbS9dUX{oMt8pf`6t%j4b$Xk;0*i@Jkyo` z^{X&K_T0)?H9AMy^1q+@jqYi62JV0DPtk{etyz2hk|-X?yXJF}Xb7xL;59gQHxNO` zV>5*(C6{ris_=o8uFGK@gQ^P_4-?Onm%^z7504Y=LFxvrZm;3Y@6bm0cPm^$P7ysKWFBREf9mrs!{LRNoV@o((W%jC2kC228rPOjo>ZLu*RU{UJ-y9l#n+ck1XTOB zJA;1>0gdQi8ZAzdbwQJ|r-2U}#5slZ%*V0xRK21a5i6BKk)g6qapVlAJZ$O%?=N@N z)HqOp<*=|MJU=`?0#BaCR3fjTA$ob4$k=4dZ{~m?M)IGt%)aAXl14FG4vZH?L_|ml zuMcf-G}*7nEj8H@5&_Py}N)O;QHgGT$K1T?#@$PfkzHFw#1(!pUG4#=XNZuriNv-;Y1|(eemVkJ3PvU&nYT4cK3H)Fcl4TV(Nb+2FVQCZbsE*E*3)@ zb8qV5VD1?&^Xb@d4gL#-f`;2)L_$K+t~ZaVVj?380I_}V@w9AwBO@b!+E-KYPzB}Z z6TLkD36@LeDD6G^`?nA6J;>@ci4&6W`1;t!mxcPX&qnqT1_q{vZim*M#QPma{OY{a z3>FKlZ>`QjH`yzrL)SSX5IBheBr+aL&5)6{-E$U<%tyy1{CRHT4Q75XF1{orB(SDe zh>MHg3B(l{gE!pg{`$6iRQ)amgRyvK;OPw4tFhTS3B;&$NT*B?3)oFfDSx{DZ|l?f zwlf6eADtA+LJPPaax5(HLY{lZ6WZMl`eAXKXvqbW*Vhff4Wy%EG@i_U1~1Dw8|4cL z2rO!IKN}gqbtU`6h}_;M;-F;kNx0BC(8;)Ep4ywvZN}xo%fr2_!ANoxm{DjE}3dy(LMknmz z$;?7CmHf%BE_586&$ssapf)iX8RcnJYK)AG4Dt9TpLCyYjxES_KMoEI{%(_JTlaH% zdN@*;MB2`eV$O9xJM9;j*hnx_$R07ADXK)#*|b)2UW0fTd2b%AHp7PeI9ZrHUGdIv zS<_)OXYM}6Tx_%n9yX=or;T&9;Z8`*c-Iqx+d%s%|3;+MM5=85b3HQ!(=e-YYucD~?oy7+FPJ4vE{u-eH0eqtg%m#-kUn9BAmYLz!lAutnc8pN<@VyoHDh zk--$An>>uIsef`%URGv5^`iSNGjl8i7cvy_p0-v?47|JVSiTg3K0PgMd%Gp48vqhA zW@ZKV)%FF>M?pmVHZf=RO`utaf8UZbF*?%pgp>>O#=rwMq2 zlpjTVrQvRJFglUUc>i3x?9Y#DBsqvrvwTk`CM**HNCH)Z$~|*6VYax9awS# z6GB2He4d8ocE_~WL9isE{LCK^0oGTAhuqT>=v%8bc3tPZyg%QJo0r#Ex_C7I42V8% zZm;N3X2K!j<~7kXh=8@`mf&u!+X@ZD=K$s6va?Y=6dAwYc~bD;{*(0;&oSz|>aGhS z0dJSAk$?q5a{V=CpM{c180u8-aY5rtae9oxudWdyj%NZPH5fUqo#r15uCj1yZJze)8hCxCySg$D5TVs@Rxs$*xEWqn$b1-yV~gzs=+$I20$c+y%t7wCK({t`VW({_8TtWqN5%GklH# z@R=rYxVkCn6-7R35>p(iP+`X~Lv@fWG8LIK0oxu=4kr_pJC9rXEpy5kP%b|~t)YfN zm(MEJ9ykkw+7Am`=yK$Rk)rn{!d@3JNuW>qkI6+Li!Y?t(D3hiFCWq*7>R?Dy1p+n zwaD7O-^<>Nmm)dGl(d_#BBqpzH3g@)p3{JOf3xKg!h{D$d20%f( zIA^e$z=7A1g1nYk$2D;OA(h`9b!y6qs_9r*-zJN!4fo6y{Nl{T!R6I+-3|{oC!-CD zT^BMnY7#dT*1iu{8|BsAAT?N@QE%U_=U}OsrBY|sJf*m}*!gN-8ITow0yLv+z^=Hx zw?~QZpTl#orV{zbynJ)zw&-9HkU9N-jk>KmzO5VDcNnP!ZzC?VG$h6k&xN3 zrKR4S^8?2>wBRsy3n@vbm#f9ZaT;>>17eOZBM$qFknbdI>)ZY5LP`VQb5`hg9_0wH z3;Xlc+|K5cKm1<>!G9ArHHDcJdZc3M`upFRAOw_hn>qU{pC+BktF!rE7HbUEM925- zxw+qh!{Rs&@SY!T%G?wmUuEEfdO*3qbDOi^!`wsTUj7)FmgB#ls2BLi?{_@)uH~`n zT(CJTG}N&#^@xD!OUCy6s{(g-1POGBSd_z4@7&B3f zzlT5DA&Diyh~t|Xo0C3{g5`WuJzi)p?yH>)?J6;&g|RrD_khQrXkooti+~I(Qo}Pk* zB@WC9IRjJ&UNs3YBqf{7NX}ASf>8SfWy`^$x2$*3HKS(fgijz2BGCR^REtW=JMw_i z?ZGxWGCIoZK?mGRY&x{f`TFf#*GF!08`mV9kaN81FV} z=Irg)z9-@@V)1mG??2C0?-BxM7zXLR~5S9 zHx~{_s_wc=ZJMo`ZKL~ON2BF0)B>wAj`Nx>x@SFgK?u(RU&6;k7f3*(4k;A%SaIV< zo)zEdeMh=o)qYAro(D_n^H;Fb`&JSiX7Y@TnmXhcsedLQ$v9l9q8et~$C9R7|f~&E z@e)z~g9m#1t!JhS17O;}@TmKrv%}Xl@-!Rkjg9a}gb4XZ2*J;_WDp)D_b|29rd44| z`ZHdBpg6}*2E@`w;XIEu8MyC5MA3v?&~Debr!IM!xk8z<`0M!X&UlF_d0VNuS6n)) zPWp#cKfS&deM@daE1&(zNm;#FIvnTew~rH*rU~&1&2T6c=$f*;^5Pw>8?#X@sxvKJ zGGlWTCIv=i8H%I+u}n0u1LIj-V%FU4dy3}A( zl$YH*E@iCJvbuUOLgsb-#im$K@NW8$)a)b`+-9=;iEFLeFHFRIcc|Yg;+~$~3TC`u z!v%|f9${0+;XPWwAtLH;t}i+Y->NYRCZyKlOnteZ2LaaZfYF-j@+DmLyEI#Dt|lI} zZk%p+N1LlySZ_RQP_zKLflH%9p8?DnawiJO&?x5Vp0i3Wz3$p)_5i0h-(9j-|_P1at9E+|!`zA-7I z$5X$u&F8DYd6fqijkI3+neO7Q?LWAjEXv$&9?q7xr+A;2S6i@KoT7nQN>vDC;sP6t zTik~@XK$Sa4x4V0&hKl!VK<+cj%Nsn6MWGt9?JQKE){mu?Z62TldJvDp>W4G*V*ch zDk6;|5=KUP^S!E-J*?%2;bQjtzXb<;UtsMqTi3&|^mU_f5K!9Q+tdISbk)~^0FZIO z;p6q-1o7Hos`h(%dwzSoFzKJ|6$k*$(a?_~E1&CY+v=A}ox>2gYCNyj)A}N-PZxho zfrJsaa=L!JzzFf*5=gH8Z9#nh^SYpV$`{h$krt~5<95#UMdVfwYs?>3@IWF%(aH!{M;ra-!Jb>MPwF{uK91TnT- zMmKX7^bcP&hta!^yfVkE$i&L+Jdxu7{{d_A##=(fA+BhJCMI9to@UJT> zsi=^LV88?t4(i3Jung%dW3Akp)M{zHT&hGO)(bd0%|-QMN?^#VcQT&1wXdLgI_vJ= zoX!%8yoH9IQXAL*u8&?moc5SgR0@FszKD|5M$C_Ra{lnGLEXH)Wu)-8QaJLQ8S>(i zUlT!Z`1h|;bw-O|qCKM5oU_sCihEC#&zrGV!GZEh#T=6CZhpw|3BlEl@lwZ*X3jLW zKMwQ$?+IB&)5ln~YCkSrY*3G8&b*DhtNr%g7EBF+AS72ivC#j_kv=C3)i5yM`an9p zIwuP6bkVV}r1kU|Wg?h5aF!FO9x7BdNTYCGk7t{_Zy_uRq*(to=xCeQM+^p@BX*q&FgyDaQg|@bF!W}C^*=KStfv(zk^+lQ=ZapQcz#lL_=Ic zxpl8g3YfXA&cBS`*%-2=eZmiZIeg+wvIL z0_F1)#SIl&7Nsg`C^A<~hOK(2$gV-T5+Y#O-!4!jecCN!(*D}bZ0ApD}BO{*$ zpOkX$X3znlKak9nv7!VF}H)YtU5;Ls}ey^o1OqDWc*L)VSP-e2Nj^Xj6Wz*tf8l;_rZyBn?J|;z{E7W z;s8piy86d7Ci?^%1i(Fmj^cr+J2&@DIVXA)^_wW_-!r#yKiB3Y;5}eFz-;ddDWDSZ z1P%!vu8$-h-Rual^Pia1m_H~R3_tfrs0u#rY;UV5E0=ng&M_e+qE~kxWGR4-+M;7{ ziX&#MUVCp`9`F4YF_pkxJHGrPH)BJfb*6kkFPYQBm3Azt(1GU^8Zlcq$i)<*^S;_I za@McXYQ^dK^?5>w8UIfl5V@urZD@5G*;T;`Ntjkyx!rX*`39cssRF6# zY6FMGW}78CjPh>yQbHEkZ`oC}U7S1_X*>0ua8t_45R>kkL+6g0vSydhzIU=WB_h_= z1mO6wt5IZs(ZNH8iZZv_-cZRkg>u9zE;&a>^Gr`~WFO zB8Ez-{PDRYp4zQnsC&udQx9A4RxlAIE1-IhS35IqPHb>Tuj}!8e-K#i;W;Q@;mK<Ca_&sz77vF3G`06t^^MZP}{C`l@FVQhK7E;yhOYA zd3e+ldLUSAl6esu!WMRlIFkrm{0bAj3a>uPi3z6FM!ZpLbIni4V?pn&z3286$^W|~ZZ`wcXE=UqN3VwTCnBPV+KniS<}YggWPSEQN)!25cRB*^+lpo}j`#Q`@t=cnlNqw#W!)A3rj!917g5c2spk6-$e zo*vKNx!RSL)wwRAg37Ve>X74k=+xb>rLN8*8`Rx}f)M$3i23XU8#_tmNA20>Z8C zZ*hpr@%Dsp10Z0xI$$&!ER~PQ5s;9!fO54yr|WR7SN->|V|Mxz%$YyWN_i;6Zxw&8T35c={`6z= zKRp?I!-SNSVg~%n3I4N>4-d7e6o=(&?FDJfEy4ybMY*{>qcyo!1pk^DU|4FfTheG& zTRjAVSQ;HQIstJ29A@{%p3bKP;sas`I?!jbNL zui*&)fjs$+OO0w^NJvjs(Z$_zn2C~GJVB+A^Z6ZB@w_)Ib=N5w^%NwkkE5%fqOKpF zFRdnEqojXpA>kQfuj}E_qB*E%^2-wbx{`o1Ji_Wu^0M$naCeY@qTJg8z!R|O$!o%U zXk8YdS?=1}M(1iLG%i?B7CIXS-3m`PRv$?U>!x?D2nBuq@V;mKOIkjScNkT$c;aT? zy#;l1%AafF?{#6~_Y3SW82OwraiRX&RW9N}d8=#>$<|zYpO|gZ7dnn|Iv@QC0ZQeR zN}oGji2to2(-a~ya-L>f-u6z8*-1Ln& z?14pAn`4F`Q{g0H>4d5J9I7BjekJPo>1pGC=%pu%eEz?np>oaYT`+M}=o`kITCVSX zozoqpM2yaa{{~7sIise=Sy#*LY_$@p?rfo+7Bwv9ca5b!@;Ck}2Jzwob!P#}^4>Eg6VDPp` zv(}K5K8N+)X+~Ww+sCppPXRn1D+{3eKU|$|%tCOxHeC)!EHlwc%F1TC z4*{_XaPQP*D-f0Kx=(E{Ji6)#h3-v@0Jxx{rt!FbWOovI=d02aiT&@p>*?7RXw0kP zKX}(!Oxg?so&n7 zkyKa#Y)92(Db?Pj-%=yy(B{?}{o*V2F?UTG?ut;x#f3p}?SWD|diGLfhi`ji#r&H% zQ5C)ELI~rixMUkH8i=d|WqfUvNrM!uN-zQ_wBd7C!0#IA$=aEj>%b*IE4JtX?X9bJ z+K@Q~I;;RuhONeag>S`~myt1AmIXhc$de@>TVifN018k^(b3T}Kq&GMSUc;uR2caj z^{3r}zg@Li8KN;Z6#sB*WoAZAtC$UPyBZ-?i?B#td%f>y9CxoU5RvMJ#)pBWCKGr9Bc-6_XI)@ME79CebyD_%x>0~Q6%Qz!iyK{*%Zn&QZ#)0jU`7ALzZTE9 z-^O=OZb>gE2PJSaf*NhEwe90EHuT(A;Po@;+LG#zz^MAgQIn2JjYai~1ft`wlV*=b zec##Nl3V#4@|T!39S>!Bkkip4DT5iyXhlFNR;x(dt~!Tl?&|0R9ag8&T1VJQwbsp! z3BaOw3`%S4m-&j7KPBzu%s$CpwFkpV3$_3y-!bD~S@d+W%~dxXP9m&_(u4<}R=Tq^ z+OLd^8q-EnnEZOF?{(5+PD!EaV>0>W0XsDxLL~!93mdrq}EY^M!Tit z@NoF*x{t1`CWFXeL5qa(sGIn&|Cq}-mizNg`RT~W-V_&%m<(K!!sW$hc?N;SSjjSq zpM{P`GI*%JlO^zx5!F5zrq3z8{tv4HRVDId$sG_=5PzWf|MObNI6*z$oNl`eva+#HdJNl=J z>xVl=n~e}bjTZFqD9b+^B3L;ziGDg=7yamylrd`j2yY^{bAnd@mq4TFzDx8mcJ6A2wgTBeMnC>b!3S zD>T^qs!57e+u@|O8{}x49ru4@C}fZQj&UBqPPv?V=TPS{{DmqQSY5JO4+f{?)8~7G zrCeAgfqw48Z^I|Vq$dh2eS*GTjw_9n1bBHkIg_b{qeQ@ZhH|;*H@wGg;j}%(3KVj9 zWaJ;NUu2m(e+Yr_lJ94@KmN`wk#bl)6&Ws7^Me9(kr4extG3Qbptx;l;s&+GZ@<^u z!)}EC`}gnDUB0?6*VW!G*026t9H3w0PwD5=sJD@X3_M(S*Eqb2yV-2r zo`7YRt!5R1CRT?_)&H0}9v8Z-mKMCKjxX>)@UeEO03+e7cNemU{XSn=9VT#nxSnNi zPWYvblrc1-LXw#h_m`S!=c79!1-#Mzu6Jki{9^bW-HmDgh5As>%e(+TF>fM&tXivt zu$xw}+|8qxlEa!)=KEr1YK(o|`qVGX<|9CjYsl{r$3xxd_d zkBu!+4SBE#=r#PC;8Y?aBZIK!drPcPe7HkVQEM2HFYY1Eh=g18yWT<(7(_b0FWM-* z_fAer8vxkR6~^}GR|K#Z^YXyL0G)^^mhQ8fdeVEs_Y+_jWEtTe09E0Fn{tiTxJJ(& zO#Y_FaX*2Thf2n}0V_JcMDF#8LGa=3a?L06nvjeAr77c6PH=GW-EpFO=L0ql&T|zb zucK~$cIp+8b63U&Yaw7m_Lb*9f`?r2me-jpv3c!W1LpPb)@0hl4MQ7Xd09;Mf9n|W zsH;2iWX7~{tKu+SB5t1kITx)yGJrr}k+($=HDX*Rs++2*INu5Ov$Jn>&n z$TgwgRI8wvXi(bAf;Z%upxwc}@YHBD`K0Yk2$)*MUZ<;{4pRy*LRPH2)9wejmpN;j z+>F5~Ue)8u{?cy5LD0!QL`6vxjHcfM7sdM+H~HmknrEtPO0q0aP5)`(xw}1Q%64F< zr0G;6qa8Wn&8!|RYbi0%mMNH+V&*EBzJX-K75hC9X|)tvzcQol1t0KWR^=~ z3z12riBC`e?CHr1OkjDgM{|&`RzMxZlkZ9#ZwJ16O;x`E6|QA(Rfz*%MRS#K-cNyMf!hX8?>rX&@1l}A9 zcJ`&So6SwkqAvdNvzxZmhf@y0=VRIK@@w;_+nl=;UFwc4 zG%wYdR}3ym9Il$t1c#UNnGcHj9iCGDXNrMnnXW6pY>bj?vL^@wE9Jz+jwZqWyYW$& z&;3NI{YGDjgVG71@?g=IEJEhw({|ZQCaAGIRGE`PuX?wFi16@+r^{zN-h;YI1i5u} z-{&FT+EO~p3t>2O75cw`2-`u02P2YeHku4<(o0c#0GOMPr-v-qN}$73%#pnfru4ku z(foIh0pSmttdIULKK~|iBC#2?Y7C)}aG6bkr2s4&3o2Dzqh>3Eg)2ZP7#5Yb+KK=ozcHT zmd^#=poIK0IXT9^u1@At0RMswFlesKa zQ8l3RWw-FvWNy}604o#suF~p%6rPwI%@O-O&iz`b*T#!(ELIkRuCA#G&u~Z^N>u#R zpVSw+Um*|{N3;VJg~Wt9z{T;O6{# zT}2)24fjsh9gN)o8r0z+JxjIW$zn{$>xPA?#a@le0%BEZEH_w@c*)BW);Ii5Q-Bp> zHvZ3*5}(iXnH)#K^~QU^4R|BEN~j@iyX)sL?{yKiGV1+1IqyEakC{3PL|%$fpOT&vfg#T75e z&sTA;2$j~=&_KQ6L#v%u5m08PquVa3b-&ntkLJ8n)7!Q_-4`}4iOBzeLph3EQL9TKV!g7F4bK1qPgx0q8ZQp|9$`)c{d)8 zUVHytbvB+}VoR^%KM4Om6HSQuB3lt6NS@c2tm3a@G|uDyjcSL8VDiN~8$_ zQ9%*JZ@D*ro+oFY=j@r8v$ONtck~F1GzmTQjENIIKNX3B92{?ey=Vn2(|?_vl(If^ zPXD1b>>yruyDfG7gM_sW1^tk4Q)jBT( zyO{5upvz*5;_emqbZWvVgP30qoYd_fV?!vE^!nhscaaBmd4QgQ;T?#?QYe4z@Lf@$ zmspy3qS|EHJ?zuDs&5(+U2v%1I&*tX0`cd6qORcqMGJW*$JZc@Glv7#x|91V0CPyqp} zhzQAp1BNwj?rrv-o}PY$30Cxq^G$I@#lKmz436mxeLnFNm!k+iM4nz{%7tn6_V(HN zKZoY+-e|!xCEjs$+I?5yf&#p7q^G?gXVj0zo*n9u?-Q7V@l6fhHG#<1+Gf)nlr}S& z0A*-9DxJ-5!YGM~mJVC#UoA39QegF|Z~ZacPBi(me}^fQ`pb)5_2zpC#r#NTX8};_ zq5zSyB#`R@-scH^+iWcfXb?_x40Y%?QaVyu*duw6<|ZF*cisbq|H%CDPl{lhD%Yieydi$0<3~)uv}z zA(7twfBkk`AG4}_0v&GRSXz#kLx=-L!i{`!T80U@%_C<^o`PA3y&53 zf9)~>*(ROvP!BjzL7k{;DT$alTABXTr0?{-XCy6=h3?LD%qNT*{`fdSJ6-wGCG65t zR=JOyOMd0vtdM#81+KC(uLuzjUhFh3vP4xwV|z0+woH0utj2fl=9_AoX?FRnloYv$ z6uB3F4n`*yGTROULlV$BR1`PhqZ-1*7)ch_)w4miOZ0GcaRe4YV zC5#83+xxge)D3n^gH}A^A55I=J`_QFQvrt_7fSjC%_21(x z)@c+CKw74kKkKmiLGyvPzR5#dcxVvz35Ev*CH9ANr@U(KN7d6dTjV7ut7{a^4)`tIM0-p>P)(({TWKDkh zlqE^|xps4^xx0eI?2hYQ#ovn8(;M^%wjxrv7ccKuu0`GKrv(Vfrq(*6u&{f62xZ~g z-ago$fcMQr;=Q_|Z^=T*K1|uirhMo_iVj$qyF%uP}cl+8B~zP({0M z$!+VeG#TUk<@6og?qoQ_j+h4$g;M`@)L2&^w&Moc^MErum3ytvUI8zn@2JfU+J^JvgvC;TxX?z}8j7++< zo%L`L`fzCA$uz$+syQu+&b7q1z)&mW&;r$_6SM>}nzte1OPKVfFP|mAv3Tyk&N^Y+ z!ohk6x@MPTx3;3$!#;-axwyD2Yu{KKE3)nwQ=)T!#o4vno9gtu`=hVyS1l?=bWQkL z+#mIhjt*ewFq^C6@r2nesfI~)3o2LvdA`n5NJgC33g6XJQ+WL;IkqHV*X=j|Tr;@pl_FM~&8)EVn}qVAP94)>Fc<>~&K}V1 z@T5w6jk>JG*3sD+u)pQpb+B4w)7m7ECKHh5$xmaCniS(F^}Ymc;P>A#f0(j7(w#cP z#gjXUx$VLn?|ob7elj}iinQBGQH4OQDOYb$K{~UVy4PqniSPj=)H*skN=i!clp*Wl z5bwydITf9hsR{Ly?hz&y7A*gCe$_yf*#w7%4h~J7p$ohWpGK-Fbn+C%P$jJ%MGC;R zAc=4#WM9mfGEd1B9zj9J!SjyJfzAAPZZa-O%YK?&V6KqHxJ>0WdIp}%cgnjEk=^$@JZ!pid_9mab!B>PRStlo3yi z?YFHmpz41|;>N~dsbYHA$I|lpEv8Rweh?UhWa%=`*6bB_xR*SxlAWFHzBm1M*CZ_? z0~^FPuJ&$fDrt3fwapj~CUkL8l#z)^87Q0pE|izVi{F#5%-CH|Vji$5wx234RWafb zcFm%Qym@nhcmbt9dq+8mFv}SI>96GI@qXCbV39LOxtJP$wU0RF6e{`k2~F^9)RrC9n0%gyMsDkdr*ZdRS7F_V%IhOS&(W$bJd#}mAJuJp>M~>oy$TmSMRwJ1}G;q z!C_$(pqftbc3%IA+xdw zLL$L!;83#kT6C)YJ!6z{TPEh#t$w-(JH}CmI@N9=a=lCDxIPaY+|c!zPD)9M$B+H} zr9bQ$i`T9W4d*8hL%dkOe%O3tDV)Rm8#MF7Rhpt9Bcm0i0YHT8*_JYW@ zm8gf!{F$~d$iAk-;eoN*w#3f!5Ul8O6)D@5z@AjRA!skPT;!OmEfS;uy-`gse)hMz zmh#(sNZ$~M;H^t9MNHRC_&1f;{8KbLx)aYCWO6NB%0~pAMsg^xT@~CB9># zDylT}n_^9(R&HIVWj?yy*_M?kAQ_rxu*05Vx#*^g9qJHYd}`jteEzL3JoIvwk!825 z($rNU)YP4W-Y`e(lZD$ft33mWLkaF~`EohXO|WD|Gea|PA!J8&uCS+hod;3lvpBEL ziXyoviH8KUTfRt%(1b8o;gjOx^wV0c(|f8r87$v5j4m~ju=^g)iRw9^2qsYv-oq~5c%r}`FHK?_Cy^`Us10>;FF zOQkBa7k|xga*HDx4vo!BAxr&;+`|jws)?5~y3pog$M_jdQIENtC}VC$W_j>146J+_ z6~0X4q5U<&xaEB}FInz^Kdq%z|FCOlY^JQ(Y>~wznDT|;f&M)`i9$; z6h7-nwsH*^(58w)N-Z;O;aDTBe|X$T;qGaM9R-G*M*E;@wB{- z64Y%~{XB+)k*vcGO{>+I5p|f{obbRZ`ms(c7eYVt1#n<#EFz1NlmE7}&-S#|q zO(|Gv5Y4De`y**#6+ubx57pBlsz1Z7{Z!>pm>&*07Wr&}CKAFQ1=b~d{u_gAlDIEm zqfLxta8r8$nLi)s9tsyi;b3%p(tmzy*9Lhd*<`M4vVpj=uUp8| znqLT2G0Es6uLfFZJiJCmdC~2BEuSDc>5U+aDFk{bv9I z15#y&X-R=2W2Ngn%8vhyX3J|5>kts+KN>SAk$m*u`yH0qIg< " in line: var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() @@ -185,7 +186,7 @@ func import_content(path: String, prefix: String, imported_line_map: Dictionary, else: content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], title_hash, bits[1]] - elif not jump in ["END", "END!"]: + elif not jump in ["END", "END!"] and not jump.begins_with("{{"): content[i] = "%s=>< %s/%s" % [line.split("=>< ")[0], str(path.hash()), jump] elif "=> " in line: @@ -198,7 +199,7 @@ func import_content(path: String, prefix: String, imported_line_map: Dictionary, else: content[i] = "%s=> %s/%s" % [line.split("=> ")[0], title_hash, bits[1]] - elif not jump in ["END", "END!"]: + elif not jump in ["END", "END!"] and not jump.begins_with("{{"): content[i] = "%s=> %s/%s" % [line.split("=> ")[0], str(path.hash()), jump] imported_paths.append(path) @@ -249,18 +250,22 @@ func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine: tree_line.notes = "\n".join(doc_comments) doc_comments.clear() - # Empty lines are only kept so that we can work out groupings of things (eg. responses and - # randomised lines). Therefore we only need to keep one empty line in a row even if there + # Empty lines are only kept so that we can work out groupings of things (eg. randomised + # lines). Therefore we only need to keep one empty line in a row even if there # are multiple. The indent of an empty line is assumed to be the same as the non-empty line # following it. That way, grouping calculations should work. if tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and raw_lines.size() > i + 1: var next_line = raw_lines[i + 1] - if previous_line and previous_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT]: + if get_line_type(next_line) in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT]: continue else: tree_line.type = DMConstants.TYPE_UNKNOWN tree_line.indent = get_indent(next_line) + # Nothing should be more than a single indent past its parent. + if tree_line.indent > parent_chain.size(): + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_INDENTATION) + # Check for indentation changes if tree_line.indent > parent_chain.size() - 1: parent_chain.append(previous_line) @@ -481,6 +486,7 @@ func parse_match_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Arr # Check that all children are when or else. for child in tree_line.children: if child.type == DMConstants.TYPE_WHEN: continue + if child.type == DMConstants.TYPE_UNKNOWN: continue if child.type == DMConstants.TYPE_CONDITION and child.text.begins_with("else"): continue result = add_error(child.line_number, child.indent, DMConstants.ERR_EXPECTED_WHEN_OR_ELSE) @@ -569,11 +575,15 @@ func parse_response_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: result = add_error(tree_line.line_number, condition.index, condition.error) else: line.expression = condition + # Extract just the raw condition text + var found: RegExMatch = regex.WRAPPED_CONDITION_REGEX.search(tree_line.text) + line.expression_text = found.strings[found.names.expression] + tree_line.text = regex.WRAPPED_CONDITION_REGEX.sub(tree_line.text, "").strip_edges() # Find the original response in this group of responses. var original_response: DMTreeLine = tree_line - for i in range(sibling_index - 1, 0, -1): + for i in range(sibling_index - 1, -1, -1): if siblings[i].type == DMConstants.TYPE_RESPONSE: original_response = siblings[i] elif siblings[i].type != DMConstants.TYPE_UNKNOWN: @@ -690,14 +700,32 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: for i in range(0, tree_line.children.size()): var child: DMTreeLine = tree_line.children[i] if child.type == DMConstants.TYPE_DIALOGUE: + # Nested dialogue lines cannot have further nested dialogue. + if child.children.size() > 0: + add_error(child.children[0].line_number, child.children[0].indent, DMConstants.ERR_INVALID_INDENTATION) + # Mark this as a dialogue child of another dialogue line. + child.is_nested_dialogue = true + var child_line = DMCompiledLine.new("", DMConstants.TYPE_DIALOGUE) + parse_character_and_dialogue(child, child_line, [], 0, parent) + var child_static_line_id: String = extract_static_line_id(child.text) + if child_line.character != "" or child_static_line_id != "": + add_error(child.line_number, child.indent, DMConstants.ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE) + # Check that only the last child (or none) has a jump reference + if i < tree_line.children.size() - 1 and " =>" in child.text: + add_error(child.line_number, child.indent, DMConstants.ERR_NESTED_DIALOGUE_INVALID_JUMP) + if i == 0 and " =>" in tree_line.text: + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_NESTED_DIALOGUE_INVALID_JUMP) + tree_line.text += "\n" + child.text + elif child.type == DMConstants.TYPE_UNKNOWN: + tree_line.text += "\n" else: result = add_error(child.line_number, child.indent, DMConstants.ERR_INVALID_INDENTATION) # Extract the static line ID var static_line_id: String = extract_static_line_id(tree_line.text) if static_line_id: - tree_line.text = tree_line.text.replace("[ID:%s]" % [static_line_id], "") + tree_line.text = tree_line.text.replace(" [ID:", "[ID:").replace("[ID:%s]" % [static_line_id], "") line.translation_key = static_line_id # Check for simultaneous lines @@ -730,7 +758,7 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: if expression.size() == 0: add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_EXPRESSION) elif expression[0].type == DMConstants.TYPE_ERROR: - add_error(tree_line.line_number, tree_line.indent + expression[0].index, expression[0].value) + add_error(tree_line.line_number, tree_line.indent + expression[0].i, expression[0].value) # If the line isn't part of a weighted random group then make it point to the next # available sibling. @@ -817,9 +845,14 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s # Replace any newlines. text = text.replace("\\n", "\n").strip_edges() - # If there was no manual translation key then just use the text itself - if line.translation_key == "": - line.translation_key = text + # If there was no manual translation key then just use the text itself (unless this is a + # child dialogue below another dialogue line). + if not tree_line.is_nested_dialogue and line.translation_key == "": + # Show an error if missing translations is enabled + if DMSettings.get_setting(DMSettings.MISSING_TRANSLATIONS_ARE_ERRORS, false): + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_MISSING_ID) + else: + line.translation_key = text line.text = text @@ -829,9 +862,6 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_DUPLICATE_ID) else: _known_translation_keys[line.translation_key] = line.text - # Show an error if missing translations is enabled - elif DMSettings.get_setting(DMSettings.MISSING_TRANSLATIONS_ARE_ERRORS, false): - result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_MISSING_ID) return result @@ -1009,7 +1039,7 @@ func extract_condition(text: String, is_wrapped: bool, index: int) -> Dictionary } elif expression[0].type == DMConstants.TYPE_ERROR: return { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: @@ -1037,7 +1067,7 @@ func extract_mutation(text: String) -> Dictionary: } elif expression[0].type == DMConstants.TYPE_ERROR: return { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd index 972fd5cb..9e34f098 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiled_line.gd @@ -26,6 +26,8 @@ var concurrent_lines: PackedStringArray = [] var tags: PackedStringArray = [] ## The condition or mutation expression for this line. var expression: Dictionary = {} +## The express as the raw text that was given. +var expression_text: String = "" ## The next sequential line to go to after this line. var next_id: String = "" ## The next line to go to after this line if it is unknown and compile time. @@ -129,6 +131,8 @@ func to_data() -> Dictionary: d.tags = tags if not notes.is_empty(): d.notes = notes + if not expression_text.is_empty(): + d.condition_as_text = expression_text DMConstants.TYPE_DIALOGUE: d.text = text diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd index ead998ba..ca104135 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/compiler_regex.gd @@ -38,6 +38,7 @@ var TOKEN_DEFINITIONS: Dictionary = { DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), + DMConstants.TOKEN_NULL_COALESCE: RegEx.create_from_string("^\\?\\."), DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."), DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"), DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd index 384340fc..7dfb2afc 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/expression_parser.gd @@ -2,6 +2,9 @@ class_name DMExpressionParser extends RefCounted +var include_comments: bool = false + + # Reference to the common [RegEx] that the parser needs. var regex: DMCompilerRegEx = DMCompilerRegEx.new() @@ -25,7 +28,7 @@ func tokenise(text: String, line_type: String, index: int) -> Array: index += 1 text = text.substr(1) else: - return _build_token_tree_error(DMConstants.ERR_INVALID_EXPRESSION, index) + return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index) return _build_token_tree(tokens, line_type, "")[0] @@ -59,7 +62,7 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]: } elif expression[0].type == DMConstants.TYPE_ERROR: replacement = { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: @@ -76,8 +79,13 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]: # Create a token that represents an error. -func _build_token_tree_error(error: int, index: int) -> Array: - return [{ type = DMConstants.TOKEN_ERROR, value = error, index = index }] +func _build_token_tree_error(tree: Array, error: int, index: int) -> Array: + tree.insert(0, { + type = DMConstants.TOKEN_ERROR, + value = error, + i = index + }) + return tree # Convert a list of tokens into an abstract syntax tree. @@ -91,14 +99,22 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var error = _check_next_token(token, tokens, line_type, expected_close_token) if error != OK: var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token - return [_build_token_tree_error(error, error_token.index), tokens] + return [_build_token_tree_error(tree, error, error_token.index), tokens] match token.type: + DMConstants.TOKEN_COMMENT: + if include_comments: + tree.append({ + type = DMConstants.TOKEN_COMMENT, + value = token.value, + i = token.index + }) + DMConstants.TOKEN_FUNCTION: var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] tree.append({ type = DMConstants.TOKEN_FUNCTION, @@ -113,11 +129,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var args = _tokens_to_list(sub_tree[0]) if args.size() != 1: - return [_build_token_tree_error(DMConstants.ERR_INVALID_INDEX, token.index), tokens] + return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens] tree.append({ type = DMConstants.TOKEN_DICTIONARY_REFERENCE, @@ -132,7 +148,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var t = sub_tree[0] for i in range(0, t.size() - 2): @@ -154,7 +170,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var type = DMConstants.TOKEN_ARRAY var value = _tokens_to_list(sub_tree[0]) @@ -177,7 +193,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] tree.append({ type = DMConstants.TOKEN_GROUP, @@ -190,7 +206,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl DMConstants.TOKEN_BRACE_CLOSE, \ DMConstants.TOKEN_BRACKET_CLOSE: if token.type != expected_close_token: - return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] tree.append({ type = token.type, @@ -211,10 +227,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_COLON, \ - DMConstants.TOKEN_DOT: + DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE: tree.append({ type = token.type, - i = token.index + i = token.index }) DMConstants.TOKEN_COMPARISON, \ @@ -230,7 +247,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl tree.append({ type = token.type, value = value, - i = token.index + i = token.index }) DMConstants.TOKEN_STRING: @@ -248,7 +265,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl }) DMConstants.TOKEN_CONDITION: - return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] DMConstants.TOKEN_BOOL: tree.append({ @@ -280,8 +297,8 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl }) if expected_close_token != "": - var index: int = tokens[0].index if tokens.size() > 0 else 0 - return [_build_token_tree_error(DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] + var index: int = tokens[0].i if tokens.size() > 0 else 0 + return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] return [tree, tokens] @@ -347,8 +364,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_COMPARISON, \ DMConstants.TOKEN_OPERATOR, \ - DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE, \ DMConstants.TOKEN_NOT, \ DMConstants.TOKEN_AND_OR, \ DMConstants.TOKEN_DICTIONARY_REFERENCE: @@ -366,6 +383,20 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_DOT ] + DMConstants.TOKEN_COMMA: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_PARENS_CLOSE, + DMConstants.TOKEN_BRACE_CLOSE, + DMConstants.TOKEN_BRACKET_CLOSE, + DMConstants.TOKEN_DOT + ] + DMConstants.TOKEN_COLON: unexpected_token_types = [ DMConstants.TOKEN_COMMA, @@ -409,7 +440,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.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): + 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 DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd index f172a8a4..667daad1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/compiler/tree_line.gd @@ -22,6 +22,8 @@ var text: String = "" var children: Array[DMTreeLine] = [] ## Any doc comments attached to this line. var notes: String = "" +## Is this a dialogue line that is the child of another dialogue line? +var is_nested_dialogue: bool = false func _init(initial_id: String) -> void: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd index e5a7c8c5..df9a3738 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit.gd @@ -53,6 +53,10 @@ var font_size: int: var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s") +var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new() +var _autoloads: Dictionary[String, String] = {} +var _autoload_member_cache: Dictionary[String, Dictionary] = {} + func _ready() -> void: # Add error gutter @@ -65,6 +69,10 @@ func _ready() -> void: syntax_highlighter = DMSyntaxHighlighter.new() + # Keep track of any autoloads + ProjectSettings.settings_changed.connect(_on_project_settings_changed) + _on_project_settings_changed() + func _gui_input(event: InputEvent) -> void: # Handle shortcuts that come from the editor @@ -141,6 +149,7 @@ func _request_code_completion(force: bool) -> void: var cursor: Vector2 = get_cursor() var current_line: String = get_line(cursor.y) + # Match jumps if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")): var prompt: String = current_line.split("=>")[1] if prompt.begins_with("< "): @@ -166,9 +175,8 @@ func _request_code_completion(force: bool) -> void: add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons")) elif matches_prompt(prompt, title): add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons")) - update_code_completion_options(true) - return + # Match character names 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 @@ -176,9 +184,72 @@ func _request_code_completion(force: bool) -> void: if names.size() > 0: for name in names: add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons")) - update_code_completion_options(true) - else: - cancel_code_completion() + + # Match autoloads on mutation lines + for prefix in ["do ", "do! ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]: + if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))): + var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3) + # Find the last couple of tokens + var possible_prompt: String = expression.reverse() + possible_prompt = possible_prompt.substr(0, possible_prompt.find(" ")) + possible_prompt = possible_prompt.substr(0, possible_prompt.find("(")) + possible_prompt = possible_prompt.reverse() + var segments: PackedStringArray = possible_prompt.split(".").slice(-2) + var auto_completes: Array[Dictionary] = [] + + # Autoloads and state shortcuts + if segments.size() == 1: + var prompt: String = segments[0] + for autoload in _autoloads.keys(): + if matches_prompt(prompt, autoload): + auto_completes.append({ + prompt = prompt, + text = autoload, + type = "script" + }) + for autoload in get_state_shortcuts(): + for member: Dictionary in get_members_for_autoload(autoload): + if matches_prompt(prompt, member.name): + auto_completes.append({ + prompt = prompt, + text = member.name, + type = member.type + }) + + # Members of an autoload + elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "): + var prompt: String = segments[1] + for member: Dictionary in get_members_for_autoload(segments[0]): + if matches_prompt(prompt, member.name): + auto_completes.append({ + prompt = prompt, + text = member.name, + type = member.type + }) + + auto_completes.sort_custom(func(a, b): return a.text < b.text) + + for auto_complete in auto_completes: + var icon: Texture2D + var text: String = auto_complete.text + match auto_complete.type: + "script": + icon = get_theme_icon("Script", "EditorIcons") + "property": + icon = get_theme_icon("MemberProperty", "EditorIcons") + "method": + icon = get_theme_icon("MemberMethod", "EditorIcons") + text += "()" + "signal": + icon = get_theme_icon("MemberSignal", "EditorIcons") + "constant": + icon = get_theme_icon("MemberConstant", "EditorIcons") + var insert: String = text.substr(auto_complete.prompt.length()) + add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon) + + update_code_completion_options(true) + if get_code_completion_options().size() == 0: + cancel_code_completion() func _filter_code_completion_candidates(candidates: Array) -> Array: @@ -190,17 +261,21 @@ func _confirm_code_completion(replace: bool) -> void: var completion = get_code_completion_option(get_code_completion_selected_index()) begin_complex_operation() # Delete any part of the text that we've already typed - for i in range(0, completion.display_text.length() - completion.insert_text.length()): - backspace() + if completion.insert_text.length() > 0: + for i in range(0, completion.display_text.length() - completion.insert_text.length()): + backspace() # Insert the whole match insert_text_at_caret(completion.display_text) end_complex_operation() + if completion.display_text.ends_with("()"): + set_cursor(get_cursor() - Vector2.RIGHT) + # Close the autocomplete menu on the next tick call_deferred("cancel_code_completion") -### Helpers +#region Helpers # Get the current caret as a Vector2 @@ -219,6 +294,69 @@ func matches_prompt(prompt: String, matcher: String) -> bool: return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower()) +func get_state_shortcuts() -> PackedStringArray: + # Get any shortcuts defined in settings + var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, []) + # Check for "using" clauses + for line: String in text.split("\n"): + var found: RegExMatch = compiler_regex.USING_REGEX.search(line) + if found: + shortcuts.append(found.strings[found.names.state]) + # Check for any other script sources + for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []): + shortcuts.append(extra_script_source) + + return shortcuts + + +func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]: + # Debounce method list lookups + if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 5000: + return _autoload_member_cache.get(autoload_name).get("members") + + if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return [] + + var autoload = load(_autoloads.get(autoload_name, autoload_name)) + var script: Script = autoload if autoload is Script else autoload.get_script() + + if not is_instance_valid(script): return [] + + var members: Array[Dictionary] = [] + if script.resource_path.ends_with(".gd"): + for m: Dictionary in script.get_script_method_list(): + if not m.name.begins_with("@"): + members.append({ + name = m.name, + type = "method" + }) + for m: Dictionary in script.get_script_property_list(): + members.append({ + name = m.name, + type = "property" + }) + for m: Dictionary in script.get_script_signal_list(): + members.append({ + name = m.name, + type = "signal" + }) + for c: String in script.get_script_constant_map(): + members.append({ + name = c, + type = "constant" + }) + elif script.resource_path.ends_with(".cs"): + var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new() + for m: Dictionary in dotnet.GetMembersForAutoload(script): + members.append(m) + + _autoload_member_cache[autoload_name] = { + at = Time.get_ticks_msec(), + members = members + } + + return members + + ## Get a list of titles from the current text func get_titles() -> PackedStringArray: var titles = PackedStringArray([]) @@ -417,7 +555,18 @@ func move_line(offset: int) -> void: scroll_vertical = starting_scroll + offset -### Signals +#endregion + +#region Signals + + +func _on_project_settings_changed() -> void: + _autoloads = {} + var project = ConfigFile.new() + project.load("res://project.godot") + for autoload in project.get_section_keys("autoload"): + if autoload != "DialogueManager": + _autoloads[autoload] = project.get_value("autoload", autoload).substr(1) func _on_code_edit_symbol_validate(symbol: String) -> void: @@ -456,3 +605,6 @@ 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: error_clicked.emit(line) + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd index 1553d1f1..3f4e0d93 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd @@ -14,6 +14,8 @@ func _clear_highlighting_cache() -> void: func _get_line_syntax_highlighting(line: int) -> Dictionary: + expression_parser.include_comments = true + var colors: Dictionary = {} var text_edit: TextEdit = get_text_edit() var text: String = text_edit.get_line(line) @@ -38,9 +40,10 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: DMConstants.TYPE_IMPORT: colors[index] = { color = theme.conditions_color } var import: RegExMatch = regex.IMPORT_REGEX.search(text) - colors[index + import.get_start("path") - 1] = { color = theme.strings_color } - colors[index + import.get_end("path") + 1] = { color = theme.conditions_color } - colors[index + import.get_start("prefix")] = { color = theme.text_color } + if import: + colors[index + import.get_start("path") - 1] = { color = theme.strings_color } + colors[index + import.get_end("path") + 1] = { color = theme.conditions_color } + colors[index + import.get_start("prefix")] = { color = theme.text_color } DMConstants.TYPE_COMMENT: colors[index] = { color = theme.comments_color } @@ -53,7 +56,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: index = text.find(" ") if index > -1: var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0) - if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + if expression.size() == 0: colors[index] = { color = theme.critical_color } else: _highlight_expression(expression, colors, index) @@ -62,7 +65,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: colors[0] = { color = theme.mutations_color } index = text.find(" ") var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0) - if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + if expression.size() == 0: colors[index] = { color = theme.critical_color } else: _highlight_expression(expression, colors, index) @@ -84,9 +87,13 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: var dialogue_text: String = text.substr(index, text.find("=>")) - # Highlight character name + # Highlight character name (but ignore ":" within line ID reference) var split_index: int = dialogue_text.replace("\\:", "??").find(":") - colors[index + split_index + 1] = { color = theme.text_color } + if text.substr(split_index - 3, 3) != "[ID": + colors[index + split_index + 1] = { color = theme.text_color } + else: + # If there's no character name then just highlight the text as dialogue. + colors[index] = { color = theme.text_color } # Interpolation var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text) @@ -155,6 +162,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int for token: Dictionary in tokens: last_index = token.i match token.type: + DMConstants.TOKEN_COMMENT: + colors[index + token.i] = { color = theme.comments_color } + DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: colors[index + token.i] = { color = theme.conditions_color } @@ -164,7 +174,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int else: colors[index + token.i] = { color = theme.members_color } - DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, DMConstants.TOKEN_COMMA, DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: + DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \ + DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \ + DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: colors[index + token.i] = { color = theme.symbols_color } DMConstants.TOKEN_STRING: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd index 2614ecaa..de64b717 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/find_in_files.gd @@ -99,7 +99,7 @@ func update_results_view() -> void: 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]" + highlight = "[bgcolor=" + colors.notice_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): diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd index e91574e9..ceb40280 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/components/search_and_replace.gd @@ -114,7 +114,7 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int: return line.findn(text, from_index) -### Signals +#region Signals func _on_text_edit_gui_input(event: InputEvent) -> void: @@ -177,14 +177,17 @@ func _on_replace_button_pressed() -> void: # Replace the selection at result index var r: Array = results[result_index] + code_edit.begin_complex_operation() var lines: PackedStringArray = code_edit.text.split("\n") var line: String = lines[r[0]] line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2]) lines[r[0]] = line code_edit.text = "\n".join(lines) - search(input.text, result_index) + code_edit.end_complex_operation() code_edit.text_changed.emit() + search(input.text, result_index) + func _on_replace_all_button_pressed() -> void: if match_case_button.button_pressed: @@ -210,3 +213,6 @@ func _on_input_focus_entered() -> void: func _on_match_case_check_box_toggled(button_pressed: bool) -> void: search() + + +#endregion diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd index 91e6bc7c..56b5f02e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/constants.gd @@ -37,6 +37,7 @@ const TOKEN_COMPARISON = &"comparison" const TOKEN_ASSIGNMENT = &"assignment" const TOKEN_OPERATOR = &"operator" const TOKEN_COMMA = &"comma" +const TOKEN_NULL_COALESCE = &"null_coalesce" const TOKEN_DOT = &"dot" const TOKEN_CONDITION = &"condition" const TOKEN_BOOL = &"bool" @@ -119,6 +120,12 @@ const ERR_ONLY_ONE_ELSE_ALLOWED = 137 const ERR_WHEN_MUST_BELONG_TO_MATCH = 138 const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139 const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140 +const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141 +const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142 + + +static var _current_locale: String = "" +static var _current_translation: Translation ## Get the error message @@ -204,16 +211,21 @@ static func get_error_message(error: int) -> String: return translate(&"errors.concurrent_line_without_origin") ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: return translate(&"errors.goto_not_allowed_on_concurrect_lines") + ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE: + return translate(&"errors.unexpected_syntax_on_nested_dialogue_line") + ERR_NESTED_DIALOGUE_INVALID_JUMP: + return translate(&"errors.err_nested_dialogue_invalid_jump") return translate(&"errors.unknown") static func translate(string: String) -> String: - 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) + var locale: String = TranslationServer.get_tool_locale() + if _current_translation == null or _current_locale != locale: + var base_path: String = new().get_script().resource_path.get_base_dir() + var translation_path: String = "%s/l10n/%s.po" % [base_path, locale] + var fallback_translation_path: String = "%s/l10n/%s.po" % [base_path, locale.substr(0, 2)] + var en_translation_path: String = "%s/l10n/en.po" % base_path + _current_translation = load(translation_path if FileAccess.file_exists(translation_path) else (fallback_translation_path if FileAccess.file_exists(fallback_translation_path) else en_translation_path)) + _current_locale = locale + return _current_translation.get_message(string) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd index ab9a17ed..e073107e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_manager.gd @@ -65,6 +65,8 @@ var _method_info_cache: Dictionary = {} var _dotnet_dialogue_manager: RefCounted +var _expression_parser: DMExpressionParser = DMExpressionParser.new() + func _ready() -> void: # Cache the known Node2D properties @@ -185,6 +187,7 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) continue elif await _check_case_value(value, case, extra_game_states): next_id = case.next_id + break # Nothing matched so check for else case if next_id == "": if not else_case.is_empty(): @@ -206,16 +209,6 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) else: cummulative_weight += sibling.weight - # Find any simultaneously said lines. - var concurrent_lines: Array[DialogueLine] = [] - if data.has(&"concurrent_lines"): - # If the list includes this line then it isn't the origin line so ignore it. - if not data.concurrent_lines.has(data.id): - for concurrent_id: String in data.concurrent_lines: - var concurrent_line: DialogueLine = await get_line(resource, concurrent_id, extra_game_states) - if concurrent_line: - concurrent_lines.append(concurrent_line) - # If this line is blank and it's the last line then check for returning snippets. if data.type in [DMConstants.TYPE_COMMENT, DMConstants.TYPE_UNKNOWN]: if data.next_id in [DMConstants.ID_END, DMConstants.ID_NULL, null]: @@ -252,11 +245,18 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) # Set up a line object. var line: DialogueLine = await create_dialogue_line(data, extra_game_states) - line.concurrent_lines = concurrent_lines # If the jump point somehow has no content then just end. if not line: return null + # Find any simultaneously said lines. + if data.has(&"concurrent_lines"): + # If the list includes this line then it isn't the origin line so ignore it. + if not data.concurrent_lines.has(data.id): + # Resolve IDs to their actual lines. + for line_id: String in data.concurrent_lines: + line.concurrent_lines.append(await get_line(resource, line_id, extra_game_states)) + # If we are the first of a list of responses then get the other ones. if data.type == DMConstants.TYPE_RESPONSE: # Note: For some reason C# has occasional issues with using the responses property directly @@ -293,7 +293,15 @@ func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> var text: String = translate(data) # Resolve variables - for replacement in data.get(&"text_replacements", [] as Array[Dictionary]): + var text_replacements: Array[Dictionary] = data.get(&"text_replacements", [] as Array[Dictionary]) + if text_replacements.size() == 0 and "{{" in text: + # This line is translated but has expressions that didn't exist in the base text. + text_replacements = _expression_parser.extract_replacements(text, 0) + + for replacement in text_replacements: + if replacement.has("error"): + assert(false, "%s \"%s\"" % [DMConstants.get_error_message(replacement.get("error")), text]) + var value = await _resolve(replacement.expression.duplicate(true), extra_game_states) var index: int = text.find(replacement.value_in_text) if index == -1: @@ -442,6 +450,18 @@ func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, titl return balloon +## Resolve a static line ID to an actual line ID +func static_id_to_line_id(resource: DialogueResource, static_id: String) -> String: + var ids = static_id_to_line_ids(resource, static_id) + if ids.size() == 0: return "" + return ids[0] + + +## Resolve a static line ID to any actual line IDs that match +func static_id_to_line_ids(resource: DialogueResource, static_id: String) -> PackedStringArray: + return resource.lines.values().filter(func(l): return l.get(&"translation_key", "") == static_id).map(func(l): return l.id) + + # Call "start" on the given balloon. func _start_balloon(balloon: Node, resource: DialogueResource, title: String, extra_game_states: Array) -> void: get_current_scene.call().add_child(balloon) @@ -524,7 +544,7 @@ func show_error_for_missing_state_value(message: String, will_show: bool = true) # Translate a string func translate(data: Dictionary) -> String: - if translation_source == DMConstants.TranslationSource.None: + if TranslationServer.get_loaded_locales().size() == 0 or translation_source == DMConstants.TranslationSource.None: return data.text var translation_key: String = data.get(&"translation_key", data.text) @@ -605,12 +625,13 @@ func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResp type = DMConstants.TYPE_RESPONSE, next_id = data.next_id, is_allowed = data.is_allowed, + condition_as_text = data.get(&"condition_as_text", ""), character = await get_resolved_character(data, extra_game_states), character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]), text = resolved_data.text, text_replacements = data.get(&"text_replacements", [] as Array[Dictionary]), tags = data.get(&"tags", []), - translation_key = data.get(&"translation_key", data.text) + translation_key = data.get(&"translation_key", data.text), }) @@ -629,7 +650,7 @@ func _get_game_states(extra_game_states: Array) -> Array: game_states = [_autoloads] # Add any other state shortcuts from settings for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): - var state: Node = Engine.get_main_loop().root.get_node_or_null(node_name) + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) if state: game_states.append(state) @@ -661,21 +682,32 @@ func _check_case_value(match_value: Variant, data: Dictionary, extra_game_states var expression: Array[Dictionary] = data.condition.expression.duplicate(true) - # If the when is a comparison when insert the match value as the first value to compare to - var already_compared: bool = false - if expression[0].type == DMConstants.TOKEN_COMPARISON: - expression.insert(0, { - type = DMConstants.TOKEN_VALUE, - value = match_value - }) - already_compared = true + # Check for multiple values + var expressions_to_check: Array = [] + var previous_comma_index: int = 0 + for i in range(0, expression.size()): + if expression[i].type == DMConstants.TOKEN_COMMA: + expressions_to_check.append(expression.slice(previous_comma_index, i)) + previous_comma_index = i + 1 + elif i == expression.size() - 1: + expressions_to_check.append(expression.slice(previous_comma_index)) - var resolved_value = await _resolve(expression, extra_game_states) + for expression_to_check in expressions_to_check: + # If the when is a comparison when insert the match value as the first value to compare to + var already_compared: bool = false + if expression_to_check[0].type == DMConstants.TOKEN_COMPARISON: + expression_to_check.insert(0, { + type = DMConstants.TOKEN_VALUE, + value = match_value + }) + already_compared = true + + var resolved_value = await _resolve(expression_to_check, extra_game_states) + if (already_compared and resolved_value) or match_value == resolved_value: + return true + + return false - if already_compared: - return resolved_value - else: - return match_value == resolved_value # Make a change to game state or run a method @@ -757,6 +789,13 @@ func _get_state_value(property: String, extra_game_states: Array): if state.has(property): return state.get(property) else: + # Try for a C# constant first + if state.get_script() \ + and state.get_script().resource_path.ends_with(".cs") \ + and _get_dotnet_dialogue_manager().ThingHasConstant(state, property): + return _get_dotnet_dialogue_manager().ResolveThingConstant(state, property) + + # Otherwise just let Godot try and resolve it. var result = expression.execute([], state, false) if not expression.has_execute_failed(): return result @@ -782,7 +821,7 @@ func _warn_about_state_name_collisions(target_key: String, extra_game_states: Ar # Get the list of state shortcuts. var state_shortcuts: Array = [] for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): - var state: Node = Engine.get_main_loop().root.get_node_or_null(node_name) + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) if state: state_shortcuts.append(state) @@ -867,7 +906,18 @@ func _resolve(tokens: Array, extra_game_states: Array): limit += 1 var token: Dictionary = tokens[i] - if token.type == DMConstants.TOKEN_FUNCTION: + if token.type == DMConstants.TOKEN_NULL_COALESCE: + var caller: Dictionary = tokens[i - 1] + if caller.value == null: + # If the caller is null then the method/property is also null + caller.type = DMConstants.TOKEN_VALUE + caller.value = null + tokens.remove_at(i + 1) + tokens.remove_at(i) + else: + token.type = DMConstants.TOKEN_DOT + + elif token.type == DMConstants.TOKEN_FUNCTION: var function_name: String = token.function var args = await _resolve_each(token.value, extra_game_states) if tokens[i - 1].type == DMConstants.TOKEN_DOT: @@ -1330,6 +1380,9 @@ func _is_valid(line: DialogueLine) -> bool: # Check that a thing has a given method. func _thing_has_method(thing, method: String, args: Array) -> bool: + if not is_instance_valid(thing): + return false + if Builtins.is_supported(thing, method): return thing != _autoloads elif thing is Dictionary: @@ -1344,7 +1397,7 @@ func _thing_has_method(thing, method: String, args: Array) -> bool: if thing.has_method(method): return true - if method.to_snake_case() != method and DMSettings.check_for_dotnet_solution(): + if thing.get_script() and thing.get_script().resource_path.ends_with(".cs"): # If we get this far then the method might be a C# method with a Task return type return _get_dotnet_dialogue_manager().ThingHasMethod(thing, method, args) @@ -1363,6 +1416,10 @@ func _thing_has_property(thing: Object, property: String) -> bool: if p.name == property: return true + if thing.get_script() and thing.get_script().resource_path.ends_with(".cs"): + # If we get this far then the property might be a C# constant. + return _get_dotnet_dialogue_manager().ThingHasConstant(thing, property) + return false diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd index 701ce926..479b81cf 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_response.gd @@ -14,6 +14,9 @@ var next_id: String = "" ## [code]true[/code] if the condition of this line was met. var is_allowed: bool = true +## The original condition text. +var condition_as_text: String = "" + ## A character (depending on the "characters in responses" behaviour setting). var character: String = "" @@ -45,6 +48,7 @@ func _init(data: Dictionary = {}) -> void: text_replacements = data.text_replacements tags = data.tags translation_key = data.translation_key + condition_as_text = data.condition_as_text func _to_string() -> String: diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd index cd66ae5b..0ae2c50d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/dialogue_responses_menu.gd @@ -100,16 +100,20 @@ func _configure_focus() -> void: if i == 0: item.focus_neighbor_top = item.get_path() + item.focus_neighbor_left = item.get_path() item.focus_previous = item.get_path() else: item.focus_neighbor_top = items[i - 1].get_path() + item.focus_neighbor_left = 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_neighbor_right = item.get_path() item.focus_next = item.get_path() else: item.focus_neighbor_bottom = items[i + 1].get_path() + item.focus_neighbor_right = items[i + 1].get_path() item.focus_next = items[i + 1].get_path() item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs index 58527122..980f0679 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/ExampleBalloon.cs @@ -5,219 +5,219 @@ namespace DialogueManagerRuntime { public partial class ExampleBalloon : CanvasLayer { - [Export] public string NextAction = "ui_accept"; - [Export] public string SkipAction = "ui_cancel"; + [Export] public string NextAction = "ui_accept"; + [Export] public string SkipAction = "ui_cancel"; - Control balloon; - RichTextLabel characterLabel; - RichTextLabel dialogueLabel; - VBoxContainer responsesMenu; + Control balloon; + RichTextLabel characterLabel; + RichTextLabel dialogueLabel; + VBoxContainer responsesMenu; - Resource resource; - Array temporaryGameStates = new Array(); - bool isWaitingForInput = false; - bool willHideBalloon = false; + Resource resource; + Array temporaryGameStates = new Array(); + bool isWaitingForInput = false; + bool willHideBalloon = false; - DialogueLine dialogueLine; - DialogueLine DialogueLine - { - get => dialogueLine; - set - { - if (value == null) - { - QueueFree(); - return; - } + DialogueLine dialogueLine; + DialogueLine DialogueLine + { + get => dialogueLine; + set + { + if (value == null) + { + QueueFree(); + return; + } - dialogueLine = value; - ApplyDialogueLine(); - } - } + dialogueLine = value; + ApplyDialogueLine(); + } + } - Timer MutationCooldown = new Timer(); + Timer MutationCooldown = new Timer(); - public override void _Ready() - { - balloon = GetNode("%Balloon"); - characterLabel = GetNode("%CharacterLabel"); - dialogueLabel = GetNode("%DialogueLabel"); - responsesMenu = GetNode("%ResponsesMenu"); + public override void _Ready() + { + balloon = GetNode("%Balloon"); + characterLabel = GetNode("%CharacterLabel"); + dialogueLabel = GetNode("%DialogueLabel"); + responsesMenu = GetNode("%ResponsesMenu"); - balloon.Hide(); + 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; - } - } + 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; + if (!isWaitingForInput) return; + if (dialogueLine.Responses.Count > 0) return; - GetViewport().SetInputAsHandled(); + 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 (@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); - })); + if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action"))) + { + responsesMenu.Set("next_action", NextAction); + } + responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => + { + Next(response.NextId); + })); - // Hide the balloon when a mutation is running - MutationCooldown.Timeout += () => - { - if (willHideBalloon) - { - willHideBalloon = false; - balloon.Hide(); - } - }; - AddChild(MutationCooldown); + // Hide the balloon when a mutation is running + MutationCooldown.Timeout += () => + { + if (willHideBalloon) + { + willHideBalloon = false; + balloon.Hide(); + } + }; + AddChild(MutationCooldown); - DialogueManager.Mutated += OnMutated; - } + DialogueManager.Mutated += OnMutated; + } - public override void _ExitTree() - { - 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 override void _UnhandledInput(InputEvent @event) + { + // Only the balloon is allowed to handle input while it's showing + GetViewport().SetInputAsHandled(); + } - public override async void _Notification(int what) - { - // Detect a change of locale and update the current dialogue line to show the new language - if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel)) - { - float visibleRatio = dialogueLabel.VisibleRatio; - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates); - if (visibleRatio < 1.0f) - { - dialogueLabel.Call("skip_typing"); - } - } - } + public override async void _Notification(int what) + { + // Detect a change of locale and update the current dialogue line to show the new language + if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel)) + { + float visibleRatio = dialogueLabel.VisibleRatio; + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates); + if (visibleRatio < 1.0f) + { + dialogueLabel.Call("skip_typing"); + } + } + } - public async void Start(Resource dialogueResource, string title, Array extraGameStates = null) - { - temporaryGameStates = new Array { this } + (extraGameStates ?? new Array()); - isWaitingForInput = false; - resource = dialogueResource; + public async void Start(Resource dialogueResource, string title, Array extraGameStates = null) + { + temporaryGameStates = new Array { this } + (extraGameStates ?? new Array()); + isWaitingForInput = false; + resource = dialogueResource; - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); - } + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); + } - public async void Next(string nextId) - { - DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); - } + public async void Next(string nextId) + { + DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); + } - #region Helpers + #region Helpers - private async void ApplyDialogueLine() - { - MutationCooldown.Stop(); + private async void ApplyDialogueLine() + { + MutationCooldown.Stop(); - isWaitingForInput = false; - balloon.FocusMode = Control.FocusModeEnum.All; - balloon.GrabFocus(); + isWaitingForInput = false; + balloon.FocusMode = Control.FocusModeEnum.All; + balloon.GrabFocus(); - // Set up the character name - characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); - characterLabel.Text = Tr(dialogueLine.Character, "dialogue"); + // 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 dialogue + dialogueLabel.Hide(); + dialogueLabel.Set("dialogue_line", dialogueLine); - // Set up the responses - responsesMenu.Hide(); - responsesMenu.Set("responses", dialogueLine.Responses); + // 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"); - } + // 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(); - } - } + // 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 + #endregion - #region signals + #region signals - private void OnMutated(Dictionary _mutation) - { - isWaitingForInput = false; - willHideBalloon = true; - MutationCooldown.Start(0.1f); - } + private void OnMutated(Dictionary _mutation) + { + isWaitingForInput = false; + willHideBalloon = true; + MutationCooldown.Start(0.1f); + } - #endregion + #endregion } } diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn index 85e82348..64b33658 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/example_balloon.tscn @@ -40,7 +40,7 @@ corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"] bg_color = Color(0, 0, 0, 1) border_width_left = 3 border_width_top = 3 @@ -61,7 +61,7 @@ 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") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt") [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 @@ -77,33 +77,28 @@ grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") -[node name="Panel" type="Panel" parent="Balloon"] -clip_children = 2 +[node name="MarginContainer" type="MarginContainer" parent="Balloon"] 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 +offset_top = -219.0 grow_horizontal = 2 grow_vertical = 0 + +[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] +clip_children = 2 +layout_mode = 2 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"] +[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] layout_mode = 2 -[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 @@ -113,37 +108,35 @@ text = "Character" fit_content = true scroll_active = false -[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_a8ve6")] +[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_a8ve6")] unique_name_in_owner = true layout_mode = 2 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")] +[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -290.5 +offset_top = -35.0 +offset_right = 290.5 +offset_bottom = 35.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 +alignment = 1 script = ExtResource("3_72ixx") response_template = NodePath("ResponseExample") -[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] +[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] layout_mode = 2 text = "Response example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] -[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] +[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn index 6e00358e..57f8e4e1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/example_balloon/small_example_balloon.tscn @@ -66,7 +66,7 @@ corner_radius_top_right = 3 corner_radius_bottom_right = 3 corner_radius_bottom_left = 3 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"] bg_color = Color(0, 0, 0, 1) border_width_left = 1 border_width_top = 1 @@ -87,7 +87,7 @@ 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") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm") [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 @@ -103,33 +103,28 @@ grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") -[node name="Panel" type="Panel" parent="Balloon"] -clip_children = 2 +[node name="MarginContainer" type="MarginContainer" 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 +offset_top = -71.0 grow_horizontal = 2 grow_vertical = 0 + +[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] +clip_children = 2 +layout_mode = 2 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"] +[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] layout_mode = 2 -[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 @@ -139,36 +134,33 @@ text = "Character" fit_content = true scroll_active = false -[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_hfvdi")] +[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_hfvdi")] unique_name_in_owner = true layout_mode = 2 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 = -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"] +[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -116.5 +offset_top = -9.0 +offset_right = 116.5 +offset_bottom = 9.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 script = ExtResource("3_1j1j0") -[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] +[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] layout_mode = 2 text = "Response Example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] -[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] +[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd index 1cdcb7e8..ef65500a 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/export_plugin.gd @@ -3,10 +3,16 @@ class_name DMExportPlugin extends EditorExportPlugin const IGNORED_PATHS = [ "/assets", "/components", - "/l10n", - "/views" + "/views", + "inspector_plugin", + "test_scene" ] + +func _get_name() -> String: + return "Dialogue Manager Export Plugin" + + func _export_file(path: String, type: String, features: PackedStringArray) -> void: var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path() diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd index 345fe844..9c4ad78b 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/import_plugin.gd @@ -5,12 +5,15 @@ class_name DMImportPlugin extends EditorImportPlugin signal compiled_resource(resource: Resource) -const COMPILER_VERSION = 14 +const COMPILER_VERSION = 15 func _get_importer_name() -> String: - # NOTE: A change to this forces a re-import of all dialogue - return "dialogue_manager_compiler_%s" % COMPILER_VERSION + return "dialogue_manager" + + +func _get_format_version() -> int: + return COMPILER_VERSION func _get_visible_name() -> String: @@ -74,7 +77,7 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo if result.errors.size() > 0: printerr("%d errors found in %s" % [result.errors.size(), source_file]) cache.add_errors_to_file(source_file, result.errors) - return ERR_PARSE_ERROR + return OK # Get the current addon version var config: ConfigFile = ConfigFile.new() diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po index 5e241657..132bff33 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/en.po @@ -5,7 +5,7 @@ msgstr "" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" -"Language: de\n" +"Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -108,6 +108,15 @@ msgstr "End dialogue" msgid "generate_line_ids" msgstr "Generate line IDs" +msgid "use_uuid_only_for_ids" +msgstr "Use UUID only for IDs" + +msgid "set_id_prefix_length" +msgstr "Set ID prefix length" + +msgid "id_prefix_length" +msgstr "ID prefix length:" + msgid "save_characters_to_csv" msgstr "Save character names to CSV..." @@ -324,6 +333,12 @@ msgstr "Concurrent lines need an origin line that doesn't start with \"| \"." msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "Goto references are not allowed on concurrent dialogue lines." +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "Nested dialogue lines may only contain dialogue." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Only the last line of nested dialogue is allowed to include a jump." + msgid "errors.unknown" msgstr "Unknown syntax." diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po index ef604e1d..6c6bffbb 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/es.po @@ -92,6 +92,15 @@ msgstr "Finalizar diálogo" msgid "generate_line_ids" msgstr "Generar IDs de línea" +msgid "use_uuid_only_for_ids" +msgstr "Usar solo UUID como ID" + +msgid "set_id_prefix_length" +msgstr "Establecer la longitud del prefijo de ID" + +msgid "id_prefix_length" +msgstr "Longitud del prefijo de ID:" + msgid "save_characters_to_csv" msgstr "Guardar los nombres de los personajes en un archivo CSV..." diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot index 85cb8ac9..ad06ac7e 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/translations.pot @@ -101,6 +101,15 @@ msgstr "" msgid "generate_line_ids" msgstr "" +msgid "use_uuid_only_for_ids" +msgstr "" + +msgid "set_id_prefix_length" +msgstr "" + +msgid "id_prefix_length" +msgstr "" + msgid "save_to_csv" msgstr "" @@ -314,6 +323,12 @@ msgstr "" msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "" +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "" + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "" + msgid "errors.unknown" msgstr "" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po index 8cd41ac4..43dd51b6 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/uk.po @@ -104,6 +104,15 @@ msgstr "Кінець діалогу" msgid "generate_line_ids" msgstr "Згенерувати ідентифікатори рядків" +msgid "use_uuid_only_for_ids" +msgstr "Використовувати лише UUID як ID" + +msgid "set_id_prefix_length" +msgstr "Встановити довжину префікса ID" + +msgid "id_prefix_length" +msgstr "Довжина префікса ID:" + msgid "save_characters_to_csv" msgstr "Зберегти імена персонажів в CSV..." @@ -320,6 +329,12 @@ msgstr "Паралельні рядки потребують початково msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "У паралельних діалогових рядках не допускаються Goto посилання." +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "Вкладені рядки діалогу можуть містити лише діалог." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Лише останній рядок вкладеного діалогу може містити перехід." + msgid "errors.unknown" msgstr "Невідомий синтаксис." diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po index bafd1d58..2a48a51d 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh.po @@ -95,6 +95,15 @@ msgstr "结束对话" msgid "generate_line_ids" msgstr "生成行 ID" +msgid "use_uuid_only_for_ids" +msgstr "仅使用UUID作为ID" + +msgid "set_id_prefix_length" +msgstr "设置ID前缀长度" + +msgid "id_prefix_length" +msgstr "ID前缀长度:" + msgid "save_characters_to_csv" msgstr "保存角色到 CSV" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po index e20feee8..1be98135 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/l10n/zh_TW.po @@ -95,6 +95,15 @@ msgstr "結束對話" msgid "generate_line_ids" msgstr "生成行 ID" +msgid "use_uuid_only_for_ids" +msgstr "僅使用 UUID 作為 ID" + +msgid "set_id_prefix_length" +msgstr "設定 ID 前綴長度" + +msgid "id_prefix_length" +msgstr "ID 前綴長度:" + msgid "save_characters_to_csv" msgstr "保存角色到 CSV" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg index 9b558dbb..c8fa08f1 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.cfg @@ -3,5 +3,5 @@ name="Dialogue Manager" description="A powerful nonlinear dialogue system" author="Nathan Hoad" -version="3.4.0" +version="3.7.1" script="plugin.gd" diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd index 4c4edb24..a5d607fa 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/plugin.gd @@ -13,9 +13,15 @@ var main_view var dialogue_cache: DMCache -func _enter_tree() -> void: +func _enable_plugin() -> void: add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd") + +func _disable_plugin() -> void: + remove_autoload_singleton("DialogueManager") + + +func _enter_tree() -> void: if Engine.is_editor_hint(): Engine.set_meta("DialogueManagerPlugin", self) @@ -105,8 +111,6 @@ func _enter_tree() -> void: func _exit_tree() -> void: - remove_autoload_singleton("DialogueManager") - remove_import_plugin(import_plugin) import_plugin = null @@ -172,6 +176,11 @@ func _apply_changes() -> void: _update_localization() +func _save_external_data() -> void: + if dialogue_cache != null: + dialogue_cache.reimport_files() + + func _build() -> bool: # If this is the dotnet Godot then we need to check if the solution file exists DMSettings.check_for_dotnet_solution() @@ -320,6 +329,9 @@ func update_import_paths(from_path: String, to_path: String) -> void: func _update_localization() -> void: + if not DMSettings.get_setting(DMSettings.UPDATE_POT_FILES_AUTOMATICALLY, true): + return + var dialogue_files = dialogue_cache.get_files() # Add any new files to POT generation diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd index 0a0c12f2..01ed4089 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/settings.gd @@ -23,9 +23,13 @@ const EXTRA_CSV_LOCALES = "editor/translations/extra_csv_locales" const INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS = "editor/translations/include_character_in_translation_exports" ## Includes a "_notes" column in CSV exports const INCLUDE_NOTES_IN_TRANSLATION_EXPORTS = "editor/translations/include_notes_in_translation_exports" +## Automatically update the project's list of translatable files when dialogue files are added or removed +const UPDATE_POT_FILES_AUTOMATICALLY = "editor/translations/update_pot_files_automatically" ## A custom test scene to use when testing dialogue. const CUSTOM_TEST_SCENE_PATH = "editor/advanced/custom_test_scene_path" +## Extra script files to include in the auto-complete-able list +const EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES = "editor/advanced/extra_auto_complete_script_sources" ## The custom balloon for this game. const BALLOON_PATH = "runtime/balloon_path" @@ -38,9 +42,12 @@ const WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS = "runtime/warn_about_ const IGNORE_MISSING_STATE_VALUES = "runtime/advanced/ignore_missing_state_values" ## Whether or not the project is utilising dotnet. const USES_DOTNET = "runtime/advanced/uses_dotnet" +## Maximum length of text prefix in auto-generated IDs +const AUTO_GENERATED_ID_PREFIX_LENGTH = "editor/translations/auto_generated_id_prefix_length" +## Use only UUID for auto-generated IDs without text prefix +const USE_UUID_ONLY_FOR_IDS = "editor/translations/use_uuid_only_for_ids" - -const SETTINGS_CONFIGURATION = { +static var SETTINGS_CONFIGURATION = { WRAP_LONG_LINES: { value = false, type = TYPE_BOOL, @@ -68,6 +75,8 @@ const SETTINGS_CONFIGURATION = { EXTRA_CSV_LOCALES: { value = [], type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], is_advanced = true }, INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: { @@ -80,6 +89,11 @@ const SETTINGS_CONFIGURATION = { type = TYPE_BOOL, is_advanced = true }, + UPDATE_POT_FILES_AUTOMATICALLY: { + value = true, + type = TYPE_BOOL, + is_advanced = true + }, CUSTOM_TEST_SCENE_PATH: { value = preload("./test_scene.tscn").resource_path, @@ -87,6 +101,13 @@ const SETTINGS_CONFIGURATION = { hint = PROPERTY_HINT_FILE, is_advanced = true }, + EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES: { + value = [], + type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d/%d:*.*" % [TYPE_STRING, PROPERTY_HINT_FILE], + is_advanced = true + }, BALLOON_PATH: { value = "", @@ -96,6 +117,8 @@ const SETTINGS_CONFIGURATION = { STATE_AUTOLOAD_SHORTCUTS: { value = [], type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], }, WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: { value = false, @@ -112,7 +135,19 @@ const SETTINGS_CONFIGURATION = { value = false, type = TYPE_BOOL, is_hidden = true - } + }, + AUTO_GENERATED_ID_PREFIX_LENGTH: { + value = 30, + type = TYPE_INT, + hint = PROPERTY_HINT_RANGE, + hint_string = "0,100,1", + is_advanced = true + }, + USE_UUID_ONLY_FOR_IDS: { + value = false, + type = TYPE_BOOL, + is_advanced = true + }, } @@ -203,7 +238,7 @@ static func get_user_config() -> Dictionary: recent_files = [], reopen_files = [], most_recent_reopen_file = "", - carets = {}, + file_meta = {}, run_title = "", run_resource_path = "", is_running_test_scene = false, @@ -229,10 +264,17 @@ static func set_user_value(key: String, value) -> void: save_user_config(user_config) -static func get_user_value(key: String, default = null): +static func get_user_value(key: String, default = null) -> Variant: return get_user_config().get(key, default) +static func forget_path(path: String) -> void: + remove_recent_file(path) + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta.erase(path) + set_user_value("file_meta", file_meta) + + static func add_recent_file(path: String) -> void: var recent_files: Array = get_user_value("recent_files", []) if path in recent_files: @@ -266,23 +308,34 @@ 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, - y = cursor.y - } - set_user_value("carets", carets) + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta[path] = file_meta.get(path, {}).merged({ cursor = "%d,%d" % [cursor.x, cursor.y] }, true) + set_user_value("file_meta", file_meta) static func get_caret(path: String) -> Vector2: - var carets = get_user_value("carets", {}) - if carets.has(path): - var caret = carets.get(path) - return Vector2(caret.x, caret.y) + var file_meta: Dictionary = get_user_value("file_meta", {}) + if file_meta.has(path): + var cursor: PackedStringArray = file_meta.get(path).get("cursor", "0,0").split(",") + return Vector2(cursor[0].to_int(), cursor[1].to_int()) else: return Vector2.ZERO +static func set_scroll(path: String, scroll_vertical: int) -> void: + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta[path] = file_meta.get(path, {}).merged({ scroll_vertical = scroll_vertical }, true) + set_user_value("file_meta", file_meta) + + +static func get_scroll(path: String) -> int: + var file_meta: Dictionary = get_user_value("file_meta", {}) + if file_meta.has(path): + return file_meta.get(path).get("scroll_vertical", 0) + else: + return 0 + + static func check_for_dotnet_solution() -> bool: if Engine.is_editor_hint(): var has_dotnet_solution: bool = false diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd index dd1da441..33fc5ac2 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/utilities/dialogue_cache.gd @@ -41,6 +41,7 @@ func reimport_files(and_files: PackedStringArray = []) -> void: if _files_marked_for_reimport.is_empty(): return EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) + _files_marked_for_reimport.clear() ## Add a dialogue file to the cache. diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd index b890f6e9..5ef46947 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.gd @@ -30,6 +30,12 @@ signal confirmation_closed() @onready var parse_timer: Timer = $ParseTimer +# Banner +@onready var banner: CenterContainer = %Banner +@onready var banner_new_button: Button = %BannerNewButton +@onready var banner_quick_open: Button = %BannerQuickOpen +@onready var banner_examples: Button = %BannerExamples + # Dialogs @onready var new_dialog: FileDialog = $NewDialog @onready var save_dialog: FileDialog = $SaveDialog @@ -87,6 +93,7 @@ var current_file_path: String = "": title_list.hide() code_edit.hide() errors_panel.hide() + banner.show() else: test_button.disabled = false test_line_button.disabled = false @@ -97,17 +104,25 @@ var current_file_path: String = "": files_list.show() title_list.show() code_edit.show() + banner.hide() + + var cursor: Vector2 = DMSettings.get_caret(current_file_path) + var scroll_vertical: int = DMSettings.get_scroll(current_file_path) code_edit.text = open_buffers[current_file_path].text code_edit.errors = [] code_edit.clear_undo_history() - code_edit.set_cursor(DMSettings.get_caret(current_file_path)) + code_edit.set_cursor(cursor) + code_edit.scroll_vertical = scroll_vertical code_edit.grab_focus() _on_code_edit_text_changed() errors_panel.errors = [] code_edit.errors = [] + + if search_and_replace.visible: + search_and_replace.search() get: return current_file_path @@ -177,6 +192,8 @@ func _ready() -> void: EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) + code_edit.get_v_scroll_bar().value_changed.connect(_on_code_edit_scroll_changed) + func _exit_tree() -> void: DMSettings.set_user_value("reopen_files", open_buffers.keys()) @@ -269,6 +286,12 @@ func open_file(path: String) -> void: self.current_file_path = path +func quick_open() -> void: + quick_open_files_list.files = Engine.get_meta("DMCache").get_files() + quick_open_dialog.popup_centered() + quick_open_files_list.focus_filter() + + func show_file_in_filesystem(path: String) -> void: EditorInterface.get_file_system_dock().navigate_to_path(path) @@ -367,6 +390,9 @@ func apply_theme() -> void: font_size = editor_settings.get_setting("interface/editor/code_font_size") } + banner_new_button.icon = get_theme_icon("New", "EditorIcons") + banner_quick_open.icon = get_theme_icon("Load", "EditorIcons") + new_button.icon = get_theme_icon("New", "EditorIcons") new_button.tooltip_text = DMConstants.translate(&"start_a_new_file") @@ -493,8 +519,8 @@ func show_build_error_dialog() -> void: # Generate translation line IDs for any line that doesn't already have one func generate_translations_keys() -> void: - randomize() - seed(Time.get_unix_time_from_system()) + var rng: RandomNumberGenerator = RandomNumberGenerator.new() + rng.randomize() var cursor: Vector2 = code_edit.get_cursor() var lines: PackedStringArray = code_edit.text.split("\n") @@ -502,6 +528,8 @@ func generate_translations_keys() -> void: var key_regex = RegEx.new() key_regex.compile("\\[ID:(?.*?)\\]") + var compiled_lines: Dictionary = DMCompiler.compile_string(code_edit.text, "").lines + # Make list of known keys var known_keys = {} for i in range(0, lines.size()): @@ -524,6 +552,7 @@ func generate_translations_keys() -> void: var l = line.strip_edges() if not [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE].has(DMCompiler.get_line_type(l)): continue + if not compiled_lines.has(str(i)): continue if "[ID:" in line: continue @@ -538,17 +567,24 @@ func generate_translations_keys() -> void: key = known_keys.find_key(text) else: var regex: DMCompilerRegEx = DMCompilerRegEx.new() - key = regex.ALPHA_NUMERIC.sub(text.strip_edges(), "_", true).substr(0, 30) - if key.begins_with("_"): - key = key.substr(1) - if key.ends_with("_"): - key = key.substr(0, key.length() - 1) + if DMSettings.get_setting(DMSettings.USE_UUID_ONLY_FOR_IDS, false): + # Generate UUID only + var uuid = str(randi() % 1000000).sha1_text().substr(0, 12) + key = uuid.to_upper() + else: + # Generate text prefix + hash + var prefix_length = DMSettings.get_setting(DMSettings.AUTO_GENERATED_ID_PREFIX_LENGTH, 30) + key = regex.ALPHA_NUMERIC.sub(text.strip_edges(), "_", true).substr(0, prefix_length) + if key.begins_with("_"): + key = key.substr(1) + if key.ends_with("_"): + key = key.substr(0, key.length() - 1) - # Make sure key is unique - var hashed_key: String = key + "_" + str(randi() % 1000000).sha1_text().substr(0, 6) - while hashed_key in known_keys and text != known_keys.get(hashed_key): - hashed_key = key + "_" + str(randi() % 1000000).sha1_text().substr(0, 6) - key = hashed_key.to_upper() + # Make sure key is unique + var hashed_key: String = key + "_" + str(randi() % 1000000).sha1_text().substr(0, 6) + while hashed_key in known_keys and text != known_keys.get(hashed_key): + hashed_key = key + "_" + str(randi() % 1000000).sha1_text().substr(0, 6) + key = hashed_key.to_upper() line = line.replace("\\n", "!NEWLINE!") text = text.replace("\n", "!NEWLINE!") @@ -799,6 +835,16 @@ func show_search_form(is_enabled: bool) -> void: search_and_replace.focus_line_edit() +func run_test_scene(from_key: String) -> void: + DMSettings.set_user_value("run_title", from_key) + DMSettings.set_user_value("is_running_test_scene", true) + DMSettings.set_user_value("run_resource_path", current_file_path) + var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") + if ResourceUID.has_id(ResourceUID.text_to_id(test_scene_path)): + test_scene_path = ResourceUID.get_id_path(ResourceUID.text_to_id(test_scene_path)) + EditorInterface.play_custom_scene(test_scene_path) + + ### Signals @@ -831,9 +877,7 @@ func _on_open_menu_id_pressed(id: int) -> void: OPEN_OPEN: open_dialog.popup_centered() OPEN_QUICK: - quick_open_files_list.files = Engine.get_meta("DMCache").get_files() - quick_open_dialog.popup_centered() - quick_open_files_list.focus_filter() + quick_open() OPEN_CLEAR: DMSettings.clear_recent_files() build_open_menu() @@ -923,15 +967,16 @@ func _on_main_view_visibility_changed() -> void: func _on_new_button_pressed() -> void: - new_dialog.current_file = "dialogue" + new_dialog.current_file = "untitled" new_dialog.popup_centered() func _on_new_dialog_confirmed() -> void: - if new_dialog.current_file.get_basename() == "": - var path = "res://untitled.dialogue" - new_file(path) - open_file(path) + var path: String = new_dialog.current_path + if path.get_file() == ".dialogue": + path = "%s/untitled.dialogue" % path.get_basename() + new_file(path) + open_file(path) func _on_new_dialog_file_selected(path: String) -> void: @@ -960,8 +1005,8 @@ func _on_quick_open_files_list_file_double_clicked(file_path: String) -> void: func _on_quick_open_dialog_confirmed() -> void: - if quick_open_files_list.current_file_path: - open_file(quick_open_files_list.current_file_path) + if quick_open_files_list.last_selected_file_path: + open_file(quick_open_files_list.last_selected_file_path) func _on_save_all_button_pressed() -> void: @@ -983,6 +1028,10 @@ func _on_code_edit_text_changed() -> void: parse_timer.start(1) +func _on_code_edit_scroll_changed(value: int) -> void: + DMSettings.set_scroll(current_file_path, code_edit.scroll_vertical) + + func _on_code_edit_active_title_change(title: String) -> void: title_list.select_title(title) @@ -1033,11 +1082,7 @@ func _on_test_button_pressed() -> void: errors_dialog.popup_centered() return - DMSettings.set_user_value("run_title", "") - DMSettings.set_user_value("is_running_test_scene", true) - DMSettings.set_user_value("run_resource_path", current_file_path) - var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") - EditorInterface.play_custom_scene(test_scene_path) + run_test_scene("") func _on_test_line_button_pressed() -> void: @@ -1052,12 +1097,9 @@ func _on_test_line_button_pressed() -> void: for i in range(code_edit.get_cursor().y, code_edit.get_line_count()): if not code_edit.get_line(i).is_empty(): line_to_run = i - break; - DMSettings.set_user_value("run_title", str(line_to_run)) - DMSettings.set_user_value("is_running_test_scene", true) - DMSettings.set_user_value("run_resource_path", current_file_path) - var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") - EditorInterface.play_custom_scene(test_scene_path) + break + + run_test_scene(str(line_to_run)) func _on_support_button_pressed() -> void: @@ -1139,3 +1181,22 @@ func _on_close_confirmation_dialog_custom_action(action: StringName) -> void: 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) + code_edit.set_line_as_center_visible(cursor.y) + + +func _on_banner_image_gui_input(event: InputEvent) -> void: + if event.is_pressed(): + OS.shell_open("https://bravestcoconut.com/wishlist") + + +func _on_banner_new_button_pressed() -> void: + new_dialog.current_file = "untitled" + new_dialog.popup_centered() + + +func _on_banner_quick_open_pressed() -> void: + quick_open() + + +func _on_banner_examples_pressed() -> void: + OS.shell_open("https://itch.io/c/5226650/godot-dialogue-manager-example-projects") diff --git a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn index 4e70b263..b21f7084 100644 --- a/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn +++ b/Zennysoft.Game.Ma/addons/dialogue_manager/views/main_view.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=3 uid="uid://cbuf1q3xsse3q"] +[gd_scene load_steps=16 format=3 uid="uid://cbuf1q3xsse3q"] [ext_resource type="Script" uid="uid://cipjcc7bkh1pc" 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"] @@ -8,20 +8,9 @@ [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" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="7_necsa"] +[ext_resource type="Texture2D" uid="uid://cnm67htuohhlo" path="res://addons/dialogue_manager/assets/banner.png" id="9_y6rqu"] [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_faxki"] -data = { -"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_ka3gk"] -image = SubResource("Image_faxki") - [sub_resource type="Image" id="Image_y6rqu"] data = { "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), @@ -31,10 +20,22 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_57eek"] +[sub_resource type="ImageTexture" id="ImageTexture_ka3gk"] image = SubResource("Image_y6rqu") -[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_kb7f8"] +[sub_resource type="Image" id="Image_mpdoc"] +data = { +"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_57eek"] +image = SubResource("Image_mpdoc") + +[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_xv2j4"] script = ExtResource("7_necsa") [node name="MainView" type="Control"] @@ -132,7 +133,6 @@ size_flags_vertical = 3 unique_name_in_owner = true visible = false layout_mode = 2 -size_flags_vertical = 3 [node name="FilesPopupMenu" type="PopupMenu" parent="Margin/Content/SidePanel/Bookmarks/FilesList"] unique_name_in_owner = true @@ -158,6 +158,7 @@ text = "Insert" item_count = 15 popup/item_0/text = "Wave BBCode" popup/item_0/icon = SubResource("ImageTexture_57eek") +popup/item_0/id = 0 popup/item_1/text = "Shake BBCode" popup/item_1/icon = SubResource("ImageTexture_57eek") popup/item_1/id = 1 @@ -291,9 +292,9 @@ visible = false layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 -theme_override_colors/current_line_color = Color(0.266667, 0.278431, 0.352941, 0.243137) -theme_override_colors/background_color = Color(0.156863, 0.164706, 0.211765, 1) theme_override_colors/font_color = Color(0.972549, 0.972549, 0.94902, 1) +theme_override_colors/background_color = Color(0.156863, 0.164706, 0.211765, 1) +theme_override_colors/current_line_color = Color(0.266667, 0.278431, 0.352941, 0.243137) theme_override_font_sizes/font_size = 14 theme_override_colors/bookmark_color = Color(1, 0.333333, 0.333333, 1) text = "~ start @@ -312,12 +313,57 @@ Coco: Meow. => END" scroll_smooth = true -syntax_highlighter = SubResource("SyntaxHighlighter_kb7f8") +syntax_highlighter = SubResource("SyntaxHighlighter_xv2j4") [node name="ErrorsPanel" parent="Margin/Content/CodePanel" instance=ExtResource("7_5cvl4")] unique_name_in_owner = true layout_mode = 2 +[node name="Banner" type="CenterContainer" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 34.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="PanelContainer" type="VBoxContainer" parent="Banner"] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="BannerImage" type="TextureRect" parent="Banner/PanelContainer"] +custom_minimum_size = Vector2(600, 200) +layout_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 2 +texture = ExtResource("9_y6rqu") +expand_mode = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="Banner/PanelContainer"] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="BannerNewButton" type="Button" parent="Banner/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "New Dialogue..." + +[node name="BannerQuickOpen" type="Button" parent="Banner/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Quick open..." + +[node name="BannerExamples" type="Button" parent="Banner/PanelContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Example projects..." + [node name="NewDialog" type="FileDialog" parent="."] size = Vector2i(900, 750) min_size = Vector2i(600, 500) @@ -417,6 +463,10 @@ code_edit = NodePath("../../Margin/Content/CodePanel/CodeEdit") [connection signal="external_file_requested" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_external_file_requested"] [connection signal="text_changed" from="Margin/Content/CodePanel/CodeEdit" to="." method="_on_code_edit_text_changed"] [connection signal="error_pressed" from="Margin/Content/CodePanel/ErrorsPanel" to="." method="_on_errors_panel_error_pressed"] +[connection signal="gui_input" from="Banner/PanelContainer/BannerImage" to="." method="_on_banner_image_gui_input"] +[connection signal="pressed" from="Banner/PanelContainer/HBoxContainer/BannerNewButton" to="." method="_on_banner_new_button_pressed"] +[connection signal="pressed" from="Banner/PanelContainer/HBoxContainer/BannerQuickOpen" to="." method="_on_banner_quick_open_pressed"] +[connection signal="pressed" from="Banner/PanelContainer/HBoxContainer/BannerExamples" to="." method="_on_banner_examples_pressed"] [connection signal="confirmed" from="NewDialog" to="." method="_on_new_dialog_confirmed"] [connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"] [connection signal="file_selected" from="SaveDialog" to="." method="_on_save_dialog_file_selected"] diff --git a/Zennysoft.Game.Ma/project.godot b/Zennysoft.Game.Ma/project.godot index a93a4fdd..b1962f62 100644 --- a/Zennysoft.Game.Ma/project.godot +++ b/Zennysoft.Game.Ma/project.godot @@ -30,8 +30,8 @@ runtime/advanced/uses_dotnet=true [display] -window/size/viewport_width=1920 -window/size/viewport_height=1080 +window/size/viewport_width=1280 +window/size/viewport_height=720 [dotnet] diff --git a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import index bb7761ed..d06b262d 100644 --- a/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import +++ b/Zennysoft.Game.Ma/src/dialog/Dialogue.dialogue.import @@ -1,6 +1,7 @@ [remap] -importer="dialogue_manager_compiler_14" +importer="dialogue_manager" +importer_version=15 type="Resource" uid="uid://lao0opxww3ib" path="res://.godot/imported/Dialogue.dialogue-176033575bc12c347010f3a30b2e302a.tres" diff --git a/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml b/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml index 112e6c2b..3baf022f 100644 --- a/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml +++ b/Zennysoft.Game.Ma/src/enemy/state/EnemyLogic.g.puml @@ -1,12 +1,12 @@ @startuml EnemyLogic state "EnemyLogic State" as Zennysoft_Game_Ma_EnemyLogic_State { state "Alive" as Zennysoft_Game_Ma_EnemyLogic_State_Alive { + state "Idle" as Zennysoft_Game_Ma_EnemyLogic_State_Idle state "Activated" as Zennysoft_Game_Ma_EnemyLogic_State_Activated { + state "Patrolling" as Zennysoft_Game_Ma_EnemyLogic_State_Patrolling state "FollowPlayer" as Zennysoft_Game_Ma_EnemyLogic_State_FollowPlayer state "Attacking" as Zennysoft_Game_Ma_EnemyLogic_State_Attacking - state "Patrolling" as Zennysoft_Game_Ma_EnemyLogic_State_Patrolling } - state "Idle" as Zennysoft_Game_Ma_EnemyLogic_State_Idle } state "Defeated" as Zennysoft_Game_Ma_EnemyLogic_State_Defeated } diff --git a/Zennysoft.Game.Ma/src/game/Game.tscn b/Zennysoft.Game.Ma/src/game/Game.tscn index ff47afc5..ab34902c 100644 --- a/Zennysoft.Game.Ma/src/game/Game.tscn +++ b/Zennysoft.Game.Ma/src/game/Game.tscn @@ -83,7 +83,7 @@ script = ExtResource("1_ytcii") [node name="SubViewportContainer" type="SubViewportContainer" parent="."] material = SubResource("ShaderMaterial_e75a2") -custom_minimum_size = Vector2(1280, 960) +custom_minimum_size = Vector2(1280, 720) anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -94,7 +94,7 @@ stretch = true [node name="SubViewport" type="SubViewport" parent="SubViewportContainer"] transparent_bg = true handle_input_locally = false -size = Vector2i(1920, 1080) +size = Vector2i(1280, 720) render_target_update_mode = 4 [node name="PauseContainer" type="Node3D" parent="SubViewportContainer/SubViewport"] @@ -119,6 +119,7 @@ wait_time = 30.0 [node name="InGameUI" parent="." instance=ExtResource("5_lxtnp")] unique_name_in_owner = true visible = false +custom_minimum_size = Vector2(1280, 720) [node name="InGameAudio" parent="." instance=ExtResource("6_qc71l")] unique_name_in_owner = true diff --git a/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import b/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import index 53b52b20..09775e7e 100644 --- a/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import +++ b/Zennysoft.Game.Ma/src/npc/Ran/ran.dialogue.import @@ -1,6 +1,7 @@ [remap] -importer="dialogue_manager_compiler_12" +importer="dialogue_manager" +importer_version=15 type="Resource" uid="uid://b681mabgdkg6j" path="res://.godot/imported/ran.dialogue-f0ecb078c74af14bf6b7b4a9926b74f8.tres" diff --git a/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import b/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import index 3a37d471..b14548cf 100644 --- a/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import +++ b/Zennysoft.Game.Ma/src/npc/Rat/ratdialogue.dialogue.import @@ -1,6 +1,7 @@ [remap] -importer="dialogue_manager_compiler_12" +importer="dialogue_manager" +importer_version=15 type="Resource" uid="uid://cf7ycgdiihyh" path="res://.godot/imported/ratdialogue.dialogue-7389c3591865c01b56ddf5a724bb1208.tres" diff --git a/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn b/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn index 0c70a096..d92ac925 100644 --- a/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn +++ b/Zennysoft.Game.Ma/src/ui/in_game_ui/InGameUI.tscn @@ -23,7 +23,7 @@ script = ExtResource("1_sc13i") [node name="PlayerInfoUI" parent="." instance=ExtResource("4_46s5l")] unique_name_in_owner = true -custom_minimum_size = Vector2(1024, 768) +custom_minimum_size = Vector2(1280, 720) layout_mode = 1 [node name="MiniMap" parent="." instance=ExtResource("2_6sfje")] diff --git a/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn b/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn index 7cf138b6..979cf88b 100644 --- a/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn +++ b/Zennysoft.Game.Ma/src/ui/player_ui/PlayerInfoUI.tscn @@ -10,6 +10,7 @@ layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +offset_bottom = 40.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_d8yyu") diff --git a/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn b/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn index dc3f4c0d..3372bf3f 100644 --- a/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn +++ b/Zennysoft.Game.Ma/src/ui/teleport_prompt/UseTeleportPrompt.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=14 format=3 uid="uid://bea2waybmgd6u"] -[ext_resource type="Script" path="res://src/ui/in_game_ui/UseTeleportPrompt.cs" id="1_x3wkp"] +[ext_resource type="Script" uid="uid://dvn7g207w5jaj" path="res://src/ui/in_game_ui/UseTeleportPrompt.cs" id="1_x3wkp"] [ext_resource type="FontFile" uid="uid://cm8j5vcdop5x0" path="res://src/ui/fonts/Mrs-Eaves-OT-Roman_31443.ttf" id="2_i6kb2"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ahhj2"] @@ -102,9 +102,9 @@ tracks/1/keys = { [sub_resource type="AnimationLibrary" id="AnimationLibrary_7x216"] _data = { -"RESET": SubResource("Animation_xrfau"), -"fade_in": SubResource("Animation_efhb5"), -"fade_out": SubResource("Animation_ibgld") +&"RESET": SubResource("Animation_xrfau"), +&"fade_in": SubResource("Animation_efhb5"), +&"fade_out": SubResource("Animation_ibgld") } [node name="UseTeleportPrompt" type="Control"] @@ -145,8 +145,8 @@ focus_neighbor_left = NodePath(".") focus_neighbor_top = NodePath(".") focus_neighbor_right = NodePath(".") focus_neighbor_bottom = NodePath("../NoButton") -theme_override_colors/font_focus_color = Color(1, 0.94902, 0, 1) theme_override_colors/font_color = Color(0.737255, 0.705882, 0.690196, 1) +theme_override_colors/font_focus_color = Color(1, 0.94902, 0, 1) theme_override_fonts/font = ExtResource("2_i6kb2") theme_override_font_sizes/font_size = 50 theme_override_styles/focus = SubResource("StyleBoxEmpty_1tca4") @@ -162,8 +162,8 @@ focus_neighbor_left = NodePath(".") focus_neighbor_top = NodePath("../YesButton") focus_neighbor_right = NodePath(".") focus_neighbor_bottom = NodePath(".") -theme_override_colors/font_focus_color = Color(1, 0.94902, 0, 1) theme_override_colors/font_color = Color(0.737255, 0.705882, 0.690196, 1) +theme_override_colors/font_focus_color = Color(1, 0.94902, 0, 1) theme_override_fonts/font = ExtResource("2_i6kb2") theme_override_font_sizes/font_size = 50 theme_override_styles/focus = SubResource("StyleBoxEmpty_yoep7") @@ -175,5 +175,5 @@ text = "No" [node name="AnimationPlayer" type="AnimationPlayer" parent="."] unique_name_in_owner = true libraries = { -"": SubResource("AnimationLibrary_7x216") +&"": SubResource("AnimationLibrary_7x216") }