Add more NPCs, update dialogue manager to 4.4 compatible version

This commit is contained in:
2025-03-12 00:29:39 -07:00
parent 76b94f7be3
commit 22c9590442
167 changed files with 5874 additions and 5697 deletions

View File

@@ -6,420 +6,457 @@ using System.Threading.Tasks;
#nullable enable #nullable enable
namespace DialogueManagerRuntime; namespace DialogueManagerRuntime
public enum TranslationSource
{ {
None, public enum TranslationSource
Guess,
CSV,
PO
}
public partial class DialogueManager : Node
{
public delegate void PassedTitleEventHandler(string title);
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
public delegate void MutatedEventHandler(Dictionary mutation);
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
public static PassedTitleEventHandler? PassedTitle;
public static GotDialogueEventHandler? GotDialogue;
public static MutatedEventHandler? Mutated;
public static DialogueEndedEventHandler? DialogueEnded;
[Signal] public delegate void ResolvedEventHandler(Variant value);
private static GodotObject? instance;
public static GodotObject Instance
{
get
{ {
if (instance == null) None,
{ Guess,
instance = Engine.GetSingleton("DialogueManager"); CSV,
} PO
return instance;
}
}
public static Godot.Collections.Array GameStates
{
get => (Godot.Collections.Array)Instance.Get("game_states");
set => Instance.Set("game_states", value);
}
public static bool IncludeSingletons
{
get => (bool)Instance.Get("include_singletons");
set => Instance.Set("include_singletons", value);
}
public static bool IncludeClasses
{
get => (bool)Instance.Get("include_classes");
set => Instance.Set("include_classes", value);
}
public static TranslationSource TranslationSource
{
get => (TranslationSource)(int)Instance.Get("translation_source");
set => Instance.Set("translation_source", (int)value);
}
public static Func<Node> GetCurrentScene
{
set => Instance.Set("get_current_scene", Callable.From(value));
}
public void Prepare()
{
Instance.Connect("passed_title", Callable.From((string title) => PassedTitle?.Invoke(title)));
Instance.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line))));
Instance.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation)));
Instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource)));
}
public static async Task<GodotObject> GetSingleton()
{
if (instance != null)
return instance;
var tree = Engine.GetMainLoop();
int x = 0;
// Try and find the singleton for a few seconds
while (!Engine.HasSingleton("DialogueManager") && x < 300)
{
await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame);
x++;
} }
// If it times out something is wrong public partial class DialogueManager : RefCounted
if (x >= 300)
{ {
throw new Exception("The DialogueManager singleton is missing."); 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);
instance = Engine.GetSingleton("DialogueManager"); public static DialogueStartedEventHandler? DialogueStarted;
return instance; public static PassedTitleEventHandler? PassedTitle;
} public static GotDialogueEventHandler? GotDialogue;
public static MutatedEventHandler? Mutated;
public static DialogueEndedEventHandler? DialogueEnded;
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) [Signal] public delegate void ResolvedEventHandler(Variant value);
{
Instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>());
var result = await Instance.ToSignal(Instance, "bridge_get_next_dialogue_line_completed");
if ((RefCounted)result[0] == null) private static GodotObject? instance;
return null; public static GodotObject Instance
{
return new DialogueLine((RefCounted)result[0]); get
} {
if (instance == null)
{
instance = Engine.GetSingleton("DialogueManager");
instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource)));
}
return instance;
}
}
public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) public static Godot.Collections.Array GameStates
{ {
return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>()); get => (Godot.Collections.Array)Instance.Get("game_states");
} set => Instance.Set("game_states", value);
}
public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) public static bool IncludeSingletons
{ {
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>()); get => (bool)Instance.Get("include_singletons");
} set => Instance.Set("include_singletons", value);
}
public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null) public static bool IncludeClasses
{ {
return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>()); get => (bool)Instance.Get("include_classes");
} set => Instance.Set("include_classes", value);
}
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false) public static TranslationSource TranslationSource
{ {
Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation); get => (TranslationSource)(int)Instance.Get("translation_source");
await Instance.ToSignal(Instance, "bridge_mutated"); set => Instance.Set("translation_source", (int)value);
} }
public bool ThingHasMethod(GodotObject thing, string method) public static Func<Node> GetCurrentScene
{ {
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); set => Instance.Set("get_current_scene", Callable.From(value));
return info != null; }
}
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args) public static void Prepare(GodotObject instance)
{ {
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); 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)));
}
if (info == null)
return; public static async Task<GodotObject> GetSingleton()
{
if (instance != null) return instance;
var tree = Engine.GetMainLoop();
int x = 0;
// Try and find the singleton for a few seconds
while (!Engine.HasSingleton("DialogueManager") && x < 300)
{
await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame);
x++;
}
// If it times out something is wrong
if (x >= 300)
{
throw new Exception("The DialogueManager singleton is missing.");
}
instance = Engine.GetSingleton("DialogueManager");
return instance;
}
public static Resource CreateResourceFromText(string text)
{
return (Resource)Instance.Call("create_resource_from_text", text);
}
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? 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<Variant>());
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<Variant>? extraGameStates = null)
{
return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
{
Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
await Instance.ToSignal(Instance, "bridge_mutated");
}
public bool ThingHasMethod(GodotObject thing, string method, Array<Variant> 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)
{
return true;
}
}
return false;
}
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> 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)
{
info = methodInfo;
}
}
if (info == null) return;
#nullable disable #nullable disable
// Convert the method args to something reflection can handle // Convert the method args to something reflection can handle
ParameterInfo[] argTypes = info.GetParameters(); ParameterInfo[] argTypes = info.GetParameters();
object[] _args = new object[argTypes.Length]; object[] _args = new object[argTypes.Length];
for (int i = 0; i < argTypes.Length; i++) for (int i = 0; i < argTypes.Length; i++)
{ {
// check if args is assignable from derived type // check if args is assignable from derived type
if (i < args.Count && args[i].Obj != null) if (i < args.Count && args[i].Obj != null)
{ {
if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType())) if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType()))
{ {
_args[i] = args[i].Obj; _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);
// 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);
}
catch (Exception err)
{
EmitSignal(SignalName.Resolved);
}
}
else
{
EmitSignal(SignalName.Resolved, (Variant)result);
}
} }
// 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);
// invoke method and handle the result based on return type
object result = info.Invoke(thing, _args);
if (result is Task taskResult)
{
// await Tasks and handle result if it is a Task<T>
await taskResult;
var taskType = taskResult.GetType();
if (taskType.IsGenericType && taskType.GetGenericTypeDefinition() == typeof(Task<>))
{
var resultProperty = taskType.GetProperty("Result");
var taskResultValue = resultProperty.GetValue(taskResult);
EmitSignal(SignalName.Resolved, (Variant)taskResultValue);
}
else
{
EmitSignal(SignalName.Resolved, null);
}
}
else
{
EmitSignal(SignalName.Resolved, (Variant)result);
}
}
#nullable enable #nullable enable
} }
public partial class DialogueLine : RefCounted public partial class DialogueLine : RefCounted
{ {
private string id = ""; private string id = "";
public string Id public string Id
{ {
get => id; get => id;
set => id = value; set => id = value;
} }
private string type = "dialogue"; private string type = "dialogue";
public string Type public string Type
{ {
get => type; get => type;
set => type = value; set => type = value;
} }
private string next_id = ""; private string next_id = "";
public string NextId public string NextId
{ {
get => next_id; get => next_id;
set => next_id = value; set => next_id = value;
} }
private string character = ""; private string character = "";
public string Character public string Character
{ {
get => character; get => character;
set => character = value; set => character = value;
} }
private string text = ""; private string text = "";
public string Text public string Text
{ {
get => text; get => text;
set => text = value; set => text = value;
} }
private string translation_key = ""; private string translation_key = "";
public string TranslationKey public string TranslationKey
{ {
get => translation_key; get => translation_key;
set => translation_key = value; set => translation_key = value;
} }
private Array<DialogueResponse> responses = new Array<DialogueResponse>(); private Array<DialogueResponse> responses = new Array<DialogueResponse>();
public Array<DialogueResponse> Responses public Array<DialogueResponse> Responses
{ {
get => responses; get => responses;
} }
private string? time = null; private string? time = null;
public string? Time public string? Time
{ {
get => time; get => time;
} }
private Dictionary pauses = new Dictionary(); private Dictionary pauses = new Dictionary();
public Dictionary Pauses public Dictionary Pauses
{ {
get => pauses; get => pauses;
} }
private Dictionary speeds = new Dictionary(); private Dictionary speeds = new Dictionary();
public Dictionary Speeds public Dictionary Speeds
{ {
get => speeds; get => speeds;
} }
private Array<Godot.Collections.Array> inline_mutations = new Array<Godot.Collections.Array>(); private Array<Godot.Collections.Array> inline_mutations = new Array<Godot.Collections.Array>();
public Array<Godot.Collections.Array> InlineMutations public Array<Godot.Collections.Array> InlineMutations
{ {
get => inline_mutations; get => inline_mutations;
} }
private Array<Variant> extra_game_states = new Array<Variant>(); private Array<DialogueLine> concurrent_lines = new Array<DialogueLine>();
public Array<DialogueLine> ConcurrentLines
private Array<string> tags = new Array<string>(); {
public Array<string> Tags get => concurrent_lines;
{ }
get => tags;
} private Array<Variant> extra_game_states = new Array<Variant>();
public Array<Variant> ExtraGameStates
public DialogueLine(RefCounted data) {
{ get => extra_game_states;
type = (string)data.Get("type"); }
next_id = (string)data.Get("next_id");
character = (string)data.Get("character"); private Array<string> tags = new Array<string>();
text = (string)data.Get("text"); public Array<string> Tags
translation_key = (string)data.Get("translation_key"); {
pauses = (Dictionary)data.Get("pauses"); get => tags;
speeds = (Dictionary)data.Get("speeds"); }
inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations");
time = (string)data.Get("time"); public DialogueLine(RefCounted data)
tags = (Array<string>)data.Get("tags"); {
type = (string)data.Get("type");
foreach (var response in (Array<RefCounted>)data.Get("responses")) next_id = (string)data.Get("next_id");
{ character = (string)data.Get("character");
responses.Add(new DialogueResponse(response)); text = (string)data.Get("text");
} translation_key = (string)data.Get("translation_key");
} pauses = (Dictionary)data.Get("pauses");
speeds = (Dictionary)data.Get("speeds");
inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations");
public string GetTagValue(string tagName) time = (string)data.Get("time");
{ tags = (Array<string>)data.Get("tags");
string wrapped = $"{tagName}=";
foreach (var tag in tags) foreach (var concurrent_line_data in (Array<RefCounted>)data.Get("concurrent_lines"))
{ {
if (tag.StartsWith(wrapped)) concurrent_lines.Add(new DialogueLine(concurrent_line_data));
{ }
return tag.Substring(wrapped.Length);
} foreach (var response in (Array<RefCounted>)data.Get("responses"))
} {
return ""; responses.Add(new DialogueResponse(response));
} }
}
public override string ToString()
{
switch (type) public string GetTagValue(string tagName)
{ {
case "dialogue": string wrapped = $"{tagName}=";
return $"<DialogueLine character=\"{character}\" text=\"{text}\">"; foreach (var tag in tags)
case "mutation": {
return "<DialogueLine mutation>"; if (tag.StartsWith(wrapped))
default: {
return ""; return tag.Substring(wrapped.Length);
} }
} }
} return "";
}
public partial class DialogueResponse : RefCounted public override string ToString()
{ {
private string next_id = ""; switch (type)
public string NextId {
{ case "dialogue":
get => next_id; return $"<DialogueLine character=\"{character}\" text=\"{text}\">";
set => next_id = value; case "mutation":
} return "<DialogueLine mutation>";
default:
private bool is_allowed = true; return "";
public bool IsAllowed }
{ }
get => is_allowed; }
set => is_allowed = value;
}
public partial class DialogueResponse : RefCounted
private string text = ""; {
public string Text private string next_id = "";
{ public string NextId
get => text; {
set => text = value; get => next_id;
} set => next_id = value;
}
private string translation_key = "";
public string TranslationKey private bool is_allowed = true;
{ public bool IsAllowed
get => translation_key; {
set => translation_key = value; get => is_allowed;
} set => is_allowed = value;
}
private Array<string> tags = new Array<string>();
public Array<string> Tags private string text = "";
{ public string Text
get => tags; {
} get => text;
set => text = value;
public DialogueResponse(RefCounted data) }
{
next_id = (string)data.Get("next_id"); private string translation_key = "";
is_allowed = (bool)data.Get("is_allowed"); public string TranslationKey
text = (string)data.Get("text"); {
translation_key = (string)data.Get("translation_key"); get => translation_key;
tags = (Array<string>)data.Get("tags"); set => translation_key = value;
} }
public string GetTagValue(string tagName) private Array<string> tags = new Array<string>();
{ public Array<string> Tags
string wrapped = $"{tagName}="; {
foreach (var tag in tags) get => tags;
{ }
if (tag.StartsWith(wrapped))
{ public DialogueResponse(RefCounted data)
return tag.Substring(wrapped.Length); {
} next_id = (string)data.Get("next_id");
} is_allowed = (bool)data.Get("is_allowed");
return ""; text = (string)data.Get("text");
} translation_key = (string)data.Get("translation_key");
tags = (Array<string>)data.Get("tags");
public override string ToString() }
{
return $"<DialogueResponse text=\"{text}\""; public string GetTagValue(string tagName)
} {
string wrapped = $"{tagName}=";
foreach (var tag in tags)
{
if (tag.StartsWith(wrapped))
{
return tag.Substring(wrapped.Length);
}
}
return "";
}
public override string ToString()
{
return $"<DialogueResponse text=\"{text}\"";
}
}
} }

View File

@@ -1 +1 @@
uid://dx7nmsb8wv8ck uid://c4c5lsrwy3opj

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://dsgpnyqg6cprg

View File

@@ -0,0 +1,157 @@
## A compiled line of dialogue.
class_name DMCompiledLine extends RefCounted
## The ID of the line
var id: String
## The translation key (or static line ID).
var translation_key: String = ""
## The type of line.
var type: String = ""
## The character name.
var character: String = ""
## Any interpolation expressions for the character name.
var character_replacements: Array[Dictionary] = []
## The text of the line.
var text: String = ""
## Any interpolation expressions for the text.
var text_replacements: Array[Dictionary] = []
## Any response siblings associated with this line.
var responses: PackedStringArray = []
## Any randomise or case siblings for this line.
var siblings: Array[Dictionary] = []
## Any lines said simultaneously.
var concurrent_lines: PackedStringArray = []
## Any tags on this line.
var tags: PackedStringArray = []
## The condition or mutation expression for this line.
var expression: Dictionary = {}
## 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.
var next_id_expression: Array[Dictionary] = []
## Whether this jump line should return after the jump target sequence has ended.
var is_snippet: bool = false
## The ID of the next sibling line.
var next_sibling_id: String = ""
## The ID after this line if it belongs to a block (eg. conditions).
var next_id_after: String = ""
## Any doc comments attached to this line.
var notes: String = ""
#region Hooks
func _init(initial_id: String, initial_type: String) -> void:
id = initial_id
type = initial_type
func _to_string() -> String:
var s: Array = [
"[%s]" % [type],
"%s:" % [character] if character != "" else null,
text if text != "" else null,
expression if expression.size() > 0 else null,
"[%s]" % [",".join(tags)] if tags.size() > 0 else null,
str(siblings) if siblings.size() > 0 else null,
str(responses) if responses.size() > 0 else null,
"=> END" if "end" in next_id else "=> %s" % [next_id],
"(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null,
"(==> %s)" % [next_id_after] if next_id_after != "" else null,
].filter(func(item): return item != null)
return " ".join(s)
#endregion
#region Helpers
## Express this line as a [Dictionary] that can be stored in a resource.
func to_data() -> Dictionary:
var d: Dictionary = {
id = id,
type = type,
next_id = next_id
}
if next_id_expression.size() > 0:
d.next_id_expression = next_id_expression
match type:
DMConstants.TYPE_CONDITION:
d.condition = expression
if not next_sibling_id.is_empty():
d.next_sibling_id = next_sibling_id
d.next_id_after = next_id_after
DMConstants.TYPE_WHILE:
d.condition = expression
d.next_id_after = next_id_after
DMConstants.TYPE_MATCH:
d.condition = expression
d.next_id_after = next_id_after
d.cases = siblings
DMConstants.TYPE_MUTATION:
d.mutation = expression
DMConstants.TYPE_GOTO:
d.is_snippet = is_snippet
d.next_id_after = next_id_after
if not siblings.is_empty():
d.siblings = siblings
DMConstants.TYPE_RANDOM:
d.siblings = siblings
DMConstants.TYPE_RESPONSE:
d.text = text
if not responses.is_empty():
d.responses = responses
if translation_key != text:
d.translation_key = translation_key
if not expression.is_empty():
d.condition = expression
if not character.is_empty():
d.character = character
if not character_replacements.is_empty():
d.character_replacements = character_replacements
if not text_replacements.is_empty():
d.text_replacements = text_replacements
if not tags.is_empty():
d.tags = tags
if not notes.is_empty():
d.notes = notes
DMConstants.TYPE_DIALOGUE:
d.text = text
if translation_key != text:
d.translation_key = translation_key
if not character.is_empty():
d.character = character
if not character_replacements.is_empty():
d.character_replacements = character_replacements
if not text_replacements.is_empty():
d.text_replacements = text_replacements
if not tags.is_empty():
d.tags = tags
if not notes.is_empty():
d.notes = notes
if not siblings.is_empty():
d.siblings = siblings
if not concurrent_lines.is_empty():
d.concurrent_lines = concurrent_lines
return d
#endregion

View File

@@ -0,0 +1 @@
uid://dg8j5hudp4210

View File

@@ -0,0 +1,51 @@
## A compiler of Dialogue Manager dialogue.
class_name DMCompiler extends RefCounted
## Compile a dialogue script.
static func compile_string(text: String, path: String) -> DMCompilerResult:
var compilation: DMCompilation = DMCompilation.new()
compilation.compile(text, path)
var result: DMCompilerResult = DMCompilerResult.new()
result.imported_paths = compilation.imported_paths
result.using_states = compilation.using_states
result.character_names = compilation.character_names
result.titles = compilation.titles
result.first_title = compilation.first_title
result.errors = compilation.errors
result.lines = compilation.data
result.raw_text = text
return result
## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants].
static func get_line_type(text: String) -> String:
var compilation: DMCompilation = DMCompilation.new()
return compilation.get_line_type(text)
## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text.
static func get_static_line_id(text: String) -> String:
var compilation: DMCompilation = DMCompilation.new()
return compilation.extract_static_line_id(text)
## Get the translatable part of a line.
static func extract_translatable_string(text: String) -> String:
var compilation: DMCompilation = DMCompilation.new()
var tree_line = DMTreeLine.new("")
tree_line.text = text
var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text))
compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null)
return line.text
## Get the known titles in a dialogue script.
static func get_titles_in_text(text: String, path: String) -> Dictionary:
var compilation: DMCompilation = DMCompilation.new()
compilation.build_line_tree(compilation.inject_imported_files(text, path))
return compilation.titles

View File

@@ -0,0 +1 @@
uid://chtfdmr0cqtp4

View File

@@ -0,0 +1,49 @@
## A collection of [RegEx] for use by the [DMCompiler].
class_name DMCompilerRegEx extends RefCounted
var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?<path>[^\"]+)\" as (?<prefix>[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)")
var USING_REGEX: RegEx = RegEx.create_from_string("^using (?<state>.*)$")
var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+")
var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$")
var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d")
var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?<expression>.*)\\:?")
var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?<expression>.*)\\]")
var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?<keyword>do|do!|set) (?<expression>.*)")
var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?<id>.*?)\\]")
var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?<weight>[\\d.]+)?( \\[if (?<condition>.+?)\\])? ")
var GOTO_REGEX: RegEx = RegEx.create_from_string("=><? (?<goto>.*)")
var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?<condition>.+?)\\](?<body>.*?)\\[\\/if\\]")
var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?<tags>.*?)\\]")
var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}")
var ALPHA_NUMERIC: RegEx = RegEx.create_from_string("[^a-zA-Z0-9\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+")
var TOKEN_DEFINITIONS: Dictionary = {
DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("),
DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["),
DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("),
DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"),
DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["),
DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"),
DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"),
DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"),
DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"),
DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"),
DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"),
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_DOT: RegEx.create_from_string("^\\."),
DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"),
DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),
DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"),
DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"),
DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"),
DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"),
DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)")
}

View File

@@ -0,0 +1 @@
uid://d3tvcrnicjibp

View File

@@ -0,0 +1,27 @@
## The result of using the [DMCompiler] to compile some dialogue.
class_name DMCompilerResult extends RefCounted
## Any paths that were imported into the compiled dialogue file.
var imported_paths: PackedStringArray = []
## Any "using" directives.
var using_states: PackedStringArray = []
## All titles in the file and the line they point to.
var titles: Dictionary = {}
## The first title in the file.
var first_title: String = ""
## All character names.
var character_names: PackedStringArray = []
## Any compilation errors.
var errors: Array[Dictionary] = []
## A map of all compiled lines.
var lines: Dictionary = {}
## The raw dialogue text.
var raw_text: String = ""

View File

@@ -0,0 +1 @@
uid://dmk74tknimqvg

View File

@@ -0,0 +1,497 @@
## A class for parsing a condition/mutation expression for use with the [DMCompiler].
class_name DMExpressionParser extends RefCounted
# Reference to the common [RegEx] that the parser needs.
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
## Break a string down into an expression.
func tokenise(text: String, line_type: String, index: int) -> Array:
var tokens: Array[Dictionary] = []
var limit: int = 0
while text.strip_edges() != "" and limit < 1000:
limit += 1
var found = _find_match(text)
if found.size() > 0:
tokens.append({
index = index,
type = found.type,
value = found.value
})
index += found.value.length()
text = found.remaining_text
elif text.begins_with(" "):
index += 1
text = text.substr(1)
else:
return _build_token_tree_error(DMConstants.ERR_INVALID_EXPRESSION, index)
return _build_token_tree(tokens, line_type, "")[0]
## Extract any expressions from some text
func extract_replacements(text: String, index: int) -> Array[Dictionary]:
var founds: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(text)
if founds == null or founds.size() == 0:
return []
var replacements: Array[Dictionary] = []
for found in founds:
var replacement: Dictionary = {}
var value_in_text: String = found.strings[0].substr(0, found.strings[0].length() - 2).substr(2)
# If there are closing curlie hard-up against the end of a {{...}} block then check for further
# curlies just outside of the block.
var text_suffix: String = text.substr(found.get_end(0))
var expression_suffix: String = ""
while text_suffix.begins_with("}"):
expression_suffix += "}"
text_suffix = text_suffix.substr(1)
value_in_text += expression_suffix
var expression: Array = tokenise(value_in_text, DMConstants.TYPE_DIALOGUE, index + found.get_start(1))
if expression.size() == 0:
replacement = {
index = index + found.get_start(1),
error = DMConstants.ERR_INCOMPLETE_EXPRESSION
}
elif expression[0].type == DMConstants.TYPE_ERROR:
replacement = {
index = expression[0].index,
error = expression[0].value
}
else:
replacement = {
value_in_text = "{{%s}}" % value_in_text,
expression = expression
}
replacements.append(replacement)
return replacements
#region Helpers
# 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 }]
# Convert a list of tokens into an abstract syntax tree.
func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Array:
var tree: Array[Dictionary] = []
var limit = 0
while tokens.size() > 0 and limit < 1000:
limit += 1
var token = tokens.pop_front()
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]
match token.type:
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]
tree.append({
type = DMConstants.TOKEN_FUNCTION,
# Consume the trailing "("
function = token.value.substr(0, token.value.length() - 1),
value = _tokens_to_list(sub_tree[0]),
i = token.index
})
tokens = sub_tree[1]
DMConstants.TOKEN_DICTIONARY_REFERENCE:
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]
var args = _tokens_to_list(sub_tree[0])
if args.size() != 1:
return [_build_token_tree_error(DMConstants.ERR_INVALID_INDEX, token.index), tokens]
tree.append({
type = DMConstants.TOKEN_DICTIONARY_REFERENCE,
# Consume the trailing "["
variable = token.value.substr(0, token.value.length() - 1),
value = args[0],
i = token.index
})
tokens = sub_tree[1]
DMConstants.TOKEN_BRACE_OPEN:
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]
var t = sub_tree[0]
for i in range(0, t.size() - 2):
# Convert Lua style dictionaries to string keys
if t[i].type == DMConstants.TOKEN_VARIABLE and t[i+1].type == DMConstants.TOKEN_ASSIGNMENT:
t[i].type = DMConstants.TOKEN_STRING
t[i+1].type = DMConstants.TOKEN_COLON
t[i+1].erase("value")
tree.append({
type = DMConstants.TOKEN_DICTIONARY,
value = _tokens_to_dictionary(sub_tree[0]),
i = token.index
})
tokens = sub_tree[1]
DMConstants.TOKEN_BRACKET_OPEN:
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]
var type = DMConstants.TOKEN_ARRAY
var value = _tokens_to_list(sub_tree[0])
# See if this is referencing a nested dictionary value
if tree.size() > 0:
var previous_token = tree[tree.size() - 1]
if previous_token.type in [DMConstants.TOKEN_DICTIONARY_REFERENCE, DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE]:
type = DMConstants.TOKEN_DICTIONARY_NESTED_REFERENCE
value = value[0]
tree.append({
type = type,
value = value,
i = token.index
})
tokens = sub_tree[1]
DMConstants.TOKEN_PARENS_OPEN:
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]
tree.append({
type = DMConstants.TOKEN_GROUP,
value = sub_tree[0],
i = token.index
})
tokens = sub_tree[1]
DMConstants.TOKEN_PARENS_CLOSE, \
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]
tree.append({
type = token.type,
i = token.index
})
return [tree, tokens]
DMConstants.TOKEN_NOT:
# Double nots negate each other
if tokens.size() > 0 and tokens.front().type == DMConstants.TOKEN_NOT:
tokens.pop_front()
else:
tree.append({
type = token.type,
i = token.index
})
DMConstants.TOKEN_COMMA, \
DMConstants.TOKEN_COLON, \
DMConstants.TOKEN_DOT:
tree.append({
type = token.type,
i = token.index
})
DMConstants.TOKEN_COMPARISON, \
DMConstants.TOKEN_ASSIGNMENT, \
DMConstants.TOKEN_OPERATOR, \
DMConstants.TOKEN_AND_OR, \
DMConstants.TOKEN_VARIABLE:
var value = token.value.strip_edges()
if value == "&&":
value = "and"
elif value == "||":
value = "or"
tree.append({
type = token.type,
value = value,
i = token.index
})
DMConstants.TOKEN_STRING:
if token.value.begins_with("&"):
tree.append({
type = token.type,
value = StringName(token.value.substr(2, token.value.length() - 3)),
i = token.index
})
else:
tree.append({
type = token.type,
value = token.value.substr(1, token.value.length() - 2),
i = token.index
})
DMConstants.TOKEN_CONDITION:
return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token]
DMConstants.TOKEN_BOOL:
tree.append({
type = token.type,
value = token.value.to_lower() == "true",
i = token.index
})
DMConstants.TOKEN_NUMBER:
var value = token.value.to_float() if "." in token.value else token.value.to_int()
# If previous token is a number and this one is a negative number then
# inject a minus operator token in between them.
if tree.size() > 0 and token.value.begins_with("-") and tree[tree.size() - 1].type == DMConstants.TOKEN_NUMBER:
tree.append(({
type = DMConstants.TOKEN_OPERATOR,
value = "-",
i = token.index
}))
tree.append({
type = token.type,
value = -1 * value,
i = token.index
})
else:
tree.append({
type = token.type,
value = value,
i = token.index
})
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]
return [tree, tokens]
# Check the next token to see if it is valid to follow this one.
func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_type: String, expected_close_token: String) -> Error:
var next_token: Dictionary = { type = null }
if next_tokens.size() > 0:
next_token = next_tokens.front()
# Guard for assigning in a condition. If the assignment token isn't inside a Lua dictionary
# then it's an unexpected assignment in a condition line.
if token.type == DMConstants.TOKEN_ASSIGNMENT and line_type == DMConstants.TYPE_CONDITION and not next_tokens.any(func(t): return t.type == expected_close_token):
return DMConstants.ERR_UNEXPECTED_ASSIGNMENT
# Special case for a negative number after this one
if token.type == DMConstants.TOKEN_NUMBER and next_token.type == DMConstants.TOKEN_NUMBER and next_token.value.begins_with("-"):
return OK
var expected_token_types = []
var unexpected_token_types = []
match token.type:
DMConstants.TOKEN_FUNCTION, \
DMConstants.TOKEN_PARENS_OPEN:
unexpected_token_types = [
null,
DMConstants.TOKEN_COMMA,
DMConstants.TOKEN_COLON,
DMConstants.TOKEN_COMPARISON,
DMConstants.TOKEN_ASSIGNMENT,
DMConstants.TOKEN_OPERATOR,
DMConstants.TOKEN_AND_OR,
DMConstants.TOKEN_DOT
]
DMConstants.TOKEN_BRACKET_CLOSE:
unexpected_token_types = [
DMConstants.TOKEN_NOT,
DMConstants.TOKEN_BOOL,
DMConstants.TOKEN_STRING,
DMConstants.TOKEN_NUMBER,
DMConstants.TOKEN_VARIABLE
]
DMConstants.TOKEN_BRACE_OPEN:
expected_token_types = [
DMConstants.TOKEN_STRING,
DMConstants.TOKEN_VARIABLE,
DMConstants.TOKEN_NUMBER,
DMConstants.TOKEN_BRACE_CLOSE
]
DMConstants.TOKEN_PARENS_CLOSE, \
DMConstants.TOKEN_BRACE_CLOSE:
unexpected_token_types = [
DMConstants.TOKEN_NOT,
DMConstants.TOKEN_ASSIGNMENT,
DMConstants.TOKEN_BOOL,
DMConstants.TOKEN_STRING,
DMConstants.TOKEN_NUMBER,
DMConstants.TOKEN_VARIABLE
]
DMConstants.TOKEN_COMPARISON, \
DMConstants.TOKEN_OPERATOR, \
DMConstants.TOKEN_COMMA, \
DMConstants.TOKEN_DOT, \
DMConstants.TOKEN_NOT, \
DMConstants.TOKEN_AND_OR, \
DMConstants.TOKEN_DICTIONARY_REFERENCE:
unexpected_token_types = [
null,
DMConstants.TOKEN_COMMA,
DMConstants.TOKEN_COLON,
DMConstants.TOKEN_COMPARISON,
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,
DMConstants.TOKEN_COLON,
DMConstants.TOKEN_COMPARISON,
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_BOOL, \
DMConstants.TOKEN_STRING, \
DMConstants.TOKEN_NUMBER:
unexpected_token_types = [
DMConstants.TOKEN_NOT,
DMConstants.TOKEN_ASSIGNMENT,
DMConstants.TOKEN_BOOL,
DMConstants.TOKEN_STRING,
DMConstants.TOKEN_NUMBER,
DMConstants.TOKEN_VARIABLE,
DMConstants.TOKEN_FUNCTION,
DMConstants.TOKEN_PARENS_OPEN,
DMConstants.TOKEN_BRACE_OPEN,
DMConstants.TOKEN_BRACKET_OPEN
]
DMConstants.TOKEN_VARIABLE:
unexpected_token_types = [
DMConstants.TOKEN_NOT,
DMConstants.TOKEN_BOOL,
DMConstants.TOKEN_STRING,
DMConstants.TOKEN_NUMBER,
DMConstants.TOKEN_VARIABLE,
DMConstants.TOKEN_FUNCTION,
DMConstants.TOKEN_PARENS_OPEN,
DMConstants.TOKEN_BRACE_OPEN,
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):
match next_token.type:
null:
return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION
DMConstants.TOKEN_FUNCTION:
return DMConstants.ERR_UNEXPECTED_FUNCTION
DMConstants.TOKEN_PARENS_OPEN, \
DMConstants.TOKEN_PARENS_CLOSE:
return DMConstants.ERR_UNEXPECTED_BRACKET
DMConstants.TOKEN_COMPARISON, \
DMConstants.TOKEN_ASSIGNMENT, \
DMConstants.TOKEN_OPERATOR, \
DMConstants.TOKEN_NOT, \
DMConstants.TOKEN_AND_OR:
return DMConstants.ERR_UNEXPECTED_OPERATOR
DMConstants.TOKEN_COMMA:
return DMConstants.ERR_UNEXPECTED_COMMA
DMConstants.TOKEN_COLON:
return DMConstants.ERR_UNEXPECTED_COLON
DMConstants.TOKEN_DOT:
return DMConstants.ERR_UNEXPECTED_DOT
DMConstants.TOKEN_BOOL:
return DMConstants.ERR_UNEXPECTED_BOOLEAN
DMConstants.TOKEN_STRING:
return DMConstants.ERR_UNEXPECTED_STRING
DMConstants.TOKEN_NUMBER:
return DMConstants.ERR_UNEXPECTED_NUMBER
DMConstants.TOKEN_VARIABLE:
return DMConstants.ERR_UNEXPECTED_VARIABLE
return DMConstants.ERR_INVALID_EXPRESSION
return OK
# Convert a series of comma separated tokens to an [Array].
func _tokens_to_list(tokens: Array[Dictionary]) -> Array[Array]:
var list: Array[Array] = []
var current_item: Array[Dictionary] = []
for token in tokens:
if token.type == DMConstants.TOKEN_COMMA:
list.append(current_item)
current_item = []
else:
current_item.append(token)
if current_item.size() > 0:
list.append(current_item)
return list
# Convert a series of key/value tokens into a [Dictionary]
func _tokens_to_dictionary(tokens: Array[Dictionary]) -> Dictionary:
var dictionary = {}
for i in range(0, tokens.size()):
if tokens[i].type == DMConstants.TOKEN_COLON:
if tokens.size() == i + 2:
dictionary[tokens[i - 1]] = tokens[i + 1]
else:
dictionary[tokens[i - 1]] = { type = DMConstants.TOKEN_GROUP, value = tokens.slice(i + 1), i = tokens[0].i }
return dictionary
# Work out what the next token is from a string.
func _find_match(input: String) -> Dictionary:
for key in regex.TOKEN_DEFINITIONS.keys():
var regex = regex.TOKEN_DEFINITIONS.get(key)
var found = regex.search(input)
if found:
return {
type = key,
remaining_text = input.substr(found.strings[0].length()),
value = found.strings[0]
}
return {}
#endregion

View File

@@ -0,0 +1 @@
uid://dbi4hbar8ubwu

View File

@@ -0,0 +1,68 @@
## Data associated with a dialogue jump/goto line.
class_name DMResolvedGotoData extends RefCounted
## The title that was specified
var title: String = ""
## The target line's ID
var next_id: String = ""
## An expression to determine the target line at runtime.
var expression: Array[Dictionary] = []
## The given line text with the jump syntax removed.
var text_without_goto: String = ""
## Whether this is a jump-and-return style jump.
var is_snippet: bool = false
## A parse error if there was one.
var error: int
## The index in the string where
var index: int = 0
# An instance of the compiler [RegEx] list.
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
func _init(text: String, titles: Dictionary) -> void:
if not "=> " in text and not "=>< " in text: return
if "=> " in text:
text_without_goto = text.substr(0, text.find("=> ")).strip_edges()
elif "=>< " in text:
is_snippet = true
text_without_goto = text.substr(0, text.find("=>< ")).strip_edges()
var found: RegExMatch = regex.GOTO_REGEX.search(text)
if found == null:
return
title = found.strings[found.names.goto].strip_edges()
index = found.get_start(0)
if title == "":
error = DMConstants.ERR_UNKNOWN_TITLE
return
# "=> END!" means end the conversation, ignoring any "=><" chains.
if title == "END!":
next_id = DMConstants.ID_END_CONVERSATION
# "=> END" means end the current title (and go back to the previous one if there is one
# in the stack)
elif title == "END":
next_id = DMConstants.ID_END
elif titles.has(title):
next_id = titles.get(title)
elif title.begins_with("{{"):
var expression_parser: DMExpressionParser = DMExpressionParser.new()
var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0)
if title_expression[0].has("error"):
error = title_expression[0].error
else:
expression = title_expression[0].expression
else:
next_id = title
error = DMConstants.ERR_UNKNOWN_TITLE
func _to_string() -> String:
return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id]

View File

@@ -0,0 +1 @@
uid://llhl5pt47eoq

View File

@@ -0,0 +1,167 @@
## Any data associated with inline dialogue BBCodes.
class_name DMResolvedLineData extends RefCounted
## The line's text
var text: String = ""
## A map of pauses against where they are found in the text.
var pauses: Dictionary = {}
## A map of speed changes against where they are found in the text.
var speeds: Dictionary = {}
## A list of any mutations to run and where they are found in the text.
var mutations: Array[Array] = []
## A duration reference for the line. Represented as "auto" or a stringified number.
var time: String = ""
func _init(line: String) -> void:
text = line
pauses = {}
speeds = {}
mutations = []
time = ""
var bbcodes: Array = []
# Remove any escaped brackets (ie. "\[")
var escaped_open_brackets: PackedInt32Array = []
var escaped_close_brackets: PackedInt32Array = []
for i in range(0, text.length() - 1):
if text.substr(i, 2) == "\\[":
text = text.substr(0, i) + "!" + text.substr(i + 2)
escaped_open_brackets.append(i)
elif text.substr(i, 2) == "\\]":
text = text.substr(0, i) + "!" + text.substr(i + 2)
escaped_close_brackets.append(i)
# Extract all of the BB codes so that we know the actual text (we could do this easier with
# a RichTextLabel but then we'd need to await idle_frame which is annoying)
var bbcode_positions = find_bbcode_positions_in_string(text)
var accumulaive_length_offset = 0
for position in bbcode_positions:
# Ignore our own markers
if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]:
continue
bbcodes.append({
bbcode = position.bbcode,
start = position.start,
offset_start = position.start - accumulaive_length_offset
})
accumulaive_length_offset += position.bbcode.length()
for bb in bbcodes:
text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length())
# Now find any dialogue markers
var next_bbcode_position = find_bbcode_positions_in_string(text, false)
var limit = 0
while next_bbcode_position.size() > 0 and limit < 1000:
limit += 1
var bbcode = next_bbcode_position[0]
var index = bbcode.start
var code = bbcode.code
var raw_args = bbcode.raw_args
var args = {}
if code in ["do", "do!", "set"]:
var compilation: DMCompilation = DMCompilation.new()
args["value"] = compilation.extract_mutation("%s %s" % [code, raw_args])
else:
# Could be something like:
# "=1.0"
# " rate=20 level=10"
if raw_args and raw_args[0] == "=":
raw_args = "value" + raw_args
for pair in raw_args.strip_edges().split(" "):
if "=" in pair:
var bits = pair.split("=")
args[bits[0]] = bits[1]
match code:
"wait":
if pauses.has(index):
pauses[index] += args.get("value").to_float()
else:
pauses[index] = args.get("value").to_float()
"speed":
speeds[index] = args.get("value").to_float()
"/speed":
speeds[index] = 1.0
"do", "do!", "set":
mutations.append([index, args.get("value")])
"next":
time = args.get("value") if args.has("value") else "0"
# Find any BB codes that are after this index and remove the length from their start
var length = bbcode.bbcode.length()
for bb in bbcodes:
if bb.offset_start > bbcode.start:
bb.offset_start -= length
bb.start -= length
# Find any escaped brackets after this that need moving
for i in range(0, escaped_open_brackets.size()):
if escaped_open_brackets[i] > bbcode.start:
escaped_open_brackets[i] -= length
for i in range(0, escaped_close_brackets.size()):
if escaped_close_brackets[i] > bbcode.start:
escaped_close_brackets[i] -= length
text = text.substr(0, index) + text.substr(index + length)
next_bbcode_position = find_bbcode_positions_in_string(text, false)
# Put the BB Codes back in
for bb in bbcodes:
text = text.insert(bb.start, bb.bbcode)
# Put the escaped brackets back in
for index in escaped_open_brackets:
text = text.left(index) + "[" + text.right(text.length() - index - 1)
for index in escaped_close_brackets:
text = text.left(index) + "]" + text.right(text.length() - index - 1)
func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]:
if not "[" in string: return []
var positions: Array[Dictionary] = []
var open_brace_count: int = 0
var start: int = 0
var bbcode: String = ""
var code: String = ""
var is_finished_code: bool = false
for i in range(0, string.length()):
if string[i] == "[":
if open_brace_count == 0:
start = i
bbcode = ""
code = ""
is_finished_code = false
open_brace_count += 1
else:
if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"):
code += string[i]
else:
is_finished_code = true
if open_brace_count > 0:
bbcode += string[i]
if string[i] == "]":
open_brace_count -= 1
if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]):
positions.append({
bbcode = bbcode,
code = code,
start = start,
end = i,
raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges()
})
if not find_all:
return positions
return positions

View File

@@ -0,0 +1 @@
uid://0k6q8kukq0qa

View File

@@ -0,0 +1,26 @@
## Tag data associated with a line of dialogue.
class_name DMResolvedTagData extends RefCounted
## The list of tags.
var tags: PackedStringArray = []
## The line with any tag syntax removed.
var text_without_tags: String = ""
# An instance of the compiler [RegEx].
var regex: DMCompilerRegEx = DMCompilerRegEx.new()
func _init(text: String) -> void:
var resolved_tags: PackedStringArray = []
var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text)
for tag_match in tag_matches:
text = text.replace(tag_match.get_string(), "")
var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",")
for tag in tags:
tag = tag.replace("#", "")
if not tag in resolved_tags:
resolved_tags.append(tag)
tags = resolved_tags
text_without_tags = text

View File

@@ -0,0 +1 @@
uid://cqai3ikuilqfq

View File

@@ -0,0 +1,44 @@
## An intermediate representation of a dialogue line before it gets compiled.
class_name DMTreeLine extends RefCounted
## The line number where this dialogue was found (after imported files have had their content imported).
var line_number: int = 0
## The parent [DMTreeLine] of this line.
## This is stored as a Weak Reference so that this RefCounted can elegantly free itself.
## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive.
var parent: WeakRef
## The ID of this line.
var id: String
## The type of this line (as a [String] defined in [DMConstants].
var type: String = ""
## Is this line part of a randomised group?
var is_random: bool = false
## The indent count for this line.
var indent: int = 0
## The text of this line.
var text: String = ""
## The child [DMTreeLine]s of this line.
var children: Array[DMTreeLine] = []
## Any doc comments attached to this line.
var notes: String = ""
func _init(initial_id: String) -> void:
id = initial_id
func _to_string() -> String:
var tabs = []
tabs.resize(indent)
tabs.fill("\t")
tabs = "".join(tabs)
return tabs.join([tabs + "{\n",
"\tid: %s\n" % [id],
"\ttype: %s\n" % [type],
"\tis_random: %s\n" % ["true" if is_random else "false"],
"\ttext: %s\n" % [text],
"\tnotes: %s\n" % [notes],
"\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n",
"}"])

View File

@@ -0,0 +1 @@
uid://dsu4i84dpif14

View File

@@ -1,5 +1,5 @@
@tool @tool
extends CodeEdit class_name DMCodeEdit extends CodeEdit
signal active_title_change(title: String) signal active_title_change(title: String)
@@ -7,10 +7,6 @@ signal error_clicked(line_number: int)
signal external_file_requested(path: String, title: String) signal external_file_requested(path: String, title: String)
const DialogueManagerParser = preload("./parser.gd")
const DialogueSyntaxHighlighter = preload("./code_edit_syntax_highlighter.gd")
# A link back to the owner `MainView` # A link back to the owner `MainView`
var main_view var main_view
@@ -19,7 +15,7 @@ var theme_overrides: Dictionary:
set(value): set(value):
theme_overrides = value theme_overrides = value
syntax_highlighter = DialogueSyntaxHighlighter.new() syntax_highlighter = DMSyntaxHighlighter.new()
# General UI # General UI
add_theme_color_override("font_color", theme_overrides.text_color) add_theme_color_override("font_color", theme_overrides.text_color)
@@ -67,7 +63,7 @@ func _ready() -> void:
if not has_comment_delimiter("#"): if not has_comment_delimiter("#"):
add_comment_delimiter("#", "", true) add_comment_delimiter("#", "", true)
syntax_highlighter = DialogueSyntaxHighlighter.new() syntax_highlighter = DMSyntaxHighlighter.new()
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
@@ -111,26 +107,34 @@ func _can_drop_data(at_position: Vector2, data) -> bool:
if typeof(data) != TYPE_DICTIONARY: return false if typeof(data) != TYPE_DICTIONARY: return false
if data.type != "files": return false if data.type != "files": return false
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") var files: PackedStringArray = Array(data.files)
return files.size() > 0 return files.size() > 0
func _drop_data(at_position: Vector2, data) -> void: func _drop_data(at_position: Vector2, data) -> void:
var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+") var replace_regex: RegEx = RegEx.create_from_string("[^a-zA-Z_0-9]+")
var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") var files: PackedStringArray = Array(data.files)
for file in files: for file in files:
# Don't import the file into itself # Don't import the file into itself
if file == main_view.current_file_path: continue if file == main_view.current_file_path: continue
var path = file.replace("res://", "").replace(".dialogue", "") if file.get_extension() == "dialogue":
# Find the first non-import line in the file to add our import var path = file.replace("res://", "").replace(".dialogue", "")
var lines = text.split("\n") # Find the first non-import line in the file to add our import
for i in range(0, lines.size()): var lines = text.split("\n")
if not lines[i].begins_with("import "): for i in range(0, lines.size()):
insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)]) if not lines[i].begins_with("import "):
set_caret_line(i) insert_line_at(i, "import \"%s\" as %s\n" % [file, replace_regex.sub(path, "_", true)])
break set_caret_line(i)
break
else:
var cursor: Vector2 = get_line_column_at_pos(at_position)
if cursor.x > -1 and cursor.y > -1:
set_cursor(cursor)
remove_secondary_carets()
insert_text("\"%s\"" % file, cursor.y, cursor.x)
grab_focus()
func _request_code_completion(force: bool) -> void: func _request_code_completion(force: bool) -> void:
@@ -151,17 +155,18 @@ func _request_code_completion(force: bool) -> void:
add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons")) add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
# Get all titles, including those in imports # Get all titles, including those in imports
var parser: DialogueManagerParser = DialogueManagerParser.new() for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path):
parser.prepare(text, main_view.current_file_path, false) # Ignore any imported titles that aren't resolved to human readable.
for title in parser.titles: if title.to_int() > 0:
if "/" in title: continue
elif "/" in title:
var bits = title.split("/") var bits = title.split("/")
if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]): if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons")) 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): 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")) 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) update_code_completion_options(true)
parser.free()
return return
var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "") var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "")
@@ -205,8 +210,8 @@ func get_cursor() -> Vector2:
# Set the caret from a Vector2 # Set the caret from a Vector2
func set_cursor(from_cursor: Vector2) -> void: func set_cursor(from_cursor: Vector2) -> void:
set_caret_line(from_cursor.y) set_caret_line(from_cursor.y, false)
set_caret_column(from_cursor.x) set_caret_column(from_cursor.x, false)
# Check if a prompt is the start of a string without actually being that string # Check if a prompt is the start of a string without actually being that string
@@ -219,8 +224,9 @@ func get_titles() -> PackedStringArray:
var titles = PackedStringArray([]) var titles = PackedStringArray([])
var lines = text.split("\n") var lines = text.split("\n")
for line in lines: for line in lines:
if line.begins_with("~ "): if line.strip_edges().begins_with("~ "):
titles.append(line.substr(2).strip_edges()) titles.append(line.strip_edges().substr(2))
return titles return titles
@@ -259,6 +265,11 @@ func get_character_names(beginning_with: String) -> PackedStringArray:
# Mark a line as an error or not # Mark a line as an error or not
func mark_line_as_error(line_number: int, is_error: bool) -> void: func mark_line_as_error(line_number: int, is_error: bool) -> void:
# Lines display counting from 1 but are actually indexed from 0
line_number -= 1
if line_number < 0: return
if is_error: if is_error:
set_line_background_color(line_number, theme_overrides.error_line_color) set_line_background_color(line_number, theme_overrides.error_line_color)
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons")) set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))
@@ -372,6 +383,7 @@ func delete_current_line() -> void:
func move_line(offset: int) -> void: func move_line(offset: int) -> void:
offset = clamp(offset, -1, 1) offset = clamp(offset, -1, 1)
var starting_scroll := scroll_vertical
var cursor = get_cursor() var cursor = get_cursor()
var reselect: bool = false var reselect: bool = false
var from: int = cursor.y var from: int = cursor.y
@@ -395,12 +407,14 @@ func move_line(offset: int) -> void:
text = "\n".join(lines) text = "\n".join(lines)
cursor.y += offset cursor.y += offset
set_cursor(cursor)
from += offset from += offset
to += offset to += offset
if reselect: if reselect:
select(from, 0, to, get_line_width(to)) select(from, 0, to, get_line_width(to))
set_cursor(cursor)
text_changed.emit() text_changed.emit()
scroll_vertical = starting_scroll + offset
### Signals ### Signals

View File

@@ -1 +1 @@
uid://1ymv6jff0eay uid://djeybvlb332mp

View File

@@ -1,7 +1,7 @@
[gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"] [gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"] [ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"] [ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"] [sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"]
script = ExtResource("1_58cfo") script = ExtResource("1_58cfo")

View File

@@ -1,55 +1,18 @@
@tool @tool
extends SyntaxHighlighter class_name DMSyntaxHighlighter extends SyntaxHighlighter
const DialogueManagerParser = preload("./parser.gd") var regex: DMCompilerRegEx = DMCompilerRegEx.new()
var compilation: DMCompilation = DMCompilation.new()
var expression_parser = DMExpressionParser.new()
enum ExpressionType {DO, SET, IF}
var dialogue_manager_parser: DialogueManagerParser = DialogueManagerParser.new()
var regex_titles: RegEx = RegEx.create_from_string("^\\s*(?<title>~\\s+[^\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\[\\]\\;\\:\\\"\\'\\,\\.\\<\\>\\?\\/\\s]+)")
var regex_comments: RegEx = RegEx.create_from_string("(?:(?>\"(?:\\\\\"|[^\"\\n])*\")[^\"\\n]*?\\s*(?<comment>#[^\\n]*)$|^[^\"#\\n]*?\\s*(?<comment2>#[^\\n]*))")
var regex_mutation: RegEx = RegEx.create_from_string("^\\s*(do|do!|set) (?<mutation>.*)")
var regex_condition: RegEx = RegEx.create_from_string("^\\s*(if|elif|while|else if) (?<condition>.*)")
var regex_wcondition: RegEx = RegEx.create_from_string("\\[if (?<condition>((?:[^\\[\\]]*)|(?:\\[(?1)\\]))*?)\\]")
var regex_wendif: RegEx = RegEx.create_from_string("\\[(\\/if|else)\\]")
var regex_rgroup: RegEx = RegEx.create_from_string("\\[\\[(?<options>.*?)\\]\\]")
var regex_endconditions: RegEx = RegEx.create_from_string("^\\s*(endif|else):?\\s*$")
var regex_tags: RegEx = RegEx.create_from_string("\\[(?<tag>(?!(?:ID:.*)|if)[a-zA-Z_][a-zA-Z0-9_]*!?)(?:[= ](?<val>[^\\[\\]]+))?\\](?:(?<text>(?!\\[\\/\\k<tag>\\]).*?)?(?<end>\\[\\/\\k<tag>\\]))?")
var regex_dialogue: RegEx = RegEx.create_from_string("^\\s*(?:(?<random>\\%[\\d.]* )|(?<response>- ))?(?:(?<character>[^#:]*): )?(?<dialogue>.*)$")
var regex_goto: RegEx = RegEx.create_from_string("=><? (?:(?<file>[^\\/]+)\\/)?(?<title>[^\\/]*)")
var regex_string: RegEx = RegEx.create_from_string("^&?(?<delimiter>[\"'])(?<content>(?:\\\\{2})*|(?:.*?[^\\\\](?:\\\\{2})*))\\1$")
var regex_escape: RegEx = RegEx.create_from_string("\\\\.")
var regex_number: RegEx = RegEx.create_from_string("^-?(?:(?:0x(?:[0-9A-Fa-f]{2})+)|(?:0b[01]+)|(?:\\d+(?:(?:[\\.]\\d*)?(?:e\\d+)?)|(?:_\\d+)+)?)$")
var regex_array: RegEx = RegEx.create_from_string("\\[((?>[^\\[\\]]+|(?R))*)\\]")
var regex_dict: RegEx = RegEx.create_from_string("^\\{((?>[^\\{\\}]+|(?R))*)\\}$")
var regex_kvdict: RegEx = RegEx.create_from_string("^\\s*(?<left>.*?)\\s*(?<colon>:|=)\\s*(?<right>[^\\/]+)$")
var regex_commas: RegEx = RegEx.create_from_string("([^,]+)(?:\\s*,\\s*)?")
var regex_assignment: RegEx = RegEx.create_from_string("^\\s*(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*(?<op>(?:\\/|\\*|-|\\+)?=)\\s*(?<val>.*)$")
var regex_varname: RegEx = RegEx.create_from_string("^\\s*(?!true|false|and|or|&&|\\|\\|not|in|null)(?<var>[a-zA-Z_][a-zA-Z_0-9]*)(?:(?<attr>(?:\\.[a-zA-Z_][a-zA-Z_0-9]*)+)|(?:\\[(?<key>[^\\]]+)\\]))?\\s*$")
var regex_keyword: RegEx = RegEx.create_from_string("^\\s*(true|false|null)\\s*$")
var regex_function: RegEx = RegEx.create_from_string("^\\s*([a-zA-Z_][a-zA-Z_0-9]*\\s*)\\(")
var regex_comparison: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s*(?<op>==|>=|<=|<|>|!=)\\s*(?<right>.*)$")
var regex_blogical: RegEx = RegEx.create_from_string("^(?<left>.*?)\\s+(?<op>and|or|in|&&|\\|\\|)\\s+(?<right>.*)$")
var regex_ulogical: RegEx = RegEx.create_from_string("^\\s*(?<op>not)\\s+(?<right>.*)$")
var regex_paren: RegEx = RegEx.create_from_string("\\((?<paren>((?:[^\\(\\)]*)|(?:\\((?1)\\)))*?)\\)")
var cache: Dictionary = {} var cache: Dictionary = {}
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
dialogue_manager_parser.free()
func _clear_highlighting_cache() -> void: func _clear_highlighting_cache() -> void:
cache = {} cache.clear()
## Returns the syntax coloring for a dialogue file line
func _get_line_syntax_highlighting(line: int) -> Dictionary: func _get_line_syntax_highlighting(line: int) -> Dictionary:
var colors: Dictionary = {} var colors: Dictionary = {}
var text_edit: TextEdit = get_text_edit() var text_edit: TextEdit = get_text_edit()
@@ -63,323 +26,194 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
if text in cache: if text in cache:
return cache[text] return cache[text]
# Comments have to be removed to make the remaining processing easier. var theme: Dictionary = text_edit.theme_overrides
# Count both end-of-line and single-line comments
# Comments are not allowed within dialogue lines or response lines, so we ask the parser what it thinks the current line is
if not (dialogue_manager_parser.is_dialogue_line(text) or dialogue_manager_parser.is_response_line(text)) or dialogue_manager_parser.is_line_empty(text) or dialogue_manager_parser.is_import_line(text):
var comment_matches: Array[RegExMatch] = regex_comments.search_all(text)
for comment_match in comment_matches:
for i in ["comment", "comment2"]:
if i in comment_match.names:
colors[comment_match.get_start(i)] = {"color": text_edit.theme_overrides.comments_color}
text = text.substr(0, comment_match.get_start(i))
# Dialogues var index: int = 0
var dialogue_matches: Array[RegExMatch] = regex_dialogue.search_all(text)
for dialogue_match in dialogue_matches:
if "random" in dialogue_match.names:
colors[dialogue_match.get_start("random")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("random")] = {"color": text_edit.theme_overrides.text_color}
if "response" in dialogue_match.names:
colors[dialogue_match.get_start("response")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("response")] = {"color": text_edit.theme_overrides.text_color}
if "character" in dialogue_match.names:
colors[dialogue_match.get_start("character")] = {"color": text_edit.theme_overrides.members_color}
colors[dialogue_match.get_end("character")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_dialogue_syntax_highlighting(dialogue_match.get_start("dialogue"), dialogue_match.get_string("dialogue")), true)
# Title lines match DMCompiler.get_line_type(text):
if dialogue_manager_parser.is_title_line(text): DMConstants.TYPE_USING:
var title_matches: Array[RegExMatch] = regex_titles.search_all(text) colors[index] = { color = theme.conditions_color }
for title_match in title_matches: colors[index + "using ".length()] = { color = theme.text_color }
colors[title_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color}
# Import lines DMConstants.TYPE_IMPORT:
var import_matches: Array[RegExMatch] = dialogue_manager_parser.IMPORT_REGEX.search_all(text) colors[index] = { color = theme.conditions_color }
for import_match in import_matches: var import: RegExMatch = regex.IMPORT_REGEX.search(text)
colors[import_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color} colors[index + import.get_start("path") - 1] = { color = theme.strings_color }
colors[import_match.get_start("path") - 1] = {"color": text_edit.theme_overrides.strings_color} colors[index + import.get_end("path") + 1] = { color = theme.conditions_color }
colors[import_match.get_end("path") + 1] = {"color": text_edit.theme_overrides.conditions_color} colors[index + import.get_start("prefix")] = { color = theme.text_color }
colors[import_match.get_start("prefix")] = {"color": text_edit.theme_overrides.members_color}
colors[import_match.get_end("prefix")] = {"color": text_edit.theme_overrides.conditions_color}
# Using clauses DMConstants.TYPE_COMMENT:
var using_matches: Array[RegExMatch] = dialogue_manager_parser.USING_REGEX.search_all(text) colors[index] = { color = theme.comments_color }
for using_match in using_matches:
colors[using_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[using_match.get_start("state") - 1] = {"color": text_edit.theme_overrides.text_color}
# Condition keywords and expressions DMConstants.TYPE_TITLE:
var condition_matches: Array[RegExMatch] = regex_condition.search_all(text) colors[index] = { color = theme.titles_color }
for condition_match in condition_matches:
colors[condition_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[condition_match.get_end(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_expression_syntax_highlighting(condition_match.get_start("condition"), ExpressionType.IF, condition_match.get_string("condition")), true)
# endif/else
var endcondition_matches: Array[RegExMatch] = regex_endconditions.search_all(text)
for endcondition_match in endcondition_matches:
colors[endcondition_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[endcondition_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Mutations DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN:
var mutation_matches: Array[RegExMatch] = regex_mutation.search_all(text) colors[0] = { color = theme.conditions_color }
for mutation_match in mutation_matches: index = text.find(" ")
colors[mutation_match.get_start(0)] = {"color": text_edit.theme_overrides.mutations_color} if index > -1:
colors.merge(_get_expression_syntax_highlighting(mutation_match.get_start("mutation"), ExpressionType.DO if mutation_match.strings[1] == "do" else ExpressionType.SET, mutation_match.get_string("mutation")), true) var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0)
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
colors[index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index)
DMConstants.TYPE_MUTATION:
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:
colors[index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index)
DMConstants.TYPE_GOTO:
if text.strip_edges().begins_with("%"):
colors[index] = { color = theme.symbols_color }
index = text.find(" ")
_highlight_goto(text, colors, index)
DMConstants.TYPE_RANDOM:
colors[index] = { color = theme.symbols_color }
DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE:
if text.strip_edges().begins_with("%"):
colors[index] = { color = theme.symbols_color }
index = text.find(" ", text.find("%"))
colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) }
var dialogue_text: String = text.substr(index, text.find("=>"))
# Highlight character name
var split_index: int = dialogue_text.replace("\\:", "??").find(":")
colors[index + split_index + 1] = { color = theme.text_color }
# Interpolation
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text)
for replacement: RegExMatch in replacements:
var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2)
var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start())
var expression_index: int = index + replacement.get_start()
colors[expression_index] = { color = theme.symbols_color }
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
colors[expression_index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index + 2)
colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color }
colors[expression_index + expression_text.length() + 4] = { color = theme.text_color }
# Tags (and inline mutations)
var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("")
var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true)
for bbcode: Dictionary in bbcodes:
var tag: String = bbcode.code
var code: String = bbcode.raw_args
if code.begins_with("["):
colors[index + bbcode.start] = { color = theme.symbols_color }
colors[index + bbcode.start + 2] = { color = theme.text_color }
var pipe_cursor: int = code.find("|")
while pipe_cursor > -1:
colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color }
colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color }
pipe_cursor = code.find("|", pipe_cursor + 1)
colors[index + bbcode.end - 1] = { color = theme.symbols_color }
colors[index + bbcode.end + 1] = { color = theme.text_color }
else:
colors[index + bbcode.start] = { color = theme.symbols_color }
if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"):
if tag.begins_with("if"):
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
else:
colors[index + bbcode.start + 1] = { color = theme.mutations_color }
var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length())
if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR:
colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index + 2)
# else and closing if have no expression
elif tag.begins_with("else") or tag.begins_with("/if"):
colors[index + bbcode.start + 1] = { color = theme.conditions_color }
colors[index + bbcode.end] = { color = theme.symbols_color }
colors[index + bbcode.end + 1] = { color = theme.text_color }
# Jumps
if "=> " in text or "=>< " in text:
_highlight_goto(text, colors, index)
# Order the dictionary keys to prevent CodeEdit from having issues # Order the dictionary keys to prevent CodeEdit from having issues
var new_colors: Dictionary = {} var ordered_colors: Dictionary = {}
var ordered_keys: Array = colors.keys() var ordered_keys: Array = colors.keys()
ordered_keys.sort() ordered_keys.sort()
for index in ordered_keys: for key_index: int in ordered_keys:
new_colors[index] = colors[index] ordered_colors[key_index] = colors[key_index]
cache[text] = new_colors cache[text] = ordered_colors
return new_colors return ordered_colors
## Return the syntax highlighting for a dialogue line func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int:
func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictionary: var theme: Dictionary = get_text_edit().theme_overrides
var text_edit: TextEdit = get_text_edit() var last_index: int = index
var colors: Dictionary = {} for token: Dictionary in tokens:
last_index = token.i
match token.type:
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR:
colors[index + token.i] = { color = theme.conditions_color }
# #tag style tags DMConstants.TOKEN_VARIABLE:
var hashtag_matches: Array[RegExMatch] = dialogue_manager_parser.TAGS_REGEX.search_all(text) if token.value in ["true", "false"]:
for hashtag_match in hashtag_matches: colors[index + token.i] = { color = theme.conditions_color }
colors[start_index + hashtag_match.get_start(0)] = { "color": text_edit.theme_overrides.comments_color } else:
colors[start_index + hashtag_match.get_end(0)] = { "color": text_edit.theme_overrides.text_color } colors[index + token.i] = { color = theme.members_color }
# bbcode-like global tags DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, DMConstants.TOKEN_COMMA, DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT:
var tag_matches: Array[RegExMatch] = regex_tags.search_all(text) colors[index + token.i] = { color = theme.symbols_color }
for tag_match in tag_matches:
colors[start_index + tag_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
if "val" in tag_match.names:
colors.merge(_get_literal_syntax_highlighting(start_index + tag_match.get_start("val"), tag_match.get_string("val")), true)
colors[start_index + tag_match.get_end("val")] = {"color": text_edit.theme_overrides.symbols_color}
# Show the text color straight in the editor for better ease-of-use
if tag_match.get_string("tag") == "color":
colors[start_index + tag_match.get_start("val")] = {"color": Color.from_string(tag_match.get_string("val"), text_edit.theme_overrides.text_color)}
if "text" in tag_match.names:
colors[start_index + tag_match.get_start("text")] = {"color": text_edit.theme_overrides.text_color}
# Text can still contain tags if several effects are applied ([center][b]Something[/b][/center], so recursing
colors.merge(_get_dialogue_syntax_highlighting(start_index + tag_match.get_start("text"), tag_match.get_string("text")), true)
colors[start_index + tag_match.get_end("text")] = {"color": text_edit.theme_overrides.symbols_color}
if "end" in tag_match.names:
colors[start_index + tag_match.get_start("end")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + tag_match.get_end("end")] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + tag_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# ID tag DMConstants.TOKEN_STRING:
var translation_matches: Array[RegExMatch] = dialogue_manager_parser.TRANSLATION_REGEX.search_all(text) colors[index + token.i] = { color = theme.strings_color }
for translation_match in translation_matches:
colors[start_index + translation_match.get_start(0)] = {"color": text_edit.theme_overrides.comments_color}
colors[start_index + translation_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Replacements DMConstants.TOKEN_FUNCTION:
var replacement_matches: Array[RegExMatch] = dialogue_manager_parser.REPLACEMENTS_REGEX.search_all(text) colors[index + token.i] = { color = theme.mutations_color }
for replacement_match in replacement_matches: colors[index + token.i + token.function.length()] = { color = theme.symbols_color }
colors[start_index + replacement_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} for parameter: Array in token.value:
colors[start_index + replacement_match.get_start(1)] = {"color": text_edit.theme_overrides.text_color} last_index = _highlight_expression(parameter, colors, index)
colors.merge(_get_literal_syntax_highlighting(start_index + replacement_match.get_start(1), replacement_match.strings[1]), true) DMConstants.TOKEN_PARENS_CLOSE:
colors[start_index + replacement_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color} colors[index + token.i] = { color = theme.symbols_color }
colors[start_index + replacement_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Jump at the end of a response DMConstants.TOKEN_DICTIONARY_REFERENCE:
var goto_matches: Array[RegExMatch] = regex_goto.search_all(text) colors[index + token.i] = { color = theme.members_color }
for goto_match in goto_matches: colors[index + token.i + token.variable.length()] = { color = theme.symbols_color }
colors[start_index + goto_match.get_start(0)] = {"color": text_edit.theme_overrides.jumps_color} last_index = _highlight_expression(token.value, colors, index)
if "file" in goto_match.names: DMConstants.TOKEN_ARRAY:
colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.jumps_color} colors[index + token.i] = { color = theme.symbols_color }
colors[start_index + goto_match.get_end("file")] = {"color": text_edit.theme_overrides.symbols_color} for item: Array in token.value:
colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.jumps_color} last_index = _highlight_expression(item, colors, index)
colors[start_index + goto_match.get_end("title")] = {"color": text_edit.theme_overrides.jumps_color} DMConstants.TOKEN_BRACKET_CLOSE:
colors[start_index + goto_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color} colors[index + token.i] = { color = theme.symbols_color }
# Wrapped condition DMConstants.TOKEN_DICTIONARY:
var wcondition_matches: Array[RegExMatch] = regex_wcondition.search_all(text) colors[index + token.i] = { color = theme.symbols_color }
for wcondition_match in wcondition_matches: last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index)
colors[start_index + wcondition_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color} DMConstants.TOKEN_BRACE_CLOSE:
colors[start_index + wcondition_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.conditions_color} colors[index + token.i] = { color = theme.symbols_color }
colors[start_index + wcondition_match.get_start(0) + 3] = {"color": text_edit.theme_overrides.text_color} last_index += 1
colors.merge(_get_literal_syntax_highlighting(start_index + wcondition_match.get_start("condition"), wcondition_match.get_string("condition")), true)
colors[start_index + wcondition_match.get_end("condition")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# [/if] tag for color matching with the opening tag
var wendif_matches: Array[RegExMatch] = regex_wendif.search_all(text)
for wendif_match in wendif_matches:
colors[start_index + wendif_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wendif_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# Random groups DMConstants.TOKEN_GROUP:
var rgroup_matches: Array[RegExMatch] = regex_rgroup.search_all(text) last_index = _highlight_expression(token.value, colors, index)
for rgroup_match in rgroup_matches:
colors[start_index + rgroup_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options")] = {"color": text_edit.theme_overrides.text_color}
var separator_matches: Array[RegExMatch] = RegEx.create_from_string("\\|").search_all(rgroup_match.get_string("options"))
for separator_match in separator_matches:
colors[start_index + rgroup_match.get_start("options") + separator_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options") + separator_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + rgroup_match.get_end("options")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
return colors return last_index
## Returns the syntax highlighting for an expression (mutation set/do, or condition) func _highlight_goto(text: String, colors: Dictionary, index: int) -> int:
func _get_expression_syntax_highlighting(start_index: int, type: ExpressionType, text: String) -> Dictionary: var theme: Dictionary = get_text_edit().theme_overrides
var text_edit: TextEdit = get_text_edit() var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {})
var colors: Dictionary = {} colors[goto_data.index] = { color = theme.jumps_color }
if "{{" in text:
index = text.find("{{", goto_data.index)
var last_index: int = 0
if goto_data.error:
colors[index + 2] = { color = theme.critical_color }
else:
last_index = _highlight_expression(goto_data.expression, colors, index)
index = text.find("}}", index + last_index)
colors[index] = { color = theme.jumps_color }
if type == ExpressionType.SET: return index
var assignment_matches: Array[RegExMatch] = regex_assignment.search_all(text)
for assignment_match in assignment_matches:
colors[start_index + assignment_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color}
if "attr" in assignment_match.names:
colors[start_index + assignment_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + assignment_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color}
if "key" in assignment_match.names:
# Braces are outside of the key, so coloring them symbols_color
colors[start_index + assignment_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("key"), assignment_match.get_string("key")), true)
colors[start_index + assignment_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + assignment_match.get_end("key") + 1] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + assignment_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + assignment_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + assignment_match.get_start("val"), assignment_match.get_string("val")), true)
else:
colors.merge(_get_literal_syntax_highlighting(start_index, text), true)
return colors
## Return the syntax highlighting for a literal
## For this purpose, "literal" refers to a regular code line that could be used to get a value out of:
## - function calls
## - real literals (bool, string, int, float, etc.)
## - logical operators (>, <, >=, or, and, not, etc.)
func _get_literal_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# Remove spaces at start/end of the literal
var text_length: int = text.length()
text = text.lstrip(" ")
start_index += text_length - text.length()
text = text.rstrip(" ")
# Parenthesis expression.
var paren_matches: Array[RegExMatch] = regex_paren.search_all(text)
for paren_match in paren_matches:
colors[start_index + paren_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + paren_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + paren_match.get_start("paren"), paren_match.get_string("paren")), true)
colors[start_index + paren_match.get_end(0) - 1] = {"color": text_edit.theme_overrides.symbols_color}
# Strings
var string_matches: Array[RegExMatch] = regex_string.search_all(text)
for string_match in string_matches:
colors[start_index + string_match.get_start(0)] = {"color": text_edit.theme_overrides.strings_color}
if "content" in string_match.names:
var escape_matches: Array[RegExMatch] = regex_escape.search_all(string_match.get_string("content"))
for escape_match in escape_matches:
colors[start_index + string_match.get_start("content") + escape_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + string_match.get_start("content") + escape_match.get_end(0)] = {"color": text_edit.theme_overrides.strings_color}
# Numbers
var number_matches: Array[RegExMatch] = regex_number.search_all(text)
for number_match in number_matches:
colors[start_index + number_match.get_start(0)] = {"color": text_edit.theme_overrides.numbers_color}
# Arrays
var array_matches: Array[RegExMatch] = regex_array.search_all(text)
for array_match in array_matches:
colors[start_index + array_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + array_match.get_start(1), array_match.strings[1]), true)
colors[start_index + array_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Dictionaries
var dict_matches: Array[RegExMatch] = regex_dict.search_all(text)
for dict_match in dict_matches:
colors[start_index + dict_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + dict_match.get_start(1), dict_match.strings[1]), true)
colors[start_index + dict_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
# Dictionary key: value pairs
var kvdict_matches: Array[RegExMatch] = regex_kvdict.search_all(text)
for kvdict_match in kvdict_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("left"), kvdict_match.get_string("left")), true)
colors[start_index + kvdict_match.get_start("colon")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + kvdict_match.get_end("colon")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + kvdict_match.get_start("right"), kvdict_match.get_string("right")), true)
# Booleans
var bool_matches: Array[RegExMatch] = regex_keyword.search_all(text)
for bool_match in bool_matches:
colors[start_index + bool_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
# Functions
var function_matches: Array[RegExMatch] = regex_function.search_all(text)
for function_match in function_matches:
var last_brace_index: int = text.rfind(")")
colors[start_index + function_match.get_start(1)] = {"color": text_edit.theme_overrides.mutations_color}
colors[start_index + function_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_list_syntax_highlighting(start_index + function_match.get_end(0), text.substr(function_match.get_end(0), last_brace_index - function_match.get_end(0))), true)
colors[start_index + last_brace_index] = {"color": text_edit.theme_overrides.symbols_color}
# Variables
var varname_matches: Array[RegExMatch] = regex_varname.search_all(text)
for varname_match in varname_matches:
colors[start_index + varname_match.get_start("var")] = {"color": text_edit.theme_overrides.text_color}
if "attr" in varname_match.names:
colors[start_index + varname_match.get_start("attr")] = {"color": text_edit.theme_overrides.members_color}
colors[start_index + varname_match.get_end("attr")] = {"color": text_edit.theme_overrides.text_color}
if "key" in varname_match.names:
# Braces are outside of the key, so coloring them symbols_color
colors[start_index + varname_match.get_start("key") - 1] = {"color": text_edit.theme_overrides.symbols_color}
colors.merge(_get_literal_syntax_highlighting(start_index + varname_match.get_start("key"), varname_match.get_string("key")), true)
colors[start_index + varname_match.get_end("key")] = {"color": text_edit.theme_overrides.symbols_color}
# Comparison operators
var comparison_matches: Array[RegExMatch] = regex_comparison.search_all(text)
for comparison_match in comparison_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("left"), comparison_match.get_string("left")), true)
colors[start_index + comparison_match.get_start("op")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + comparison_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
var right = comparison_match.get_string("right")
if right.ends_with(":"):
right = right.substr(0, right.length() - 1)
colors.merge(_get_literal_syntax_highlighting(start_index + comparison_match.get_start("right"), right), true)
colors[start_index + comparison_match.get_start("right") + right.length()] = { "color": text_edit.theme_overrides.symbols_color }
# Logical binary operators
var blogical_matches: Array[RegExMatch] = regex_blogical.search_all(text)
for blogical_match in blogical_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("left"), blogical_match.get_string("left")), true)
colors[start_index + blogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + blogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + blogical_match.get_start("right"), blogical_match.get_string("right")), true)
# Logical unary operators
var ulogical_matches: Array[RegExMatch] = regex_ulogical.search_all(text)
for ulogical_match in ulogical_matches:
colors[start_index + ulogical_match.get_start("op")] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + ulogical_match.get_end("op")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + ulogical_match.get_start("right"), ulogical_match.get_string("right")), true)
return colors
## Returns the syntax coloring for a list of literals separated by commas
func _get_list_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
# Comma-separated list of literals (for arrays and function arguments)
var element_matches: Array[RegExMatch] = regex_commas.search_all(text)
for element_match in element_matches:
colors.merge(_get_literal_syntax_highlighting(start_index + element_match.get_start(1), element_match.strings[1]), true)
return colors

View File

@@ -1 +1 @@
uid://c2pb8gpka0t0u uid://klpiq4tk3t7a

View File

@@ -34,7 +34,7 @@ func _ready() -> void:
func _on_download_button_pressed() -> void: func _on_download_button_pressed() -> void:
# Safeguard the actual dialogue manager repo from accidentally updating itself # Safeguard the actual dialogue manager repo from accidentally updating itself
if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"): if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"):
prints("You can't update the addon from within itself.") prints("You can't update the addon from within itself.")
failed.emit() failed.emit()
return return

View File

@@ -1 +1 @@
uid://dtgq7prk0yh50 uid://kpwo418lb2t2

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"] [gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"] [ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"]
[ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"] [ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"]
[node name="DownloadUpdatePanel" type="Control"] [node name="DownloadUpdatePanel" type="Control"]

View File

@@ -1 +1 @@
uid://du1qxltygjocp uid://nyypeje1a036

View File

@@ -63,7 +63,7 @@ func build_menu() -> void:
func _on_new_dialog_file_selected(path: String) -> void: func _on_new_dialog_file_selected(path: String) -> void:
editor_plugin.main_view.new_file(path) editor_plugin.main_view.new_file(path)
is_waiting_for_file = false is_waiting_for_file = false
if Engine.get_meta("DialogueCache").has_file(path): if Engine.get_meta("DMCache").has_file(path):
resource_changed.emit(load(path)) resource_changed.emit(load(path))
else: else:
var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource
@@ -81,7 +81,7 @@ func _on_file_dialog_canceled() -> void:
func _on_resource_button_pressed() -> void: func _on_resource_button_pressed() -> void:
if is_instance_valid(resource): if is_instance_valid(resource):
editor_plugin.get_editor_interface().call_deferred("edit_resource", resource) EditorInterface.call_deferred("edit_resource", resource)
else: else:
build_menu() build_menu()
menu.position = get_viewport().position + Vector2i( menu.position = get_viewport().position + Vector2i(
@@ -112,7 +112,7 @@ func _on_menu_id_pressed(id: int) -> void:
ITEM_QUICK_LOAD: ITEM_QUICK_LOAD:
quick_selected_file = "" quick_selected_file = ""
files_list.files = Engine.get_meta("DialogueCache").get_files() files_list.files = Engine.get_meta("DMCache").get_files()
if resource: if resource:
files_list.select_file(resource.resource_path) files_list.select_file(resource.resource_path)
quick_open_dialog.popup_centered() quick_open_dialog.popup_centered()
@@ -123,13 +123,13 @@ func _on_menu_id_pressed(id: int) -> void:
open_dialog.popup_centered() open_dialog.popup_centered()
ITEM_EDIT: ITEM_EDIT:
editor_plugin.get_editor_interface().call_deferred("edit_resource", resource) EditorInterface.call_deferred("edit_resource", resource)
ITEM_CLEAR: ITEM_CLEAR:
resource_changed.emit(null) resource_changed.emit(null)
ITEM_FILESYSTEM: ITEM_FILESYSTEM:
var file_system = editor_plugin.get_editor_interface().get_file_system_dock() var file_system = EditorInterface.get_file_system_dock()
file_system.navigate_to_path(resource.resource_path) file_system.navigate_to_path(resource.resource_path)

View File

@@ -1 +1 @@
uid://d1wvrhfmr8ry6 uid://dooe2pflnqtve

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"] [gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"] [ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"]
[ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"] [ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"]
[ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"] [ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"]

View File

@@ -1 +1 @@
uid://b8gxhsrredou6 uid://damhqta55t67c

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"] [gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"] [ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"]
[node name="ResourceButton" type="Button"] [node name="ResourceButton" type="Button"]
offset_right = 8.0 offset_right = 8.0

View File

@@ -59,7 +59,7 @@ func show_error() -> void:
show() show()
count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() }) count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() })
var error = errors[error_index] var error = errors[error_index]
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
if error.has("external_error"): if error.has("external_error"):
error_button.text += " " + DialogueConstants.get_error_message(error.external_error) error_button.text += " " + DialogueConstants.get_error_message(error.external_error)
@@ -72,7 +72,7 @@ func _on_errors_panel_theme_changed() -> void:
func _on_error_button_pressed() -> void: func _on_error_button_pressed() -> void:
emit_signal("error_pressed", errors[error_index].line_number, errors[error_index].column_number) error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number)
func _on_previous_button_pressed() -> void: func _on_previous_button_pressed() -> void:

View File

@@ -1 +1 @@
uid://bodru4vhssqjm uid://d2l8nlb6hhrfp

View File

@@ -1,8 +1,8 @@
[gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"] [gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"] [ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"]
[sub_resource type="Image" id="Image_d2tnf"] [sub_resource type="Image" id="Image_w0gko"]
data = { data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8", "format": "RGBA8",
@@ -12,7 +12,7 @@ data = {
} }
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"] [sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
image = SubResource("Image_d2tnf") image = SubResource("Image_w0gko")
[node name="ErrorsPanel" type="HBoxContainer"] [node name="ErrorsPanel" type="HBoxContainer"]
visible = false visible = false

View File

@@ -21,6 +21,7 @@ const MODIFIED_SUFFIX = "(*)"
var file_map: Dictionary = {} var file_map: Dictionary = {}
var current_file_path: String = "" var current_file_path: String = ""
var last_selected_file_path: String = ""
var files: PackedStringArray = []: var files: PackedStringArray = []:
set(next_files): set(next_files):
@@ -33,7 +34,7 @@ var files: PackedStringArray = []:
var unsaved_files: Array[String] = [] var unsaved_files: Array[String] = []
var filter: String: var filter: String = "":
set(next_filter): set(next_filter):
filter = next_filter filter = next_filter
apply_filter() apply_filter()
@@ -57,6 +58,7 @@ func select_file(file: String) -> void:
var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "") var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "")
if item_text == get_nice_file(file, item_text.count("/") + 1): if item_text == get_nice_file(file, item_text.count("/") + 1):
list.select(i) list.select(i)
last_selected_file_path = file
func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void: func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void:
@@ -112,6 +114,8 @@ func apply_filter() -> void:
func apply_theme() -> void: func apply_theme() -> void:
if is_instance_valid(filter_edit): if is_instance_valid(filter_edit):
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
if is_instance_valid(list):
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
### Signals ### Signals

View File

@@ -1 +1 @@
uid://cxxkkarhbbf07 uid://dqa4a4wwoo0aa

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"] [gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] [ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"] [ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"]
[node name="FilesList" type="VBoxContainer"] [node name="FilesList" type="VBoxContainer"]
@@ -9,6 +9,7 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
size_flags_vertical = 3
script = ExtResource("1_cytii") script = ExtResource("1_cytii")
icon = ExtResource("2_3ijx1") icon = ExtResource("2_3ijx1")

View File

@@ -114,7 +114,7 @@ func find_in_files() -> Dictionary:
var results: Dictionary = {} var results: Dictionary = {}
var q: String = input.text var q: String = input.text
var cache = Engine.get_meta("DialogueCache") var cache = Engine.get_meta("DMCache")
var file: FileAccess var file: FileAccess
for path in cache.get_files(): for path in cache.get_files():
var path_results: Array = [] var path_results: Array = []

View File

@@ -1 +1 @@
uid://xe50vmll2xq4 uid://q368fmxxa8sd

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"] [gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"] [ext_resource type="Script" uid="uid://q368fmxxa8sd" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"]
bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137) bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137)

View File

@@ -1,10 +0,0 @@
class_name DialogueManagerParseResult extends RefCounted
var imported_paths: PackedStringArray = []
var using_states: PackedStringArray = []
var titles: Dictionary = {}
var character_names: PackedStringArray = []
var first_title: String = ""
var lines: Dictionary = {}
var errors: Array[Dictionary] = []
var raw_text: String = ""

View File

@@ -1 +0,0 @@
uid://nkbwbj4jt5h5

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
uid://kmwjxv2fbqfk

View File

@@ -1,15 +0,0 @@
extends RefCounted
var text: String = ""
var pauses: Dictionary = {}
var speeds: Dictionary = {}
var mutations: Array[Array] = []
var time: String = ""
func _init(data: Dictionary) -> void:
text = data.text
pauses = data.pauses
speeds = data.speeds
mutations = data.mutations
time = data.time

View File

@@ -1,10 +0,0 @@
extends RefCounted
var tags: PackedStringArray = []
var line_without_tags: String = ""
func _init(data: Dictionary) -> void:
tags = data.tags
line_without_tags = data.line_without_tags

View File

@@ -1 +1 @@
uid://bs2b6vbxavpev uid://cijsmjkq21cdq

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"] [gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"] [ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"]
[node name="SearchAndReplace" type="VBoxContainer"] [node name="SearchAndReplace" type="VBoxContainer"]
visible = false visible = false

View File

@@ -48,6 +48,8 @@ func apply_filter() -> void:
func apply_theme() -> void: func apply_theme() -> void:
if is_instance_valid(filter_edit): if is_instance_valid(filter_edit):
filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") filter_edit.right_icon = get_theme_icon("Search", "EditorIcons")
if is_instance_valid(list):
list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel"))
### Signals ### Signals

View File

@@ -1 +1 @@
uid://5t2mxrdh0xm3 uid://d0k2wndjj0ifm

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"] [gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"] [ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"]
[node name="TitleList" type="VBoxContainer"] [node name="TitleList" type="VBoxContainer"]
anchors_preset = 15 anchors_preset = 15

View File

@@ -86,9 +86,9 @@ func _on_update_button_pressed() -> void:
if needs_reload: if needs_reload:
var will_refresh = on_before_refresh.call() var will_refresh = on_before_refresh.call()
if will_refresh: if will_refresh:
Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true) EditorInterface.restart_editor(true)
else: else:
var scale: float = Engine.get_meta("DialogueManagerPlugin").get_editor_interface().get_editor_scale() var scale: float = EditorInterface.get_editor_scale()
download_dialog.min_size = Vector2(300, 250) * scale download_dialog.min_size = Vector2(300, 250) * scale
download_dialog.popup_centered() download_dialog.popup_centered()
@@ -117,7 +117,7 @@ func _on_download_update_panel_failed() -> void:
func _on_needs_reload_dialog_confirmed() -> void: func _on_needs_reload_dialog_confirmed() -> void:
Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true) EditorInterface.restart_editor(true)
func _on_timer_timeout() -> void: func _on_timer_timeout() -> void:

View File

@@ -1 +1 @@
uid://boqd8cx71f1af uid://cr1tt12dh5ecr

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"] [gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"] [ext_resource type="Script" uid="uid://cr1tt12dh5ecr" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"]
[ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"] [ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"]
[node name="UpdateButton" type="Button"] [node name="UpdateButton" type="Button"]

View File

@@ -1,9 +1,23 @@
extends Node class_name DMConstants extends RefCounted
const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json" const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json"
const CACHE_PATH = "user://dialogue_manager_cache.json" const CACHE_PATH = "user://dialogue_manager_cache.json"
enum MutationBehaviour {
Wait,
DoNotWait,
Skip
}
enum TranslationSource {
None,
Guess,
CSV,
PO
}
# Token types # Token types
const TOKEN_FUNCTION = &"function" const TOKEN_FUNCTION = &"function"
@@ -33,21 +47,27 @@ const TOKEN_NUMBER = &"number"
const TOKEN_VARIABLE = &"variable" const TOKEN_VARIABLE = &"variable"
const TOKEN_COMMENT = &"comment" const TOKEN_COMMENT = &"comment"
const TOKEN_VALUE = &"value"
const TOKEN_ERROR = &"error" const TOKEN_ERROR = &"error"
# Line types # Line types
const TYPE_UNKNOWN = &"unknown" const TYPE_UNKNOWN = &""
const TYPE_IMPORT = &"import"
const TYPE_USING = &"using"
const TYPE_COMMENT = &"comment"
const TYPE_RESPONSE = &"response" const TYPE_RESPONSE = &"response"
const TYPE_TITLE = &"title" const TYPE_TITLE = &"title"
const TYPE_CONDITION = &"condition" const TYPE_CONDITION = &"condition"
const TYPE_WHILE = &"while"
const TYPE_MATCH = &"match"
const TYPE_WHEN = &"when"
const TYPE_MUTATION = &"mutation" const TYPE_MUTATION = &"mutation"
const TYPE_GOTO = &"goto" const TYPE_GOTO = &"goto"
const TYPE_DIALOGUE = &"dialogue" const TYPE_DIALOGUE = &"dialogue"
const TYPE_RANDOM = &"random"
const TYPE_ERROR = &"error" const TYPE_ERROR = &"error"
const TYPE_ELSE = &"else"
# Line IDs # Line IDs
const ID_NULL = &"" const ID_NULL = &""
@@ -64,7 +84,6 @@ const ERR_FILE_ALREADY_IMPORTED = 101
const ERR_DUPLICATE_IMPORT_NAME = 102 const ERR_DUPLICATE_IMPORT_NAME = 102
const ERR_EMPTY_TITLE = 103 const ERR_EMPTY_TITLE = 103
const ERR_DUPLICATE_TITLE = 104 const ERR_DUPLICATE_TITLE = 104
const ERR_NESTED_TITLE = 105
const ERR_TITLE_INVALID_CHARACTERS = 106 const ERR_TITLE_INVALID_CHARACTERS = 106
const ERR_UNKNOWN_TITLE = 107 const ERR_UNKNOWN_TITLE = 107
const ERR_INVALID_TITLE_REFERENCE = 108 const ERR_INVALID_TITLE_REFERENCE = 108
@@ -95,6 +114,11 @@ const ERR_UNEXPECTED_VARIABLE = 132
const ERR_INVALID_INDEX = 133 const ERR_INVALID_INDEX = 133
const ERR_UNEXPECTED_ASSIGNMENT = 134 const ERR_UNEXPECTED_ASSIGNMENT = 134
const ERR_UNKNOWN_USING = 135 const ERR_UNKNOWN_USING = 135
const ERR_EXPECTED_WHEN_OR_ELSE = 136
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
## Get the error message ## Get the error message
@@ -110,8 +134,6 @@ static func get_error_message(error: int) -> String:
return translate(&"errors.empty_title") return translate(&"errors.empty_title")
ERR_DUPLICATE_TITLE: ERR_DUPLICATE_TITLE:
return translate(&"errors.duplicate_title") return translate(&"errors.duplicate_title")
ERR_NESTED_TITLE:
return translate(&"errors.nested_title")
ERR_TITLE_INVALID_CHARACTERS: ERR_TITLE_INVALID_CHARACTERS:
return translate(&"errors.invalid_title_string") return translate(&"errors.invalid_title_string")
ERR_TITLE_BEGINS_WITH_NUMBER: ERR_TITLE_BEGINS_WITH_NUMBER:
@@ -172,14 +194,22 @@ static func get_error_message(error: int) -> String:
return translate(&"errors.unexpected_assignment") return translate(&"errors.unexpected_assignment")
ERR_UNKNOWN_USING: ERR_UNKNOWN_USING:
return translate(&"errors.unknown_using") return translate(&"errors.unknown_using")
ERR_EXPECTED_WHEN_OR_ELSE:
return translate(&"errors.expected_when_or_else")
ERR_ONLY_ONE_ELSE_ALLOWED:
return translate(&"errors.only_one_else_allowed")
ERR_WHEN_MUST_BELONG_TO_MATCH:
return translate(&"errors.when_must_belong_to_match")
ERR_CONCURRENT_LINE_WITHOUT_ORIGIN:
return translate(&"errors.concurrent_line_without_origin")
ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES:
return translate(&"errors.goto_not_allowed_on_concurrect_lines")
return translate(&"errors.unknown") return translate(&"errors.unknown")
static func translate(string: String) -> String: static func translate(string: String) -> String:
var temp_node = new() var base_path = new().get_script().resource_path.get_base_dir()
var base_path = temp_node.get_script().resource_path.get_base_dir()
temp_node.free()
var language: String = TranslationServer.get_tool_locale() var language: String = TranslationServer.get_tool_locale()
var translations_path: String = "%s/l10n/%s.po" % [base_path, language] var translations_path: String = "%s/l10n/%s.po" % [base_path, language]

View File

@@ -1 +1 @@
uid://c4hfjcd11f1l4 uid://b1oarbmjtyesf

View File

@@ -47,6 +47,7 @@ var dialogue_line:
set(next_dialogue_line): set(next_dialogue_line):
dialogue_line = next_dialogue_line dialogue_line = next_dialogue_line
custom_minimum_size = Vector2.ZERO custom_minimum_size = Vector2.ZERO
text = ""
text = dialogue_line.text text = dialogue_line.text
get: get:
return dialogue_line return dialogue_line
@@ -186,12 +187,13 @@ func _mutate_inline_mutations(index: int) -> void:
if inline_mutation[0] > index: if inline_mutation[0] > index:
return return
if inline_mutation[0] == index and not _already_mutated_indices.has(index): if inline_mutation[0] == index and not _already_mutated_indices.has(index):
_already_mutated_indices.append(index)
_is_awaiting_mutation = true _is_awaiting_mutation = true
# The DialogueManager can't be referenced directly here so we need to get it by its path # The DialogueManager can't be referenced directly here so we need to get it by its path
await Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true) await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
_is_awaiting_mutation = false _is_awaiting_mutation = false
_already_mutated_indices.append(index)
# Determine if the current autopause character at the cursor should qualify to pause typing. # Determine if the current autopause character at the cursor should qualify to pause typing.
func _should_auto_pause() -> bool: func _should_auto_pause() -> bool:
@@ -210,7 +212,7 @@ func _should_auto_pause() -> bool:
# Ignore "." if it's between two numbers # Ignore "." if it's between two numbers
if visible_characters > 3 and parsed_text[visible_characters - 1] == ".": if visible_characters > 3 and parsed_text[visible_characters - 1] == ".":
var possible_number: String = parsed_text.substr(visible_characters - 2, 3) var possible_number: String = parsed_text.substr(visible_characters - 2, 3)
if str(float(possible_number)) == possible_number: if str(float(possible_number)).pad_decimals(1) == possible_number:
return false return false
# Ignore "." if it's used in an abbreviation # Ignore "." if it's used in an abbreviation

View File

@@ -1 +1 @@
uid://c544a0pa1ckmf uid://g32um0mltv5d

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"] [gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"]
[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"] [ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"]
[node name="DialogueLabel" type="RichTextLabel"] [node name="DialogueLabel" type="RichTextLabel"]
anchors_preset = 10 anchors_preset = 10

View File

@@ -2,14 +2,11 @@
class_name DialogueLine extends RefCounted class_name DialogueLine extends RefCounted
const _DialogueConstants = preload("./constants.gd")
## The ID of this line ## The ID of this line
var id: String var id: String
## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code] ## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code]
var type: String = _DialogueConstants.TYPE_DIALOGUE var type: String = DMConstants.TYPE_DIALOGUE
## The next line ID after this line. ## The next line ID after this line.
var next_id: String = "" var next_id: String = ""
@@ -41,6 +38,9 @@ var inline_mutations: Array[Array] = []
## A list of responses attached to this line of dialogue. ## A list of responses attached to this line of dialogue.
var responses: Array = [] var responses: Array = []
## A list of lines that are spoken simultaneously with this one.
var concurrent_lines: Array[DialogueLine] = []
## A list of any extra game states to check when resolving variables and mutations. ## A list of any extra game states to check when resolving variables and mutations.
var extra_game_states: Array = [] var extra_game_states: Array = []
@@ -65,7 +65,7 @@ func _init(data: Dictionary = {}) -> void:
extra_game_states = data.get("extra_game_states", []) extra_game_states = data.get("extra_game_states", [])
match type: match type:
_DialogueConstants.TYPE_DIALOGUE: DMConstants.TYPE_DIALOGUE:
character = data.character character = data.character
character_replacements = data.get("character_replacements", [] as Array[Dictionary]) character_replacements = data.get("character_replacements", [] as Array[Dictionary])
text = data.text text = data.text
@@ -76,16 +76,17 @@ func _init(data: Dictionary = {}) -> void:
inline_mutations = data.get("inline_mutations", [] as Array[Array]) inline_mutations = data.get("inline_mutations", [] as Array[Array])
time = data.get("time", "") time = data.get("time", "")
tags = data.get("tags", []) tags = data.get("tags", [])
concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine])
_DialogueConstants.TYPE_MUTATION: DMConstants.TYPE_MUTATION:
mutation = data.mutation mutation = data.mutation
func _to_string() -> String: func _to_string() -> String:
match type: match type:
_DialogueConstants.TYPE_DIALOGUE: DMConstants.TYPE_DIALOGUE:
return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text] return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text]
_DialogueConstants.TYPE_MUTATION: DMConstants.TYPE_MUTATION:
return "<DialogueLine mutation>" return "<DialogueLine mutation>"
return "" return ""

View File

@@ -1 +1 @@
uid://cub47u83kehp uid://rhuq0eyf8ar2

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
uid://cst7cjlwxlxj3 uid://c3rodes2l3gxb

View File

@@ -1 +0,0 @@
uid://bq8fntgcwiosq

View File

@@ -5,7 +5,6 @@
class_name DialogueResource extends Resource class_name DialogueResource extends Resource
const _DialogueManager = preload("./dialogue_manager.gd")
const DialogueLine = preload("./dialogue_line.gd") const DialogueLine = preload("./dialogue_line.gd")
## A list of state shortcuts ## A list of state shortcuts
@@ -30,7 +29,7 @@ const DialogueLine = preload("./dialogue_line.gd")
## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can ## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can
## be a title string or a stringified line number). Runs any mutations along the way and then returns ## be a title string or a stringified line number). Runs any mutations along the way and then returns
## the first dialogue line encountered. ## the first dialogue line encountered.
func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: _DialogueManager.MutationBehaviour = _DialogueManager.MutationBehaviour.Wait) -> DialogueLine: func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine:
return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour) return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour)

View File

@@ -1 +1 @@
uid://3we8jdve7k1n uid://dbs4435dsf3ry

View File

@@ -2,14 +2,11 @@
class_name DialogueResponse extends RefCounted class_name DialogueResponse extends RefCounted
const _DialogueConstants = preload("./constants.gd")
## The ID of this response ## The ID of this response
var id: String var id: String
## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code]. ## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code].
var type: String = _DialogueConstants.TYPE_RESPONSE var type: String = DMConstants.TYPE_RESPONSE
## The next line ID to use if this response is selected by the player. ## The next line ID to use if this response is selected by the player.
var next_id: String = "" var next_id: String = ""

View File

@@ -1 +1 @@
uid://5oenbic4tlvj uid://cm0xpfeywpqid

View File

@@ -14,6 +14,9 @@ signal response_selected(response)
## The action for accepting a response (is possibly overridden by parent dialogue balloon). ## The action for accepting a response (is possibly overridden by parent dialogue balloon).
@export var next_action: StringName = &"" @export var next_action: StringName = &""
## Hide any responses where [code]is_allowed[/code] is false
@export var hide_failed_responses: bool = false
## The list of dialogue responses. ## The list of dialogue responses.
var responses: Array = []: var responses: Array = []:
get: get:
@@ -31,6 +34,8 @@ var responses: Array = []:
# Add new items # Add new items
if responses.size() > 0: if responses.size() > 0:
for response in responses: for response in responses:
if hide_failed_responses and not response.is_allowed: continue
var item: Control var item: Control
if is_instance_valid(response_template): if is_instance_valid(response_template):
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS) item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
@@ -39,7 +44,7 @@ var responses: Array = []:
item = Button.new() item = Button.new()
item.name = "Response%d" % get_child_count() item.name = "Response%d" % get_child_count()
if not response.is_allowed: if not response.is_allowed:
item.name = String(item.name) + "Disallowed" item.name = item.name + &"Disallowed"
item.disabled = true item.disabled = true
# If the item has a response property then use that # If the item has a response property then use that
@@ -59,7 +64,9 @@ var responses: Array = []:
func _ready() -> void: func _ready() -> void:
visibility_changed.connect(func(): visibility_changed.connect(func():
if visible and get_menu_items().size() > 0: if visible and get_menu_items().size() > 0:
get_menu_items()[0].grab_focus() var first_item: Control = get_menu_items()[0]
if first_item.is_inside_tree():
first_item.grab_focus()
) )
if is_instance_valid(response_template): if is_instance_valid(response_template):
@@ -77,11 +84,6 @@ func get_menu_items() -> Array:
return items return items
## [b]DEPRECATED[/b]. Do not use.
func set_responses(next_responses: Array) -> void:
self.responses = next_responses
#region Internal #region Internal
@@ -130,11 +132,11 @@ func _on_response_mouse_entered(item: Control) -> void:
func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: func _on_response_gui_input(event: InputEvent, item: Control, response) -> void:
if "Disallowed" in item.name: return if "Disallowed" in item.name: return
get_viewport().set_input_as_handled()
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
get_viewport().set_input_as_handled()
response_selected.emit(response) response_selected.emit(response)
elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items(): elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items():
get_viewport().set_input_as_handled()
response_selected.emit(response) response_selected.emit(response)

View File

@@ -0,0 +1 @@
uid://bb52rsfwhkxbn

View File

@@ -1,43 +1,52 @@
extends EditorTranslationParserPlugin class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin
const DialogueConstants = preload("./constants.gd") ## Cached result of parsing a dialogue file.
const DialogueSettings = preload("./settings.gd") var data: DMCompilerResult
const DialogueManagerParser = preload("./components/parser.gd") ## List of characters that were added.
const DialogueManagerParseResult = preload("./components/parse_result.gd") var translated_character_names: PackedStringArray = []
var translated_lines: Array[Dictionary] = []
func _parse_file(path: String, msgids: Array, msgids_context_plural: Array) -> void: func _parse_file(path: String) -> Array[PackedStringArray]:
var msgs: Array[PackedStringArray] = []
var file: FileAccess = FileAccess.open(path, FileAccess.READ) var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var text: String = file.get_as_text() var text: String = file.get_as_text()
var data: DialogueManagerParseResult = DialogueManagerParser.parse_string(text, path) data = DMCompiler.compile_string(text, path)
var known_keys: PackedStringArray = PackedStringArray([]) var known_keys: PackedStringArray = PackedStringArray([])
# Add all character names if settings ask for it # Add all character names if settings ask for it
if DialogueSettings.get_setting("export_characters_in_translation", true): if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true):
var character_names: PackedStringArray = data.character_names translated_character_names = [] as Array[DialogueLine]
for character_name in character_names: for character_name: String in data.character_names:
if character_name in known_keys: continue if character_name in known_keys: continue
known_keys.append(character_name) known_keys.append(character_name)
msgids_context_plural.append([character_name.replace('"', '\\"'), "dialogue", ""]) translated_character_names.append(character_name)
msgs.append(PackedStringArray([character_name.replace('"', '\"'), "dialogue", "", DMConstants.translate("translation_plugin.character_name")]))
# Add all dialogue lines and responses # Add all dialogue lines and responses
var dialogue: Dictionary = data.lines var dialogue: Dictionary = data.lines
for key in dialogue.keys(): for key: String in dialogue.keys():
var line: Dictionary = dialogue.get(key) var line: Dictionary = dialogue.get(key)
if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue
if line.translation_key in known_keys: continue
known_keys.append(line.translation_key) var translation_key: String = line.get(&"translation_key", line.text)
if line.translation_key == "" or line.translation_key == line.text: if translation_key in known_keys: continue
msgids_context_plural.append([line.text.replace('"', '\\"'), "", ""])
known_keys.append(translation_key)
translated_lines.append(line)
if translation_key == line.text:
msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")]))
else: else:
msgids_context_plural.append([line.text.replace('"', '\\"'), line.translation_key.replace('"', '\\"'), ""]) msgs.append(PackedStringArray([line.text.replace('"', '\"'), line.translation_key.replace('"', '\"'), "", line.get("notes", "")]))
return msgs
func _get_recognized_extensions() -> PackedStringArray: func _get_recognized_extensions() -> PackedStringArray:

View File

@@ -1 +1 @@
uid://cgib8ttuvr1o8 uid://c6bya881h1egb

View File

@@ -1,204 +1,223 @@
using Godot; using Godot;
using Godot.Collections; using Godot.Collections;
namespace DialogueManagerRuntime; namespace DialogueManagerRuntime
public partial class ExampleBalloon : CanvasLayer
{ {
[Export] public string NextAction = "ui_accept"; public partial class ExampleBalloon : CanvasLayer
[Export] public string SkipAction = "ui_cancel";
Control balloon;
RichTextLabel characterLabel;
RichTextLabel dialogueLabel;
VBoxContainer responsesMenu;
Resource resource;
Array<Variant> temporaryGameStates = new Array<Variant>();
bool isWaitingForInput = false;
bool willHideBalloon = false;
DialogueLine dialogueLine;
DialogueLine DialogueLine
{ {
get => dialogueLine; [Export] public string NextAction = "ui_accept";
set [Export] public string SkipAction = "ui_cancel";
{
isWaitingForInput = false;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
if (value == null)
{
QueueFree();
return;
}
dialogueLine = value; Control balloon;
UpdateDialogue(); RichTextLabel characterLabel;
} RichTextLabel dialogueLabel;
VBoxContainer responsesMenu;
Resource resource;
Array<Variant> temporaryGameStates = new Array<Variant>();
bool isWaitingForInput = false;
bool willHideBalloon = false;
DialogueLine dialogueLine;
DialogueLine DialogueLine
{
get => dialogueLine;
set
{
if (value == null)
{
QueueFree();
return;
}
dialogueLine = value;
ApplyDialogueLine();
}
}
Timer MutationCooldown = new Timer();
public override void _Ready()
{
balloon = GetNode<Control>("%Balloon");
characterLabel = GetNode<RichTextLabel>("%CharacterLabel");
dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel");
responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu");
balloon.Hide();
balloon.GuiInput += (@event) =>
{
if ((bool)dialogueLabel.Get("is_typing"))
{
bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed();
bool skipButtonWasPressed = @event.IsActionPressed(SkipAction);
if (mouseWasClicked || skipButtonWasPressed)
{
GetViewport().SetInputAsHandled();
dialogueLabel.Call("skip_typing");
return;
}
}
if (!isWaitingForInput) return;
if (dialogueLine.Responses.Count > 0) return;
GetViewport().SetInputAsHandled();
if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left)
{
Next(dialogueLine.NextId);
}
else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon)
{
Next(dialogueLine.NextId);
}
};
if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action")))
{
responsesMenu.Set("next_action", NextAction);
}
responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) =>
{
Next(response.NextId);
}));
// Hide the balloon when a mutation is running
MutationCooldown.Timeout += () =>
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
AddChild(MutationCooldown);
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 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<Variant> extraGameStates = null)
{
temporaryGameStates = new Array<Variant> { this } + (extraGameStates ?? new Array<Variant>());
isWaitingForInput = false;
resource = dialogueResource;
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates);
}
public async void Next(string nextId)
{
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates);
}
#region Helpers
private async void ApplyDialogueLine()
{
MutationCooldown.Stop();
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 dialogue
dialogueLabel.Hide();
dialogueLabel.Set("dialogue_line", dialogueLine);
// Set up the responses
responsesMenu.Hide();
responsesMenu.Set("responses", dialogueLine.Responses);
// Type out the text
balloon.Show();
willHideBalloon = false;
dialogueLabel.Show();
if (!string.IsNullOrEmpty(dialogueLine.Text))
{
dialogueLabel.Call("type_out");
await ToSignal(dialogueLabel, "finished_typing");
}
// Wait for input
if (dialogueLine.Responses.Count > 0)
{
balloon.FocusMode = Control.FocusModeEnum.None;
responsesMenu.Show();
}
else if (!string.IsNullOrEmpty(dialogueLine.Time))
{
float time = 0f;
if (!float.TryParse(dialogueLine.Time, out time))
{
time = dialogueLine.Text.Length * 0.02f;
}
await ToSignal(GetTree().CreateTimer(time), "timeout");
Next(dialogueLine.NextId);
}
else
{
isWaitingForInput = true;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
}
}
#endregion
#region signals
private void OnMutated(Dictionary _mutation)
{
isWaitingForInput = false;
willHideBalloon = true;
MutationCooldown.Start(0.1f);
}
#endregion
} }
}
public override void _Ready()
{
balloon = GetNode<Control>("%Balloon");
characterLabel = GetNode<RichTextLabel>("%CharacterLabel");
dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel");
responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu");
balloon.Hide();
balloon.GuiInput += (@event) =>
{
if ((bool)dialogueLabel.Get("is_typing"))
{
bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed();
bool skipButtonWasPressed = @event.IsActionPressed(SkipAction);
if (mouseWasClicked || skipButtonWasPressed)
{
GetViewport().SetInputAsHandled();
dialogueLabel.Call("skip_typing");
return;
}
}
if (!isWaitingForInput)
return;
if (dialogueLine.Responses.Count > 0)
return;
GetViewport().SetInputAsHandled();
if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left)
{
Next(dialogueLine.NextId);
}
else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon)
{
Next(dialogueLine.NextId);
}
};
if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action")))
{
responsesMenu.Set("next_action", NextAction);
}
responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) =>
{
Next(response.NextId);
}));
DialogueManager.Mutated += OnMutated;
}
public override void _ExitTree()
{
DialogueManager.Mutated -= OnMutated;
}
public override void _UnhandledInput(InputEvent @event)
{
// Only the balloon is allowed to handle input while it's showing
GetViewport().SetInputAsHandled();
}
public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null)
{
temporaryGameStates = extraGameStates ?? new Array<Variant>();
isWaitingForInput = false;
resource = dialogueResource;
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates);
}
public async void Next(string nextId)
{
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates);
}
#region Helpers
private async void UpdateDialogue()
{
if (!IsNodeReady())
{
await ToSignal(this, SignalName.Ready);
}
// Set up the character name
characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character);
characterLabel.Text = Tr(dialogueLine.Character, "dialogue");
// Set up the dialogue
dialogueLabel.Hide();
dialogueLabel.Set("dialogue_line", dialogueLine);
// Set up the responses
responsesMenu.Hide();
responsesMenu.Set("responses", dialogueLine.Responses);
// Type out the text
balloon.Show();
willHideBalloon = false;
dialogueLabel.Show();
if (!string.IsNullOrEmpty(dialogueLine.Text))
{
dialogueLabel.Call("type_out");
await ToSignal(dialogueLabel, "finished_typing");
}
// Wait for input
if (dialogueLine.Responses.Count > 0)
{
balloon.FocusMode = Control.FocusModeEnum.None;
responsesMenu.Show();
}
else if (!string.IsNullOrEmpty(dialogueLine.Time))
{
float time = 0f;
if (!float.TryParse(dialogueLine.Time, out time))
{
time = dialogueLine.Text.Length * 0.02f;
}
await ToSignal(GetTree().CreateTimer(time), "timeout");
Next(dialogueLine.NextId);
}
else
{
isWaitingForInput = true;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
}
}
#endregion
#region signals
private void OnMutated(Dictionary _mutation)
{
isWaitingForInput = false;
willHideBalloon = true;
GetTree().CreateTimer(0.1f).Timeout += () =>
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
}
#endregion
}

View File

@@ -1 +1 @@
uid://dac8psvf0vqvj uid://5b3w40kwakl3

View File

@@ -1,4 +1,5 @@
extends CanvasLayer class_name DialogueManagerExampleBalloon extends CanvasLayer
## A basic dialogue balloon for use with Dialogue Manager.
## The action to use for advancing the dialogue ## The action to use for advancing the dialogue
@export var next_action: StringName = &"ui_accept" @export var next_action: StringName = &"ui_accept"
@@ -6,11 +7,6 @@ extends CanvasLayer
## The action to use to skip typing the dialogue ## The action to use to skip typing the dialogue
@export var skip_action: StringName = &"ui_cancel" @export var skip_action: StringName = &"ui_cancel"
@onready var balloon: Control = %Balloon
@onready var character_label: RichTextLabel = %CharacterLabel
@onready var dialogue_label: DialogueLabel = %DialogueLabel
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
## The dialogue resource ## The dialogue resource
var resource: DialogueResource var resource: DialogueResource
@@ -23,59 +19,38 @@ var is_waiting_for_input: bool = false
## See if we are running a long mutation and should hide the balloon ## See if we are running a long mutation and should hide the balloon
var will_hide_balloon: bool = false var will_hide_balloon: bool = false
## A dictionary to store any ephemeral variables
var locals: Dictionary = {}
var _locale: String = TranslationServer.get_locale() var _locale: String = TranslationServer.get_locale()
## The current line ## The current line
var dialogue_line: DialogueLine: var dialogue_line: DialogueLine:
set(next_dialogue_line): set(value):
is_waiting_for_input = false if value:
balloon.focus_mode = Control.FOCUS_ALL dialogue_line = value
balloon.grab_focus() apply_dialogue_line()
# The dialogue has finished so close the balloon
if not next_dialogue_line:
queue_free()
return
# If the node isn't ready yet then none of the labels will be ready yet either
if not is_node_ready():
await ready
dialogue_line = next_dialogue_line
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
dialogue_label.hide()
dialogue_label.dialogue_line = dialogue_line
responses_menu.hide()
responses_menu.set_responses(dialogue_line.responses)
# Show our balloon
balloon.show()
will_hide_balloon = false
dialogue_label.show()
if not dialogue_line.text.is_empty():
dialogue_label.type_out()
await dialogue_label.finished_typing
# Wait for input
if dialogue_line.responses.size() > 0:
balloon.focus_mode = Control.FOCUS_NONE
responses_menu.show()
elif dialogue_line.time != "":
var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
await get_tree().create_timer(time).timeout
next(dialogue_line.next_id)
else: else:
is_waiting_for_input = true # The dialogue has finished so close the balloon
balloon.focus_mode = Control.FOCUS_ALL queue_free()
balloon.grab_focus()
get: get:
return dialogue_line return dialogue_line
## A cooldown timer for delaying the balloon hide when encountering a mutation.
var mutation_cooldown: Timer = Timer.new()
## The base balloon anchor
@onready var balloon: Control = %Balloon
## The label showing the name of the currently speaking character
@onready var character_label: RichTextLabel = %CharacterLabel
## The label showing the currently spoken dialogue
@onready var dialogue_label: DialogueLabel = %DialogueLabel
## The menu of responses
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
func _ready() -> void: func _ready() -> void:
balloon.hide() balloon.hide()
@@ -85,6 +60,9 @@ func _ready() -> void:
if responses_menu.next_action.is_empty(): if responses_menu.next_action.is_empty():
responses_menu.next_action = next_action responses_menu.next_action = next_action
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
add_child(mutation_cooldown)
func _unhandled_input(_event: InputEvent) -> void: func _unhandled_input(_event: InputEvent) -> void:
# Only the balloon is allowed to handle input while it's showing # Only the balloon is allowed to handle input while it's showing
@@ -103,12 +81,52 @@ func _notification(what: int) -> void:
## Start some dialogue ## Start some dialogue
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
temporary_game_states = [self] + extra_game_states temporary_game_states = [self] + extra_game_states
is_waiting_for_input = false is_waiting_for_input = false
resource = dialogue_resource resource = dialogue_resource
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
## Apply any changes to the balloon given a new [DialogueLine].
func apply_dialogue_line() -> void:
mutation_cooldown.stop()
is_waiting_for_input = false
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
dialogue_label.hide()
dialogue_label.dialogue_line = dialogue_line
responses_menu.hide()
responses_menu.responses = dialogue_line.responses
# Show our balloon
balloon.show()
will_hide_balloon = false
dialogue_label.show()
if not dialogue_line.text.is_empty():
dialogue_label.type_out()
await dialogue_label.finished_typing
# Wait for input
if dialogue_line.responses.size() > 0:
balloon.focus_mode = Control.FOCUS_NONE
responses_menu.show()
elif dialogue_line.time != "":
var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
await get_tree().create_timer(time).timeout
next(dialogue_line.next_id)
else:
is_waiting_for_input = true
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
## Go to the next line ## Go to the next line
func next(next_id: String) -> void: func next(next_id: String) -> void:
self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states)
@@ -117,14 +135,16 @@ func next(next_id: String) -> void:
#region Signals #region Signals
func _on_mutation_cooldown_timeout() -> void:
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
func _on_mutated(_mutation: Dictionary) -> void: func _on_mutated(_mutation: Dictionary) -> void:
is_waiting_for_input = false is_waiting_for_input = false
will_hide_balloon = true will_hide_balloon = true
get_tree().create_timer(0.1).timeout.connect(func(): mutation_cooldown.start(0.1)
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
)
func _on_balloon_gui_input(event: InputEvent) -> void: func _on_balloon_gui_input(event: InputEvent) -> void:

View File

@@ -1 +1 @@
uid://by1vx32y4g8gs uid://d1wt4ma6055l8

View File

@@ -1,8 +1,8 @@
[gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"] [gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"]
[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"] [ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"]
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"]
[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_72ixx"] [ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"]
bg_color = Color(0, 0, 0, 1) bg_color = Color(0, 0, 0, 1)

View File

@@ -1,8 +1,8 @@
[gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"] [gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"]
[ext_resource type="Script" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"] [ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"]
[ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"] [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"]
[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_1j1j0"] [ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_1j1j0"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"]
content_margin_left = 6.0 content_margin_left = 6.0
@@ -104,6 +104,7 @@ grow_vertical = 2
theme = SubResource("Theme_qq3yp") theme = SubResource("Theme_qq3yp")
[node name="Panel" type="Panel" parent="Balloon"] [node name="Panel" type="Panel" parent="Balloon"]
clip_children = 2
layout_mode = 1 layout_mode = 1
anchors_preset = 12 anchors_preset = 12
anchor_top = 1.0 anchor_top = 1.0
@@ -115,6 +116,7 @@ offset_right = -4.0
offset_bottom = -4.0 offset_bottom = -4.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
mouse_filter = 1
[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] [node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"]
layout_mode = 1 layout_mode = 1
@@ -142,7 +144,6 @@ unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
text = "Dialogue..." text = "Dialogue..."
skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
[node name="Responses" type="MarginContainer" parent="Balloon"] [node name="Responses" type="MarginContainer" parent="Balloon"]
layout_mode = 1 layout_mode = 1

View File

@@ -0,0 +1,20 @@
class_name DMExportPlugin extends EditorExportPlugin
const IGNORED_PATHS = [
"/assets",
"/components",
"/l10n",
"/views"
]
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path()
# Ignore any editor stuff
for ignored_path: String in IGNORED_PATHS:
if path.begins_with(plugin_path + ignored_path):
skip()
# Ignore C# stuff it not using dotnet
if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"):
skip()

View File

@@ -0,0 +1 @@
uid://sa55ra11ji2q

View File

@@ -1,20 +1,16 @@
@tool @tool
extends EditorImportPlugin class_name DMImportPlugin extends EditorImportPlugin
signal compiled_resource(resource: Resource) signal compiled_resource(resource: Resource)
const DialogueResource = preload("./dialogue_resource.gd") const COMPILER_VERSION = 14
const DialogueManagerParser = preload("./components/parser.gd")
const DialogueManagerParseResult = preload("./components/parse_result.gd")
const compiler_version = 12
func _get_importer_name() -> String: func _get_importer_name() -> String:
# NOTE: A change to this forces a re-import of all dialogue # NOTE: A change to this forces a re-import of all dialogue
return "dialogue_manager_compiler_%s" % compiler_version return "dialogue_manager_compiler_%s" % COMPILER_VERSION
func _get_visible_name() -> String: func _get_visible_name() -> String:
@@ -63,7 +59,7 @@ func _get_option_visibility(path: String, option_name: StringName, options: Dict
func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error: func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error:
var cache = Engine.get_meta("DialogueCache") var cache = Engine.get_meta("DMCache")
# Get the raw file contents # Get the raw file contents
if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND
@@ -73,17 +69,12 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo
cache.file_content_changed.emit(source_file, raw_text) cache.file_content_changed.emit(source_file, raw_text)
# Parse the text # Compile the text
var parser: DialogueManagerParser = DialogueManagerParser.new() var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file)
var err: Error = parser.parse(raw_text, source_file) if result.errors.size() > 0:
var data: DialogueManagerParseResult = parser.get_data() printerr("%d errors found in %s" % [result.errors.size(), source_file])
var errors: Array[Dictionary] = parser.get_errors() cache.add_errors_to_file(source_file, result.errors)
parser.free() return ERR_PARSE_ERROR
if err != OK:
printerr("%d errors found in %s" % [errors.size(), source_file])
cache.add_errors_to_file(source_file, errors)
return err
# Get the current addon version # Get the current addon version
var config: ConfigFile = ConfigFile.new() var config: ConfigFile = ConfigFile.new()
@@ -94,17 +85,17 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo
var resource: DialogueResource = DialogueResource.new() var resource: DialogueResource = DialogueResource.new()
resource.set_meta("dialogue_manager_version", version) resource.set_meta("dialogue_manager_version", version)
resource.using_states = data.using_states resource.using_states = result.using_states
resource.titles = data.titles resource.titles = result.titles
resource.first_title = data.first_title resource.first_title = result.first_title
resource.character_names = data.character_names resource.character_names = result.character_names
resource.lines = data.lines resource.lines = result.lines
resource.raw_text = data.raw_text resource.raw_text = result.raw_text
# Clear errors and possibly trigger any cascade recompiles # Clear errors and possibly trigger any cascade recompiles
cache.add_file(source_file, data) cache.add_file(source_file, result)
err = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()]) var err: Error = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()])
compiled_resource.emit(resource) compiled_resource.emit(resource)

View File

@@ -1 +1 @@
uid://db6mool8x8kfk uid://dhwpj6ed8soyq

View File

@@ -1,5 +1,5 @@
@tool @tool
extends EditorInspectorPlugin class_name DMInspectorPlugin extends EditorInspectorPlugin
const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd") const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd")

View File

@@ -1 +1 @@
uid://dsdontxsvnauk uid://0x31sbqbikov

View File

@@ -21,6 +21,9 @@ msgstr "Open a file"
msgid "open.open" msgid "open.open"
msgstr "Open..." msgstr "Open..."
msgid "open.quick_open"
msgstr "Quick open..."
msgid "open.no_recent_files" msgid "open.no_recent_files"
msgstr "No recent files" msgstr "No recent files"
@@ -30,11 +33,17 @@ msgstr "Clear recent files"
msgid "save_all_files" msgid "save_all_files"
msgstr "Save all files" msgstr "Save all files"
msgid "all"
msgstr "All"
msgid "find_in_files" msgid "find_in_files"
msgstr "Find in files..." msgstr "Find in files..."
msgid "test_dialogue" msgid "test_dialogue"
msgstr "Test dialogue" msgstr "Test dialogue from start of file"
msgid "test_dialogue_from_line"
msgstr "Test dialogue from current line"
msgid "search_for_text" msgid "search_for_text"
msgstr "Search for text" msgstr "Search for text"
@@ -45,9 +54,6 @@ msgstr "Insert"
msgid "translations" msgid "translations"
msgstr "Translations" msgstr "Translations"
msgid "settings"
msgstr "Settings"
msgid "sponsor" msgid "sponsor"
msgstr "Sponsor" msgstr "Sponsor"
@@ -141,84 +147,6 @@ msgstr "Copy file path"
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "Show in FileSystem" msgstr "Show in FileSystem"
msgid "settings.invalid_test_scene"
msgstr "\"{path}\" does not extend BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Revert to default test scene"
msgid "settings.default_balloon_hint"
msgstr "Custom balloon to use when calling \"DialogueManager.show_balloon()\""
msgid "settings.revert_to_default_balloon"
msgstr "Revert to default balloon"
msgid "settings.default_balloon_path"
msgstr "<example balloon>"
msgid "settings.autoload"
msgstr "Autoload"
msgid "settings.path"
msgstr "Path"
msgid "settings.new_template"
msgstr "New dialogue files will start with template text"
msgid "settings.missing_keys"
msgstr "Treat missing translation keys as errors"
msgid "settings.missing_keys_hint"
msgstr "If you are using static translation keys then having this enabled will help you find any lines that you haven't added a key to yet."
msgid "settings.characters_translations"
msgstr "Export character names in translation files"
msgid "settings.wrap_long_lines"
msgstr "Wrap long lines"
msgid "settings.include_failed_responses"
msgstr "Include responses with failed conditions"
msgid "settings.ignore_missing_state_values"
msgstr "Skip over missing state value errors (not recommended)"
msgid "settings.custom_test_scene"
msgstr "Custom test scene (must extend BaseDialogueTestScene)"
msgid "settings.default_csv_locale"
msgstr "Default CSV Locale"
msgid "settings.states_shortcuts"
msgstr "State Shortcuts"
msgid "settings.states_message"
msgstr "If an autoload is enabled here you can refer to its properties, methods, and signals without having to use its name."
msgid "settings.states_hint"
msgstr "ie. Instead of \"SomeState.some_property\" you could just use \"some_property\""
msgid "settings.recompile_warning"
msgstr "Changing these settings will force a recompile of all dialogue. Only change them if you know what you are doing."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Create child dialogue line for responses with character names in them"
msgid "settings.open_in_external_editor"
msgstr "Open dialogue files in external editor"
msgid "settings.external_editor_warning"
msgstr "Note: Syntax highlighting and detailed error checking are not supported in external editors."
msgid "settings.include_characters_in_translations"
msgstr "Include character names in translation exports"
msgid "settings.include_notes_in_translations"
msgstr "Include notes (## comments) in translation exports"
msgid "settings.check_for_updates"
msgstr "Check for updates"
msgid "n_of_n" msgid "n_of_n"
msgstr "{index} of {total}" msgstr "{index} of {total}"
@@ -294,9 +222,6 @@ msgstr "Titles cannot be empty."
msgid "errors.duplicate_title" msgid "errors.duplicate_title"
msgstr "There is already a title with that name." msgstr "There is already a title with that name."
msgid "errors.nested_title"
msgstr "Titles cannot be indented."
msgid "errors.invalid_title_string" msgid "errors.invalid_title_string"
msgstr "Titles can only contain alphanumeric characters and numbers." msgstr "Titles can only contain alphanumeric characters and numbers."
@@ -384,6 +309,21 @@ msgstr "Invalid index."
msgid "errors.unexpected_assignment" msgid "errors.unexpected_assignment"
msgstr "Unexpected assignment." msgstr "Unexpected assignment."
msgid "errors.expected_when_or_else"
msgstr "Expecting a when or an else case."
msgid "errors.only_one_else_allowed"
msgstr "Only one else case is allowed per match."
msgid "errors.when_must_belong_to_match"
msgstr "When statements can only appear as children of match statements."
msgid "errors.concurrent_line_without_origin"
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.unknown" msgid "errors.unknown"
msgstr "Unknown syntax." msgstr "Unknown syntax."
@@ -478,4 +418,10 @@ msgid "runtime.unsupported_array_type"
msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead." msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead."
msgid "runtime.dialogue_balloon_missing_start_method" msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method." msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method."
msgid "runtime.top_level_states_share_name"
msgstr "Multiple top-level states ({states}) share method/property/signal name \"{key}\". Only the first occurance is accessible to dialogue."
msgid "translation_plugin.character_name"
msgstr "Character name"

View File

@@ -41,9 +41,6 @@ msgstr "Insertar"
msgid "translations" msgid "translations"
msgstr "Traducciones" msgstr "Traducciones"
msgid "settings"
msgstr "Ajustes"
msgid "show_support" msgid "show_support"
msgstr "Contribuye con Dialogue Manager" msgstr "Contribuye con Dialogue Manager"
@@ -134,82 +131,6 @@ msgstr "Copiar la ruta del archivo"
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "Mostrar en el sistema de archivos" msgstr "Mostrar en el sistema de archivos"
msgid "settings.invalid_test_scene"
msgstr "\"{path}\" no extiende BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Revertir a la escena de prueba por defecto"
msgid "settings.default_balloon_hint"
msgstr ""
"Globo personalizado para usar al llamar a \"DialogueManager.show_balloon()\""
msgid "settings.revert_to_default_balloon"
msgstr "Volver al globo predeterminado"
msgid "settings.default_balloon_path"
msgstr "<globo de ejemplo>"
msgid "settings.autoload"
msgstr "Autocarga"
msgid "settings.path"
msgstr "Ruta"
msgid "settings.new_template"
msgstr "Los nuevos archivos de diálogo empezarán con una plantilla"
msgid "settings.missing_keys"
msgstr "Tratar las claves de traducción faltantes como errores"
msgid "settings.missing_keys_hint"
msgstr "Si estás utilizando claves de traducción estáticas, tener esta opción habilitada te ayudará a encontrar cualquier línea a la que aún no le hayas añadido una clave."
msgid "settings.characters_translations"
msgstr "Exportar nombres de personajes en archivos de traducción"
msgid "settings.wrap_long_lines"
msgstr "Romper líneas largas"
msgid "settings.include_failed_responses"
msgstr "Incluir respuestas con condiciones fallidas"
msgid "settings.ignore_missing_state_values"
msgstr "Omitir errores de valores de estado faltantes (no recomendado)"
msgid "settings.custom_test_scene"
msgstr "Escena de prueba personalizada (debe extender BaseDialogueTestScene)"
msgid "settings.default_csv_locale"
msgstr "Localización CSV por defecto"
msgid "settings.states_shortcuts"
msgstr "Atajos de teclado"
msgid "settings.states_message"
msgstr "Si un autoload está habilitado aquí, puedes referirte a sus propiedades y métodos sin tener que usar su nombre."
msgid "settings.states_hint"
msgstr "ie. En lugar de \"SomeState.some_property\" podría simplemente usar \"some_property\""
msgid "settings.recompile_warning"
msgstr "Cambiar estos ajustes obligará a recompilar todo el diálogo. Hazlo solo si sabes lo que estás haciendo."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Crear línea de diálogo para respuestas con nombres de personajes dentro."
msgid "settings.open_in_external_editor"
msgstr "Abrir archivos de diálogo en el editor externo"
msgid "settings.external_editor_warning"
msgstr "Nota: El resaltado de sintaxis y la verificación detallada de errores no están soportados en editores externos."
msgid "settings.include_characters_in_translations"
msgstr "Incluir nombres de personajes en las exportaciones de traducción"
msgid "settings.include_notes_in_translations"
msgstr "Incluir notas (## comentarios) en las exportaciones de traducción"
msgid "n_of_n" msgid "n_of_n"
msgstr "{index} de {total}" msgstr "{index} de {total}"

View File

@@ -14,6 +14,9 @@ msgstr ""
msgid "open.open" msgid "open.open"
msgstr "" msgstr ""
msgid "open.quick_open"
msgstr ""
msgid "open.no_recent_files" msgid "open.no_recent_files"
msgstr "" msgstr ""
@@ -23,12 +26,18 @@ msgstr ""
msgid "save_all_files" msgid "save_all_files"
msgstr "" msgstr ""
msgid "all"
msgstr ""
msgid "find_in_files" msgid "find_in_files"
msgstr "" msgstr ""
msgid "test_dialogue" msgid "test_dialogue"
msgstr "" msgstr ""
msgid "test_dialogue_from_line"
msgstr ""
msgid "search_for_text" msgid "search_for_text"
msgstr "" msgstr ""
@@ -38,9 +47,6 @@ msgstr ""
msgid "translations" msgid "translations"
msgstr "" msgstr ""
msgid "settings"
msgstr ""
msgid "sponsor" msgid "sponsor"
msgstr "" msgstr ""
@@ -131,84 +137,6 @@ msgstr ""
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "" msgstr ""
msgid "settings.invalid_test_scene"
msgstr ""
msgid "settings.revert_to_default_test_scene"
msgstr ""
msgid "settings.default_balloon_hint"
msgstr ""
msgid "settings.revert_to_default_balloon"
msgstr ""
msgid "settings.default_balloon_path"
msgstr ""
msgid "settings.autoload"
msgstr ""
msgid "settings.path"
msgstr ""
msgid "settings.new_template"
msgstr ""
msgid "settings.missing_keys"
msgstr ""
msgid "settings.missing_keys_hint"
msgstr ""
msgid "settings.characters_translations"
msgstr ""
msgid "settings.wrap_long_lines"
msgstr ""
msgid "settings.include_failed_responses"
msgstr ""
msgid "settings.ignore_missing_state_values"
msgstr ""
msgid "settings.custom_test_scene"
msgstr ""
msgid "settings.default_csv_locale"
msgstr ""
msgid "settings.states_shortcuts"
msgstr ""
msgid "settings.states_message"
msgstr ""
msgid "settings.states_hint"
msgstr ""
msgid "settings.recompile_warning"
msgstr ""
msgid "settings.create_lines_for_responses_with_characters"
msgstr ""
msgid "settings.open_in_external_editor"
msgstr ""
msgid "settings.external_editor_warning"
msgstr ""
msgid "settings.include_characters_in_translations"
msgstr ""
msgid "settings.include_notes_in_translations"
msgstr ""
msgid "settings.check_for_updates"
msgstr ""
msgid "n_of_n" msgid "n_of_n"
msgstr "" msgstr ""
@@ -284,9 +212,6 @@ msgstr ""
msgid "errors.duplicate_title" msgid "errors.duplicate_title"
msgstr "" msgstr ""
msgid "errors.nested_title"
msgstr ""
msgid "errors.invalid_title_string" msgid "errors.invalid_title_string"
msgstr "" msgstr ""
@@ -374,6 +299,21 @@ msgstr ""
msgid "errors.unexpected_assignment" msgid "errors.unexpected_assignment"
msgstr "" msgstr ""
msgid "errors.expected_when_or_else"
msgstr ""
msgid "errors.only_one_else_allowed"
msgstr ""
msgid "errors.when_must_belong_to_match"
msgstr ""
msgid "errors.concurrent_line_without_origin"
msgstr ""
msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr ""
msgid "errors.unknown" msgid "errors.unknown"
msgstr "" msgstr ""
@@ -468,4 +408,10 @@ msgid "runtime.unsupported_array_type"
msgstr "" msgstr ""
msgid "runtime.dialogue_balloon_missing_start_method" msgid "runtime.dialogue_balloon_missing_start_method"
msgstr ""
msgid "runtime.top_level_states_share_name"
msgstr ""
msgid "translation_plugin.character_name"
msgstr "" msgstr ""

View File

@@ -4,7 +4,7 @@ msgstr ""
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Veydzher\n" "Language-Team: \n"
"Language: uk\n" "Language: uk\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -20,8 +20,11 @@ msgstr "Відкрити файл"
msgid "open.open" msgid "open.open"
msgstr "Відкрити..." msgstr "Відкрити..."
msgid "open.quick_open"
msgstr "Швидко відкрити..."
msgid "open.no_recent_files" msgid "open.no_recent_files"
msgstr "Немає недавніх файлів" msgstr "Жодних недавніх файлів"
msgid "open.clear_recent_files" msgid "open.clear_recent_files"
msgstr "Очистити недавні файли" msgstr "Очистити недавні файли"
@@ -30,13 +33,16 @@ msgid "save_all_files"
msgstr "Зберегти всі файли" msgstr "Зберегти всі файли"
msgid "find_in_files" msgid "find_in_files"
msgstr "Знайти у файліх..." msgstr "Знайти у файлах..."
msgid "test_dialogue" msgid "test_dialogue"
msgstr "Тестувати діалог" msgstr "Протестувати діалог з початку файлу"
msgid "test_dialogue_from_line"
msgstr "Протестувати діалог з поточного рядка"
msgid "search_for_text" msgid "search_for_text"
msgstr "Шукати текст" msgstr "Пошук тексту"
msgid "insert" msgid "insert"
msgstr "Вставити" msgstr "Вставити"
@@ -44,9 +50,6 @@ msgstr "Вставити"
msgid "translations" msgid "translations"
msgstr "Переклади" msgstr "Переклади"
msgid "settings"
msgstr "Налаштування"
msgid "sponsor" msgid "sponsor"
msgstr "Спонсор" msgstr "Спонсор"
@@ -84,7 +87,7 @@ msgid "insert.response"
msgstr "Відповідь" msgstr "Відповідь"
msgid "insert.random_lines" msgid "insert.random_lines"
msgstr "Випадковий рядок" msgstr "Випадкові рядки"
msgid "insert.random_text" msgid "insert.random_text"
msgstr "Випадковий текст" msgstr "Випадковий текст"
@@ -93,7 +96,7 @@ msgid "insert.actions"
msgstr "Дії" msgstr "Дії"
msgid "insert.jump" msgid "insert.jump"
msgstr "Перехід до заголовку" msgstr "Перейти до заголовку"
msgid "insert.end_dialogue" msgid "insert.end_dialogue"
msgstr "Кінець діалогу" msgstr "Кінець діалогу"
@@ -108,10 +111,10 @@ msgid "save_to_csv"
msgstr "Зберегти рядки в CSV..." msgstr "Зберегти рядки в CSV..."
msgid "import_from_csv" msgid "import_from_csv"
msgstr "Імпортувати зміни в рядках з CSV..." msgstr "Імпортувати зміни рядків з CSV..."
msgid "confirm_close" msgid "confirm_close"
msgstr "Зберегти зміни до '{path}'?" msgstr "Зберегти зміни до «{path}»?"
msgid "confirm_close.save" msgid "confirm_close.save"
msgstr "Зберегти зміни" msgstr "Зберегти зміни"
@@ -138,85 +141,7 @@ msgid "buffer.copy_file_path"
msgstr "Копіювати шлях файлу" msgstr "Копіювати шлях файлу"
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "Показати у системі файлів" msgstr "Показати у файловій системі"
msgid "settings.invalid_test_scene"
msgstr "«{path}» не розширює BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Повернутися до стандартної тестової сцени"
msgid "settings.default_balloon_hint"
msgstr "Користувацьке діалогове вікно для використання під час виклику «DialogueManager.show_balloon()»"
msgid "settings.revert_to_default_balloon"
msgstr "Повернутися до стандартного діалогового вікна"
msgid "settings.default_balloon_path"
msgstr "<приклад діалогового вікна>"
msgid "settings.autoload"
msgstr "Авто. завантаження"
msgid "settings.path"
msgstr "Шлях"
msgid "settings.new_template"
msgstr "Нові файли діалогів починатимуться з тексту шаблону"
msgid "settings.missing_keys"
msgstr "Вважати відсутні ключі перекладу як помилками"
msgid "settings.missing_keys_hint"
msgstr "Якщо ви використовуєте статичні ключі перекладу, увімкнення цього параметра допоможе вам знайти рядки, до яких ви ще не додали ключ."
msgid "settings.characters_translations"
msgstr "Експорт імен персонажів у файлах перекладу"
msgid "settings.wrap_long_lines"
msgstr "Переносити довгі рядки"
msgid "settings.include_failed_responses"
msgstr "Включити відповіді з невдалими умовами"
msgid "settings.ignore_missing_state_values"
msgstr "Пропускати помилки пропущених значень стану (не рекомендується)"
msgid "settings.custom_test_scene"
msgstr "Користувацька тестова сцена (повинна розширювати BaseDialogueTestScene)"
msgid "settings.default_csv_locale"
msgstr "Стандартна мова CSV"
msgid "settings.states_shortcuts"
msgstr "Скорочення станів"
msgid "settings.states_message"
msgstr "Якщо автозавантаження увімкнено, ви можете звертатися до його властивостей і методів без необхідності використовувати його назву."
msgid "settings.states_hint"
msgstr "тобто, замість «ЯкийсьСтан.якась_властивість» ви можете просто використовувати «якусь_властивість»"
msgid "settings.recompile_warning"
msgstr "Зміна цих параметрів призведе до перекомпіляції усіх діалогів. Змінюйте їх, тільки якщо ви знаєте, що робите."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Створити діалогову лінію для відповідей дочірнього елемента з іменами персонажів"
msgid "settings.open_in_external_editor"
msgstr "Відкрити файли діалогів у зовнішньому редакторі"
msgid "settings.external_editor_warning"
msgstr "Примітка: Підсвічування синтаксису та детальна перевірка помилок не підтримуються у зовнішніх редакторах."
msgid "settings.include_characters_in_translations"
msgstr "Включати імена персонажів до експорту перекладу"
msgid "settings.include_notes_in_translations"
msgstr "Включати примітки (## коментарі) до експорту перекладу"
msgid "settings.check_for_updates"
msgstr "Перевірити наявність оновлень"
msgid "n_of_n" msgid "n_of_n"
msgstr "{index} з {total}" msgstr "{index} з {total}"
@@ -234,7 +159,7 @@ msgid "search.replace_placeholder"
msgstr "Текст для заміни" msgstr "Текст для заміни"
msgid "search.replace_selected" msgid "search.replace_selected"
msgstr "Замінити виділене" msgstr "Замінити вибране"
msgid "search.previous" msgid "search.previous"
msgstr "Назад" msgstr "Назад"
@@ -267,7 +192,7 @@ msgid "errors.key_not_found"
msgstr "Ключ «{key}» не знайдено." msgstr "Ключ «{key}» не знайдено."
msgid "errors.line_and_message" msgid "errors.line_and_message"
msgstr "Помилка на {line}, {column}: {message}" msgstr "Помилка в {line}, {column}: {message}"
msgid "errors_in_script" msgid "errors_in_script"
msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз." msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз."
@@ -285,16 +210,13 @@ msgid "errors.duplicate_import"
msgstr "Дублювання назви імпорту." msgstr "Дублювання назви імпорту."
msgid "errors.unknown_using" msgid "errors.unknown_using"
msgstr "Невідоме автозавантаження в операторі використання." msgstr "Невідоме автозавантаження в операторі «using»."
msgid "errors.empty_title" msgid "errors.empty_title"
msgstr "Заголовки не можуть бути порожніми." msgstr "Заголовки не можуть бути порожніми."
msgid "errors.duplicate_title" msgid "errors.duplicate_title"
msgstr "З такою назвою уже є заголовок." msgstr "Заголовок з такою назвою уже є."
msgid "errors.nested_title"
msgstr "Заголовки не повинні мати відступів."
msgid "errors.invalid_title_string" msgid "errors.invalid_title_string"
msgstr "Заголовки можуть містити лише алфавітно-цифрові символи та цифри." msgstr "Заголовки можуть містити лише алфавітно-цифрові символи та цифри."
@@ -306,7 +228,7 @@ msgid "errors.unknown_title"
msgstr "Невідомий заголовок." msgstr "Невідомий заголовок."
msgid "errors.jump_to_invalid_title" msgid "errors.jump_to_invalid_title"
msgstr "Цей перехід вказує на недійсну назву." msgstr "Цей перехід вказує на недійсний заголовок."
msgid "errors.title_has_no_content" msgid "errors.title_has_no_content"
msgstr "Цей заголовок не має змісту. Можливо, варто змінити його на «=> END»." msgstr "Цей заголовок не має змісту. Можливо, варто змінити його на «=> END»."
@@ -318,7 +240,7 @@ msgid "errors.unexpected_condition"
msgstr "Несподівана умова." msgstr "Несподівана умова."
msgid "errors.duplicate_id" msgid "errors.duplicate_id"
msgstr "Цей ідентифікатор вже на іншому рядку." msgstr "Цей ідентифікатор уже є на іншому рядку."
msgid "errors.missing_id" msgid "errors.missing_id"
msgstr "У цьому рядку відсутній ідентифікатор." msgstr "У цьому рядку відсутній ідентифікатор."
@@ -383,6 +305,21 @@ msgstr "Недійсний індекс."
msgid "errors.unexpected_assignment" msgid "errors.unexpected_assignment"
msgstr "Несподіване призначення." msgstr "Несподіване призначення."
msgid "errors.expected_when_or_else"
msgstr "Очікувався випадок «when» або «else»."
msgid "errors.only_one_else_allowed"
msgstr "Для кожного «match» допускається лише один випадок «else»."
msgid "errors.when_must_belong_to_match"
msgstr "Оператори «when» можуть з’являтися лише як дочірні операторів «match»."
msgid "errors.concurrent_line_without_origin"
msgstr "Паралельні рядки потребують початкового рядка, який не починається з «|»."
msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr "У паралельних діалогових рядках не допускаються Goto посилання."
msgid "errors.unknown" msgid "errors.unknown"
msgstr "Невідомий синтаксис." msgstr "Невідомий синтаксис."
@@ -399,7 +336,7 @@ msgid "update.download_update"
msgstr "Завантажити оновлення" msgstr "Завантажити оновлення"
msgid "update.needs_reload" msgid "update.needs_reload"
msgstr "Щоб встановити оновлення, проєкт потрібно перезавантажити." msgstr "Щоб установити оновлення, проєкт потрібно перезавантажити."
msgid "update.reload_ok_button" msgid "update.reload_ok_button"
msgstr "Перезавантажити проєкт" msgstr "Перезавантажити проєкт"
@@ -411,10 +348,10 @@ msgid "update.reload_project"
msgstr "Перезавантажити проєкт" msgstr "Перезавантажити проєкт"
msgid "update.release_notes" msgid "update.release_notes"
msgstr "Читати примітки оновлення" msgstr "Читати зміни оновлення"
msgid "update.success" msgid "update.success"
msgstr "Менеджер діалогів тепер має версію {version}." msgstr "Dialogue Manager тепер з версією {version}."
msgid "update.failed" msgid "update.failed"
msgstr "Виникла проблема із завантаженням оновлення." msgstr "Виникла проблема із завантаженням оновлення."
@@ -441,22 +378,22 @@ msgid "runtime.array_index_out_of_bounds"
msgstr "Індекс {index} виходить за межі масиву «{array}»." msgstr "Індекс {index} виходить за межі масиву «{array}»."
msgid "runtime.left_hand_size_cannot_be_assigned_to" msgid "runtime.left_hand_size_cannot_be_assigned_to"
msgstr "Ліва частина виразу не може бути присвоєна." msgstr "Ліва частина виразу не може бути призначена."
msgid "runtime.key_not_found" msgid "runtime.key_not_found"
msgstr "Ключ «{key}» у словнику «{dictionary}»" msgstr "Ключ «{key}» не знайдено у словнику «{dictionary}»"
msgid "runtime.property_not_found" msgid "runtime.property_not_found"
msgstr "«{property}» не є властивістю для жодного стану гри ({states})." msgstr "«{property}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.property_not_found_missing_export" msgid "runtime.property_not_found_missing_export"
msgstr "«{property}» не є властивістю для жодного стану гри ({states}). Можливо, вам слід додати декоратор [Export]." msgstr "«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.method_not_found" msgid "runtime.method_not_found"
msgstr "«{method}» не є методом на жодному зі станів гри ({states})" msgstr "Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.signal_not_found" msgid "runtime.signal_not_found"
msgstr "«{signal_name}» не є сигналом на жодному зі станів гри ({states})" msgstr "Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.method_not_callable" msgid "runtime.method_not_callable"
msgstr "«{method}» не є методом, який можна викликати в «{object}»" msgstr "«{method}» не є методом, який можна викликати в «{object}»"
@@ -465,16 +402,22 @@ msgid "runtime.unknown_operator"
msgstr "Невідомий оператор." msgstr "Невідомий оператор."
msgid "runtime.unknown_autoload" msgid "runtime.unknown_autoload"
msgstr "«{autoload}» не є дійсним автозавантаженням." msgstr "Схоже, «{autoload}» не є дійсним автозавантаженням."
msgid "runtime.something_went_wrong" msgid "runtime.something_went_wrong"
msgstr "Щось пішло не так." msgstr "Щось пішло не так."
msgid "runtime.expected_n_got_n_args" msgid "runtime.expected_n_got_n_args"
msgstr "«{method}» було викликано з аргументами «{received}», але він має лише «{expected}»." msgstr "«{method}» було викликано з аргументами «{received}», але воно має лише «{expected}»."
msgid "runtime.unsupported_array_type" msgid "runtime.unsupported_array_type"
msgstr "Array[{type}] не підтримується в мутаціях. Натомість використовуйте Array як тип." msgstr "Array[{type}] не підтримується у модифікаціях. Натомість використовуйте Array як тип."
msgid "runtime.dialogue_balloon_missing_start_method" msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "У вашому діалоговому вікні відсутній метод «start» або «Start»." msgstr "У вашій кулі діалогу відсутній метод «start» або «Start»."
msgid "runtime.top_level_states_share_name"
msgstr "Кілька станів верхнього рівня ({states}) мають спільну назву методу/властивості/сигналу «{key}». Для діалогу доступний лише перший випадок."
msgid "translation_plugin.character_name"
msgstr "Ім’я персонажа"

View File

@@ -44,9 +44,6 @@ msgstr "插入"
msgid "translations" msgid "translations"
msgstr "翻译" msgstr "翻译"
msgid "settings"
msgstr "设置"
msgid "show_support" msgid "show_support"
msgstr "支持 Dialogue Manager" msgstr "支持 Dialogue Manager"
@@ -137,69 +134,6 @@ msgstr "复制文件路径"
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "在 Godot 侧边栏中显示" msgstr "在 Godot 侧边栏中显示"
msgid "settings.revert_to_default_test_scene"
msgstr "重置测试场景设定"
msgid "settings.default_balloon_hint"
msgstr "设置调用 \"DialogueManager.show_balloon()\" 时使用的对话框"
msgid "settings.autoload"
msgstr "Autoload"
msgid "settings.path"
msgstr "路径"
msgid "settings.new_template"
msgstr "新建文件时自动插入模板"
msgid "settings.missing_keys"
msgstr "将翻译键缺失视为错误"
msgid "settings.missing_keys_hint"
msgstr "如果你使用静态键,这将会帮助你寻找未添加至翻译文件的键。"
msgid "settings.characters_translations"
msgstr "在翻译文件中导出角色名"
msgid "settings.wrap_long_lines"
msgstr "文本编辑器自动换行"
msgid "settings.include_failed_responses"
msgstr "在判断条件失败时仍显示回复选项"
msgid "settings.ignore_missing_state_values"
msgstr "忽略全局变量缺失错误(不建议)"
msgid "settings.custom_test_scene"
msgstr "自定义测试场景必须继承自BaseDialogueTestScene"
msgid "settings.default_csv_locale"
msgstr "默认 CSV 区域格式"
msgid "settings.states_shortcuts"
msgstr "全局变量映射"
msgid "settings.states_message"
msgstr "当一个 Autoload 在这里被勾选,他的所有成员会被映射为全局变量。"
msgid "settings.states_hint"
msgstr "比如当你开启对于“Foo”的映射时你可以将“Foo.bar”简写成“bar”。"
msgid "settings.recompile_warning"
msgstr "更改这些选项会强制重新编译所有的对话框,当你清楚在做什么的时候更改。"
msgid "settings.create_lines_for_responses_with_characters"
msgstr "回复项带角色名时(- char: response会自动生成为选择后的下一句对话"
msgid "settings.include_characters_in_translations"
msgstr "导出 CSV 时包括角色名"
msgid "settings.include_notes_in_translations"
msgstr "导出 CSV 时包括注释(## comments"
msgid "settings.check_for_updates"
msgstr "检查升级"
msgid "n_of_n" msgid "n_of_n"
msgstr "第{index}个,共{total}个" msgstr "第{index}个,共{total}个"
@@ -272,9 +206,6 @@ msgstr "标题名不能为空。"
msgid "errors.duplicate_title" msgid "errors.duplicate_title"
msgstr "标题名不能重复。" msgstr "标题名不能重复。"
msgid "errors.nested_title"
msgstr "标题不能嵌套。"
msgid "errors.invalid_title_string" msgid "errors.invalid_title_string"
msgstr "标题名无效。" msgstr "标题名无效。"

View File

@@ -44,9 +44,6 @@ msgstr "插入"
msgid "translations" msgid "translations"
msgstr "翻譯" msgstr "翻譯"
msgid "settings"
msgstr "設定"
msgid "show_support" msgid "show_support"
msgstr "支援 Dialogue Manager" msgstr "支援 Dialogue Manager"
@@ -137,69 +134,6 @@ msgstr "複製檔案位置"
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "在 Godot 側邊欄中顯示" msgstr "在 Godot 側邊欄中顯示"
msgid "settings.revert_to_default_test_scene"
msgstr "重置測試場景設定"
msgid "settings.default_balloon_hint"
msgstr "設置使用 \"DialogueManager.show_balloon()\" 时的对话框"
msgid "settings.autoload"
msgstr "Autoload"
msgid "settings.path"
msgstr "路徑"
msgid "settings.new_template"
msgstr "新建檔案時自動插入模板"
msgid "settings.missing_keys"
msgstr "將翻譯鍵缺失視爲錯誤"
msgid "settings.missing_keys_hint"
msgstr "如果你使用靜態鍵,這將會幫助你尋找未添加至翻譯檔案的鍵。"
msgid "settings.wrap_long_lines"
msgstr "自動折行"
msgid "settings.characters_translations"
msgstr "在翻譯檔案中匯出角色名。"
msgid "settings.include_failed_responses"
msgstr "在判斷條件失敗時仍顯示回復選項"
msgid "settings.ignore_missing_state_values"
msgstr "忽略全局變量缺失錯誤(不建議)"
msgid "settings.custom_test_scene"
msgstr "自訂測試場景必須繼承自BaseDialogueTestScene"
msgid "settings.default_csv_locale"
msgstr "預設 CSV 區域格式"
msgid "settings.states_shortcuts"
msgstr "全局變量映射"
msgid "settings.states_message"
msgstr "當一個 Autoload 在這裏被勾選,他的所有成員會被映射爲全局變量。"
msgid "settings.states_hint"
msgstr "比如當你開啓對於“Foo”的映射時你可以將“Foo.bar”簡寫成“bar”。"
msgid "settings.recompile_warning"
msgstr "更改這些選項會強制重新編譯所有的對話框,當你清楚在做什麼的時候更改。"
msgid "settings.create_lines_for_responses_with_characters"
msgstr "回覆項目帶角色名稱時(- char: response會自動產生為選擇後的下一句對話"
msgid "settings.include_characters_in_translations"
msgstr "匯出 CSV 時包含角色名"
msgid "settings.include_notes_in_translations"
msgstr "匯出 CSV 時包括註解(## comments"
msgid "settings.check_for_updates"
msgstr "檢查升級"
msgid "n_of_n" msgid "n_of_n"
msgstr "第{index}個,共{total}個" msgstr "第{index}個,共{total}個"
@@ -272,9 +206,6 @@ msgstr "標題名不能爲空。"
msgid "errors.duplicate_title" msgid "errors.duplicate_title"
msgstr "標題名不能重複。" msgstr "標題名不能重複。"
msgid "errors.nested_title"
msgstr "標題不能嵌套。"
msgid "errors.invalid_title_string" msgid "errors.invalid_title_string"
msgstr "標題名無效。" msgstr "標題名無效。"

View File

@@ -1,7 +1,7 @@
[plugin] [plugin]
name="Dialogue Manager" name="Dialogue Manager"
description="A simple but powerful branching dialogue system" description="A powerful nonlinear dialogue system"
author="Nathan Hoad" author="Nathan Hoad"
version="2.41.4" version="3.4.0"
script="plugin.gd" script="plugin.gd"

View File

@@ -0,0 +1 @@
uid://hrny2utekhei

Some files were not shown because too many files have changed in this diff Show More