Envoi sur GitHub du projet updated

This commit is contained in:
Maurane
2026-01-07 12:47:48 +01:00
parent 34e887afd2
commit 3f1863f0c1
434 changed files with 16021 additions and 5685 deletions
+239 -27
View File
@@ -1,6 +1,7 @@
using Godot;
using Godot.Collections;
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -8,6 +9,14 @@ using System.Threading.Tasks;
namespace DialogueManagerRuntime
{
public enum MutationBehaviour
{
Wait,
DoNotWait,
Skip
}
public enum TranslationSource
{
None,
@@ -16,13 +25,15 @@ namespace DialogueManagerRuntime
PO
}
public partial class DialogueManager : Node
public partial class DialogueManager : RefCounted
{
public delegate void DialogueStartedEventHandler(Resource dialogueResource);
public delegate void PassedTitleEventHandler(string title);
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
public delegate void MutatedEventHandler(Dictionary mutation);
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
public static DialogueStartedEventHandler? DialogueStarted;
public static PassedTitleEventHandler? PassedTitle;
public static GotDialogueEventHandler? GotDialogue;
public static MutatedEventHandler? Mutated;
@@ -38,6 +49,7 @@ namespace DialogueManagerRuntime
if (instance == null)
{
instance = Engine.GetSingleton("DialogueManager");
instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource)));
}
return instance;
}
@@ -86,11 +98,6 @@ namespace DialogueManagerRuntime
instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource)));
}
public void Prepare()
{
Prepare(Instance);
}
public static async Task<GodotObject> GetSingleton()
{
@@ -121,11 +128,11 @@ namespace DialogueManagerRuntime
return (Resource)Instance.Call("create_resource_from_text", text);
}
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null, MutationBehaviour mutation_behaviour = MutationBehaviour.Wait)
{
var instance = (Node)Instance.Call("_bridge_get_new_instance");
Prepare(instance);
instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>());
instance.Call("_bridge_get_next_dialogue_line", dialogueResource, key, extraGameStates ?? new Array<Variant>(), (int)mutation_behaviour);
var result = await instance.ToSignal(instance, "bridge_get_next_dialogue_line_completed");
instance.QueueFree();
@@ -134,6 +141,19 @@ namespace DialogueManagerRuntime
return new DialogueLine((RefCounted)result[0]);
}
public static async Task<DialogueLine?> GetLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
var instance = (Node)Instance.Call("_bridge_get_new_instance");
Prepare(instance);
instance.Call("_bridge_get_line", dialogueResource, key, extraGameStates ?? new Array<Variant>());
var result = await instance.ToSignal(instance, "bridge_get_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)
{
@@ -163,6 +183,18 @@ namespace DialogueManagerRuntime
}
public static Array<string> StaticIdToLineIds(Resource dialogueResource, string staticId)
{
return (Array<string>)Instance.Call("static_id_to_line_ids", dialogueResource, staticId);
}
public static string StaticIdToLineId(Resource dialogueResource, string staticId)
{
return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId);
}
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
{
Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
@@ -170,16 +202,167 @@ namespace DialogueManagerRuntime
}
public bool ThingHasMethod(GodotObject thing, string method)
public static Array<Dictionary> GetMembersForAutoload(Script script)
{
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);
return info != null;
Array<Dictionary> members = new Array<Dictionary>();
string typeName = script.ResourcePath.GetFile().GetBaseName();
var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName);
foreach (var matchingType in matchingTypes)
{
var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var memberInfo in memberInfos)
{
string type;
switch (memberInfo.MemberType)
{
case MemberTypes.Field:
FieldInfo fieldInfo = memberInfo as FieldInfo;
if (fieldInfo.FieldType.ToString().Contains("EventHandler"))
{
type = "signal";
}
else if (fieldInfo.IsLiteral)
{
type = "constant";
}
else
{
type = "property";
}
break;
case MemberTypes.Method:
type = "method";
break;
case MemberTypes.NestedType:
type = "constant";
break;
default:
continue;
}
members.Add(new Dictionary() {
{ "name", memberInfo.Name },
{ "type", type }
});
}
}
return members;
}
public bool ThingHasConstant(GodotObject thing, string property)
{
var memberInfos = thing.GetType().GetMember(property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
return memberInfos.Length > 0;
}
public Variant ResolveThingConstant(GodotObject thing, string property)
{
var memberInfos = thing.GetType().GetMember(property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var memberInfo in memberInfos)
{
if (memberInfo != null)
{
try
{
switch (memberInfo.MemberType)
{
case MemberTypes.Field:
return convertValueToVariant((memberInfo as FieldInfo).GetValue(thing));
case MemberTypes.Property:
return convertValueToVariant((memberInfo as PropertyInfo).GetValue(thing));
case MemberTypes.NestedType:
var type = thing.GetType().GetNestedType(property);
if (type.IsEnum)
{
return GetEnumAsDictionary(type);
}
break;
default:
continue;
}
}
catch (Exception e)
{
throw new Exception($"{property} is not supported by Variant.");
}
}
}
throw new Exception($"{property} is not a public constant on {thing}");
}
Dictionary GetEnumAsDictionary(Type enumType)
{
Dictionary dictionary = new Dictionary();
foreach (var value in enumType.GetEnumValuesAsUnderlyingType())
{
var key = enumType.GetEnumName(value);
if (key != null)
{
dictionary.Add(key, convertValueToVariant(value));
}
}
return dictionary;
}
Variant convertValueToVariant(object value)
{
Type rawType = value.GetType();
if (rawType.IsEnum)
{
var values = GetEnumAsDictionary(rawType);
value = values[value.ToString()];
}
return value switch
{
Variant v => v,
int v => Variant.From((long)v),
float v => Variant.From((double)v),
System.String v => Variant.From((string)v),
_ => Variant.From(value)
};
}
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().Where(p => !p.HasDefaultValue).Count())
{
return true;
}
}
return false;
}
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args)
{
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);
MethodInfo? info = null;
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var methodInfo in methodInfos)
{
if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count())
{
info = methodInfo;
}
}
if (info == null) return;
@@ -216,18 +399,15 @@ namespace DialogueManagerRuntime
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<>))
try
{
var resultProperty = taskType.GetProperty("Result");
var taskResultValue = resultProperty.GetValue(taskResult);
EmitSignal(SignalName.Resolved, (Variant)taskResultValue);
Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult);
EmitSignal(SignalName.Resolved, value);
}
else
catch (Exception)
{
EmitSignal(SignalName.Resolved, null);
EmitSignal(SignalName.Resolved);
}
}
else
@@ -236,6 +416,12 @@ namespace DialogueManagerRuntime
}
}
#nullable enable
public static string GetErrorMessage(int error)
{
return (string)Instance.Call("_bridge_get_error_message", error);
}
}
@@ -295,12 +481,6 @@ namespace DialogueManagerRuntime
get => time;
}
private Dictionary pauses = new Dictionary();
public Dictionary Pauses
{
get => pauses;
}
private Dictionary speeds = new Dictionary();
public Dictionary Speeds
{
@@ -313,6 +493,12 @@ namespace DialogueManagerRuntime
get => inline_mutations;
}
private Array<DialogueLine> concurrent_lines = new Array<DialogueLine>();
public Array<DialogueLine> ConcurrentLines
{
get => concurrent_lines;
}
private Array<Variant> extra_game_states = new Array<Variant>();
public Array<Variant> ExtraGameStates
{
@@ -327,17 +513,22 @@ namespace DialogueManagerRuntime
public DialogueLine(RefCounted data)
{
id = (string)data.Get("id");
type = (string)data.Get("type");
next_id = (string)data.Get("next_id");
character = (string)data.Get("character");
text = (string)data.Get("text");
translation_key = (string)data.Get("translation_key");
pauses = (Dictionary)data.Get("pauses");
speeds = (Dictionary)data.Get("speeds");
inline_mutations = (Array<Godot.Collections.Array>)data.Get("inline_mutations");
time = (string)data.Get("time");
tags = (Array<string>)data.Get("tags");
foreach (var concurrent_line_data in (Array<RefCounted>)data.Get("concurrent_lines"))
{
concurrent_lines.Add(new DialogueLine(concurrent_line_data));
}
foreach (var response in (Array<RefCounted>)data.Get("responses"))
{
responses.Add(new DialogueResponse(response));
@@ -345,6 +536,20 @@ namespace DialogueManagerRuntime
}
public bool HasTag(string tagName)
{
string wrapped = $"{tagName}=";
foreach (var tag in tags)
{
if (tag.StartsWith(wrapped))
{
return true;
}
}
return false;
}
public string GetTagValue(string tagName)
{
string wrapped = $"{tagName}=";
@@ -389,6 +594,13 @@ namespace DialogueManagerRuntime
set => is_allowed = value;
}
private string condition_as_text = "";
public string ConditionAsText
{
get => condition_as_text;
set => condition_as_text = value;
}
private string text = "";
public string Text
{
@@ -0,0 +1 @@
uid://c4c5lsrwy3opj
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cnm67htuohhlo"
path="res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/dialogue_manager/assets/banner.png"
dest_files=["res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://dsgpnyqg6cprg
@@ -0,0 +1,161 @@
## 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 express as the raw text that was given.
var expression_text: String = ""
## The next sequential line to go to after this line.
var next_id: String = ""
## The next line to go to after this line if it is unknown and compile time.
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
if not expression_text.is_empty():
d.condition_as_text = expression_text
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
@@ -0,0 +1 @@
uid://dg8j5hudp4210
@@ -0,0 +1,57 @@
## 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
## Extract a mutation from a string.
static func extract_mutation(text: String) -> Dictionary:
var compilation: DMCompilation = DMCompilation.new()
return compilation.extract_mutation(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
@@ -0,0 +1 @@
uid://chtfdmr0cqtp4
@@ -0,0 +1,50 @@
## 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_NULL_COALESCE: RegEx.create_from_string("^\\?\\."),
DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."),
DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"),
DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),
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)")
}
@@ -0,0 +1 @@
uid://d3tvcrnicjibp
@@ -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 = ""
@@ -0,0 +1 @@
uid://dmk74tknimqvg
@@ -0,0 +1,529 @@
## A class for parsing a condition/mutation expression for use with the [DMCompiler].
class_name DMExpressionParser extends RefCounted
var include_comments: bool = false
# 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].i,
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(tree: Array, error: int, index: int) -> Array:
tree.insert(0, {
type = DMConstants.TOKEN_ERROR,
value = error,
i = index
})
return tree
# Convert a list of tokens into an abstract syntax tree.
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(tree, error, error_token.index), tokens]
match token.type:
DMConstants.TOKEN_COMMENT:
if include_comments:
tree.append({
type = DMConstants.TOKEN_COMMENT,
value = token.value,
i = token.index
})
DMConstants.TOKEN_FUNCTION:
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE)
if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR:
return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), 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(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
var args = _tokens_to_list(sub_tree[0])
if args.size() != 1:
return [_build_token_tree_error(tree, 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(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
var t = sub_tree[0]
for i in range(0, t.size() - 2):
# 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(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens]
var type = DMConstants.TOKEN_ARRAY
var value = _tokens_to_list(sub_tree[0])
# 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(tree, sub_tree[0][0].value, sub_tree[0][0].i), 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(tree, 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, \
DMConstants.TOKEN_NULL_COALESCE:
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(tree, 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].i if tokens.size() > 0 else 0
return [_build_token_tree_error(tree, 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_DOT, \
DMConstants.TOKEN_NULL_COALESCE, \
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_COMMA:
unexpected_token_types = [
null,
DMConstants.TOKEN_COMMA,
DMConstants.TOKEN_COLON,
DMConstants.TOKEN_ASSIGNMENT,
DMConstants.TOKEN_OPERATOR,
DMConstants.TOKEN_AND_OR,
DMConstants.TOKEN_PARENS_CLOSE,
DMConstants.TOKEN_BRACE_CLOSE,
DMConstants.TOKEN_BRACKET_CLOSE,
DMConstants.TOKEN_DOT
]
DMConstants.TOKEN_COLON:
unexpected_token_types = [
DMConstants.TOKEN_COMMA,
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
@@ -0,0 +1 @@
uid://dbi4hbar8ubwu
@@ -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]
@@ -0,0 +1 @@
uid://llhl5pt47eoq
@@ -0,0 +1,160 @@
## Any data associated with inline dialogue BBCodes.
class_name DMResolvedLineData extends RefCounted
## The line's text
var text: String = ""
## 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
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"]:
args["value"] = DMCompiler.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":
var wait: Dictionary = DMCompiler.extract_mutation("do wait(%s)" % [args.get("value", "null")])
mutations.append([index, wait])
"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 ["/", "!", "$", ">"].has(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
@@ -0,0 +1 @@
uid://0k6q8kukq0qa
@@ -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
@@ -0,0 +1 @@
uid://cqai3ikuilqfq
@@ -0,0 +1,46 @@
## 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 = ""
## Is this a dialogue line that is the child of another dialogue line?
var is_nested_dialogue: bool = false
func _init(initial_id: String) -> void:
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",
"}"])
@@ -0,0 +1 @@
uid://dsu4i84dpif14
+195 -24
View File
@@ -1,5 +1,5 @@
@tool
extends CodeEdit
class_name DMCodeEdit extends CodeEdit
signal active_title_change(title: String)
@@ -7,10 +7,6 @@ signal error_clicked(line_number: int)
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`
var main_view
@@ -19,7 +15,7 @@ var theme_overrides: Dictionary:
set(value):
theme_overrides = value
syntax_highlighter = DialogueSyntaxHighlighter.new()
syntax_highlighter = DMSyntaxHighlighter.new()
# General UI
add_theme_color_override("font_color", theme_overrides.text_color)
@@ -57,6 +53,10 @@ var font_size: int:
var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s")
var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new()
var _autoloads: Dictionary[String, String] = {}
var _autoload_member_cache: Dictionary[String, Dictionary] = {}
func _ready() -> void:
# Add error gutter
@@ -67,7 +67,11 @@ func _ready() -> void:
if not has_comment_delimiter("#"):
add_comment_delimiter("#", "", true)
syntax_highlighter = DialogueSyntaxHighlighter.new()
syntax_highlighter = DMSyntaxHighlighter.new()
# Keep track of any autoloads
ProjectSettings.settings_changed.connect(_on_project_settings_changed)
_on_project_settings_changed()
func _gui_input(event: InputEvent) -> void:
@@ -96,6 +100,12 @@ func _gui_input(event: InputEvent) -> void:
"text_size_reset":
self.font_size = theme_overrides.font_size
get_viewport().set_input_as_handled()
"make_bold":
insert_bbcode("[b]", "[/b]")
get_viewport().set_input_as_handled()
"make_italic":
insert_bbcode("[i]", "[/i]")
get_viewport().set_input_as_handled()
elif event is InputEventMouse:
match event.as_text():
@@ -137,10 +147,20 @@ func _drop_data(at_position: Vector2, data) -> void:
if cursor.x > -1 and cursor.y > -1:
set_cursor(cursor)
remove_secondary_carets()
if has_method("insert_text"):
call("insert_text", "\"%s\"" % file, cursor.y, cursor.x)
var resource = load(file)
# If the dropped file is an audio stream then assume it's a voice reference
if is_instance_of(resource, AudioStream):
var current_voice_regex: RegEx = RegEx.create_from_string("\\[#voice=.+\\]")
var path: String = ResourceUID.call("path_to_uid", file) if ResourceUID.has_method("path_to_uid") else file
var line_text: String = get_line(cursor.y)
var voice_text: String = "[#voice=%s]" % [path]
if current_voice_regex.search(line_text):
set_line(cursor.y, current_voice_regex.replace(get_line(cursor.y), voice_text))
else:
insert_text(" " + voice_text, cursor.y, line_text.length())
# Other wise it's just a file reference
else:
call("insert_text_at_cursor", "\"%s\"" % file)
insert_text("\"%s\"" % file, cursor.y, cursor.x)
grab_focus()
@@ -148,6 +168,7 @@ func _request_code_completion(force: bool) -> void:
var cursor: Vector2 = get_cursor()
var current_line: String = get_line(cursor.y)
# Match jumps
if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")):
var prompt: String = current_line.split("=>")[1]
if prompt.begins_with("< "):
@@ -162,19 +183,19 @@ 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"))
# Get all titles, including those in imports
var parser: DialogueManagerParser = DialogueManagerParser.new()
parser.prepare(text, main_view.current_file_path, false)
for title in parser.titles:
if "/" in title:
for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path):
# Ignore any imported titles that aren't resolved to human readable.
if title.to_int() > 0:
continue
elif "/" in title:
var bits = title.split("/")
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"))
elif matches_prompt(prompt, title):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons"))
update_code_completion_options(true)
parser.free()
return
# Match character names
var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "")
if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]:
# Only show names starting with that character
@@ -182,9 +203,72 @@ func _request_code_completion(force: bool) -> void:
if names.size() > 0:
for name in names:
add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons"))
update_code_completion_options(true)
else:
cancel_code_completion()
# Match autoloads on mutation lines
for prefix in ["$>", "$>>", "do ", "do! ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]:
if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))):
var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3)
# Find the last couple of tokens
var possible_prompt: String = expression.reverse()
possible_prompt = possible_prompt.substr(0, possible_prompt.find(" "))
possible_prompt = possible_prompt.substr(0, possible_prompt.find("("))
possible_prompt = possible_prompt.reverse()
var segments: PackedStringArray = possible_prompt.split(".").slice(-2)
var auto_completes: Array[Dictionary] = []
# Autoloads and state shortcuts
if segments.size() == 1:
var prompt: String = segments[0]
for autoload in _autoloads.keys():
if matches_prompt(prompt, autoload):
auto_completes.append({
prompt = prompt,
text = autoload,
type = "script"
})
for autoload in get_state_shortcuts():
for member: Dictionary in get_members_for_autoload(autoload):
if matches_prompt(prompt, member.name):
auto_completes.append({
prompt = prompt,
text = member.name,
type = member.type
})
# Members of an autoload
elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "):
var prompt: String = segments[1]
for member: Dictionary in get_members_for_autoload(segments[0]):
if matches_prompt(prompt, member.name):
auto_completes.append({
prompt = prompt,
text = member.name,
type = member.type
})
auto_completes.sort_custom(func(a, b): return a.text < b.text)
for auto_complete in auto_completes:
var icon: Texture2D
var text: String = auto_complete.text
match auto_complete.type:
"script":
icon = get_theme_icon("Script", "EditorIcons")
"property":
icon = get_theme_icon("MemberProperty", "EditorIcons")
"method":
icon = get_theme_icon("MemberMethod", "EditorIcons")
text += "()"
"signal":
icon = get_theme_icon("MemberSignal", "EditorIcons")
"constant":
icon = get_theme_icon("MemberConstant", "EditorIcons")
var insert: String = text.substr(auto_complete.prompt.length())
add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon)
update_code_completion_options(true)
if get_code_completion_options().size() == 0:
cancel_code_completion()
func _filter_code_completion_candidates(candidates: Array) -> Array:
@@ -196,17 +280,21 @@ func _confirm_code_completion(replace: bool) -> void:
var completion = get_code_completion_option(get_code_completion_selected_index())
begin_complex_operation()
# Delete any part of the text that we've already typed
for i in range(0, completion.display_text.length() - completion.insert_text.length()):
backspace()
if completion.insert_text.length() > 0:
for i in range(0, completion.display_text.length() - completion.insert_text.length()):
backspace()
# Insert the whole match
insert_text_at_caret(completion.display_text)
end_complex_operation()
if completion.display_text.ends_with("()"):
set_cursor(get_cursor() - Vector2.RIGHT)
# Close the autocomplete menu on the next tick
call_deferred("cancel_code_completion")
### Helpers
#region Helpers
# Get the current caret as a Vector2
@@ -225,6 +313,69 @@ func matches_prompt(prompt: String, matcher: String) -> bool:
return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower())
func get_state_shortcuts() -> PackedStringArray:
# Get any shortcuts defined in settings
var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, [])
# Check for "using" clauses
for line: String in text.split("\n"):
var found: RegExMatch = compiler_regex.USING_REGEX.search(line)
if found:
shortcuts.append(found.strings[found.names.state])
# Check for any other script sources
for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []):
shortcuts.append(extra_script_source)
return shortcuts
func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]:
# Debounce method list lookups
if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 10000:
return _autoload_member_cache.get(autoload_name).get("members")
if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return []
var autoload = load(_autoloads.get(autoload_name, autoload_name))
var script: Script = autoload if autoload is Script else autoload.get_script()
if not is_instance_valid(script): return []
var members: Array[Dictionary] = []
if script.resource_path.ends_with(".gd"):
for m: Dictionary in script.get_script_method_list():
if not m.name.begins_with("@"):
members.append({
name = m.name,
type = "method"
})
for m: Dictionary in script.get_script_property_list():
members.append({
name = m.name,
type = "property"
})
for m: Dictionary in script.get_script_signal_list():
members.append({
name = m.name,
type = "signal"
})
for c: String in script.get_script_constant_map():
members.append({
name = c,
type = "constant"
})
elif script.resource_path.ends_with(".cs"):
var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new()
for m: Dictionary in dotnet.GetMembersForAutoload(script):
members.append(m)
_autoload_member_cache[autoload_name] = {
at = Time.get_ticks_msec(),
members = members
}
return members
## Get a list of titles from the current text
func get_titles() -> PackedStringArray:
var titles = PackedStringArray([])
@@ -232,6 +383,7 @@ func get_titles() -> PackedStringArray:
for line in lines:
if line.strip_edges().begins_with("~ "):
titles.append(line.strip_edges().substr(2))
return titles
@@ -270,6 +422,11 @@ func get_character_names(beginning_with: String) -> PackedStringArray:
# Mark a line as an error or not
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:
set_line_background_color(line_number, theme_overrides.error_line_color)
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))
@@ -417,7 +574,18 @@ func move_line(offset: int) -> void:
scroll_vertical = starting_scroll + offset
### Signals
#endregion
#region Signals
func _on_project_settings_changed() -> void:
_autoloads = {}
var project = ConfigFile.new()
project.load("res://project.godot")
for autoload in project.get_section_keys("autoload"):
if autoload != "DialogueManager":
_autoloads[autoload] = project.get_value("autoload", autoload).substr(1)
func _on_code_edit_symbol_validate(symbol: String) -> void:
@@ -456,3 +624,6 @@ func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void:
var line_errors = errors.filter(func(error): return error.line_number == line)
if line_errors.size() > 0:
error_clicked.emit(line)
#endregion
@@ -0,0 +1 @@
uid://djeybvlb332mp
@@ -1,7 +1,7 @@
[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" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
[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" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"]
[sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"]
script = ExtResource("1_58cfo")
@@ -1,56 +1,21 @@
@tool
extends SyntaxHighlighter
class_name DMSyntaxHighlighter extends SyntaxHighlighter
const DialogueManagerParser = preload("./parser.gd")
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 regex: DMCompilerRegEx = DMCompilerRegEx.new()
var compilation: DMCompilation = DMCompilation.new()
var expression_parser = DMExpressionParser.new()
var cache: Dictionary = {}
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
dialogue_manager_parser.free()
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:
expression_parser.include_comments = true
var colors: Dictionary = {}
var text_edit: TextEdit = get_text_edit()
var text: String = text_edit.get_line(line)
@@ -63,323 +28,210 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
if text in cache:
return cache[text]
# Comments have to be removed to make the remaining processing easier.
# 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))
var theme: Dictionary = text_edit.theme_overrides
# Dialogues
var dialogue_matches: Array[RegExMatch] = regex_dialogue.search_all(text)
for dialogue_match in dialogue_matches:
if "random" in dialogue_match.names:
colors[dialogue_match.get_start("random")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("random")] = {"color": text_edit.theme_overrides.text_color}
if "response" in dialogue_match.names:
colors[dialogue_match.get_start("response")] = {"color": text_edit.theme_overrides.symbols_color}
colors[dialogue_match.get_end("response")] = {"color": text_edit.theme_overrides.text_color}
if "character" in dialogue_match.names:
colors[dialogue_match.get_start("character")] = {"color": text_edit.theme_overrides.members_color}
colors[dialogue_match.get_end("character")] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_dialogue_syntax_highlighting(dialogue_match.get_start("dialogue"), dialogue_match.get_string("dialogue")), true)
var index: int = 0
# Title lines
if dialogue_manager_parser.is_title_line(text):
var title_matches: Array[RegExMatch] = regex_titles.search_all(text)
for title_match in title_matches:
colors[title_match.get_start("title")] = {"color": text_edit.theme_overrides.titles_color}
match DMCompiler.get_line_type(text):
DMConstants.TYPE_USING:
colors[index] = { color = theme.conditions_color }
colors[index + "using ".length()] = { color = theme.text_color }
# Import lines
var import_matches: Array[RegExMatch] = dialogue_manager_parser.IMPORT_REGEX.search_all(text)
for import_match in import_matches:
colors[import_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[import_match.get_start("path") - 1] = {"color": text_edit.theme_overrides.strings_color}
colors[import_match.get_end("path") + 1] = {"color": text_edit.theme_overrides.conditions_color}
colors[import_match.get_start("prefix")] = {"color": text_edit.theme_overrides.members_color}
colors[import_match.get_end("prefix")] = {"color": text_edit.theme_overrides.conditions_color}
DMConstants.TYPE_IMPORT:
colors[index] = { color = theme.conditions_color }
var import: RegExMatch = regex.IMPORT_REGEX.search(text)
if import:
colors[index + import.get_start("path") - 1] = { color = theme.strings_color }
colors[index + import.get_end("path") + 1] = { color = theme.conditions_color }
colors[index + import.get_start("prefix")] = { color = theme.text_color }
# Using clauses
var using_matches: Array[RegExMatch] = dialogue_manager_parser.USING_REGEX.search_all(text)
for using_match in using_matches:
colors[using_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[using_match.get_start("state") - 1] = {"color": text_edit.theme_overrides.text_color}
DMConstants.TYPE_COMMENT:
colors[index] = { color = theme.comments_color }
# Condition keywords and expressions
var condition_matches: Array[RegExMatch] = regex_condition.search_all(text)
for condition_match in condition_matches:
colors[condition_match.get_start(0)] = {"color": text_edit.theme_overrides.conditions_color}
colors[condition_match.get_end(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_expression_syntax_highlighting(condition_match.get_start("condition"), ExpressionType.IF, condition_match.get_string("condition")), true)
# endif/else
var endcondition_matches: Array[RegExMatch] = regex_endconditions.search_all(text)
for endcondition_match in endcondition_matches:
colors[endcondition_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[endcondition_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
DMConstants.TYPE_TITLE:
colors[index] = { color = theme.titles_color }
# Mutations
var mutation_matches: Array[RegExMatch] = regex_mutation.search_all(text)
for mutation_match in mutation_matches:
colors[mutation_match.get_start(0)] = {"color": text_edit.theme_overrides.mutations_color}
colors.merge(_get_expression_syntax_highlighting(mutation_match.get_start("mutation"), ExpressionType.DO if mutation_match.strings[1].begins_with("do") else ExpressionType.SET, mutation_match.get_string("mutation")), true)
DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN:
colors[0] = { color = theme.conditions_color }
index = text.find(" ")
if index > -1:
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0)
if expression.size() == 0:
colors[index] = { color = theme.critical_color }
else:
_highlight_expression(expression, colors, index)
DMConstants.TYPE_MUTATION:
colors[0] = { color = theme.mutations_line_color }
index = text.find(" ")
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0)
if expression.size() == 0:
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 (but ignore ":" within line ID reference)
var split_index: int = dialogue_text.replace("\\:", "??").find(":")
if text.substr(split_index - 3, 3) != "[ID":
colors[index + split_index + 1] = { color = theme.text_color }
else:
# If there's no character name then just highlight the text as dialogue.
colors[index] = { color = theme.text_color }
# Interpolation
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text)
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("$>") or 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_line_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
var new_colors: Dictionary = {}
var ordered_colors: Dictionary = {}
var ordered_keys: Array = colors.keys()
ordered_keys.sort()
for index in ordered_keys:
new_colors[index] = colors[index]
for key_index: int in ordered_keys:
ordered_colors[key_index] = colors[key_index]
cache[text] = new_colors
return new_colors
cache[text] = ordered_colors
return ordered_colors
## Return the syntax highlighting for a dialogue line
func _get_dialogue_syntax_highlighting(start_index: int, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int:
var theme: Dictionary = get_text_edit().theme_overrides
var last_index: int = index
for token: Dictionary in tokens:
last_index = token.i
match token.type:
DMConstants.TOKEN_COMMENT:
colors[index + token.i] = { color = theme.comments_color }
# #tag style tags
var hashtag_matches: Array[RegExMatch] = dialogue_manager_parser.TAGS_REGEX.search_all(text)
for hashtag_match in hashtag_matches:
colors[start_index + hashtag_match.get_start(0)] = { "color": text_edit.theme_overrides.comments_color }
colors[start_index + hashtag_match.get_end(0)] = { "color": text_edit.theme_overrides.text_color }
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR:
colors[index + token.i] = { color = theme.conditions_color }
# bbcode-like global tags
var tag_matches: Array[RegExMatch] = regex_tags.search_all(text)
for tag_match in tag_matches:
colors[start_index + tag_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
if "val" in tag_match.names:
colors.merge(_get_literal_syntax_highlighting(start_index + tag_match.get_start("val"), tag_match.get_string("val")), true)
colors[start_index + tag_match.get_end("val")] = {"color": text_edit.theme_overrides.symbols_color}
# 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}
DMConstants.TOKEN_VARIABLE:
if token.value in ["true", "false"]:
colors[index + token.i] = { color = theme.conditions_color }
else:
colors[index + token.i] = { color = theme.members_color }
# ID tag
var translation_matches: Array[RegExMatch] = dialogue_manager_parser.TRANSLATION_REGEX.search_all(text)
for translation_match in translation_matches:
colors[start_index + translation_match.get_start(0)] = {"color": text_edit.theme_overrides.comments_color}
colors[start_index + translation_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \
DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \
DMConstants.TOKEN_ASSIGNMENT, DMConstants.TOKEN_COMPARISON:
if token.get("value", null) == "in":
colors[index + token.i] = { color = theme.conditions_color }
else:
colors[index + token.i] = { color = theme.symbols_color }
# Replacements
var replacement_matches: Array[RegExMatch] = dialogue_manager_parser.REPLACEMENTS_REGEX.search_all(text)
for replacement_match in replacement_matches:
colors[start_index + replacement_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + replacement_match.get_start(1)] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + replacement_match.get_start(1), replacement_match.strings[1]), true)
colors[start_index + replacement_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + replacement_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
DMConstants.TOKEN_NUMBER:
colors[index + token.i] = { color = theme.numbers_color }
# Jump at the end of a response
var goto_matches: Array[RegExMatch] = regex_goto.search_all(text)
for goto_match in goto_matches:
colors[start_index + goto_match.get_start(0)] = {"color": text_edit.theme_overrides.jumps_color}
if "file" in goto_match.names:
colors[start_index + goto_match.get_start("file")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end("file")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + goto_match.get_start("title")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end("title")] = {"color": text_edit.theme_overrides.jumps_color}
colors[start_index + goto_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
DMConstants.TOKEN_STRING:
colors[index + token.i] = { color = theme.strings_color }
# Wrapped condition
var wcondition_matches: Array[RegExMatch] = regex_wcondition.search_all(text)
for wcondition_match in wcondition_matches:
colors[start_index + wcondition_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_start(0) + 1] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wcondition_match.get_start(0) + 3] = {"color": text_edit.theme_overrides.text_color}
colors.merge(_get_literal_syntax_highlighting(start_index + wcondition_match.get_start("condition"), wcondition_match.get_string("condition")), true)
colors[start_index + wcondition_match.get_end("condition")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wcondition_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
# [/if] tag for color matching with the opening tag
var wendif_matches: Array[RegExMatch] = regex_wendif.search_all(text)
for wendif_match in wendif_matches:
colors[start_index + wendif_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_start(1)] = {"color": text_edit.theme_overrides.conditions_color}
colors[start_index + wendif_match.get_end(1)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + wendif_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
DMConstants.TOKEN_FUNCTION:
colors[index + token.i] = { color = theme.mutations_color }
colors[index + token.i + token.function.length()] = { color = theme.symbols_color }
for parameter: Array in token.value:
last_index = _highlight_expression(parameter, colors, index)
DMConstants.TOKEN_PARENS_CLOSE:
colors[index + token.i] = { color = theme.symbols_color }
# Random groups
var rgroup_matches: Array[RegExMatch] = regex_rgroup.search_all(text)
for rgroup_match in rgroup_matches:
colors[start_index + rgroup_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options")] = {"color": text_edit.theme_overrides.text_color}
var separator_matches: Array[RegExMatch] = RegEx.create_from_string("\\|").search_all(rgroup_match.get_string("options"))
for separator_match in separator_matches:
colors[start_index + rgroup_match.get_start("options") + separator_match.get_start(0)] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_start("options") + separator_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
colors[start_index + rgroup_match.get_end("options")] = {"color": text_edit.theme_overrides.symbols_color}
colors[start_index + rgroup_match.get_end(0)] = {"color": text_edit.theme_overrides.text_color}
DMConstants.TOKEN_DICTIONARY_REFERENCE:
colors[index + token.i] = { color = theme.members_color }
colors[index + token.i + token.variable.length()] = { color = theme.symbols_color }
last_index = _highlight_expression(token.value, colors, index)
DMConstants.TOKEN_ARRAY:
colors[index + token.i] = { color = theme.symbols_color }
for item: Array in token.value:
last_index = _highlight_expression(item, colors, index)
DMConstants.TOKEN_BRACKET_CLOSE:
colors[index + token.i] = { color = theme.symbols_color }
return colors
DMConstants.TOKEN_DICTIONARY:
colors[index + token.i] = { color = theme.symbols_color }
last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index)
DMConstants.TOKEN_BRACE_CLOSE:
colors[index + token.i] = { color = theme.symbols_color }
last_index += 1
DMConstants.TOKEN_GROUP:
last_index = _highlight_expression(token.value, colors, index)
return last_index
## Returns the syntax highlighting for an expression (mutation set/do, or condition)
func _get_expression_syntax_highlighting(start_index: int, type: ExpressionType, text: String) -> Dictionary:
var text_edit: TextEdit = get_text_edit()
var colors: Dictionary = {}
func _highlight_goto(text: String, colors: Dictionary, index: int) -> int:
var theme: Dictionary = get_text_edit().theme_overrides
var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {})
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:
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
return index
@@ -0,0 +1 @@
uid://klpiq4tk3t7a
@@ -34,7 +34,7 @@ func _ready() -> void:
func _on_download_button_pressed() -> void:
# Safeguard the actual dialogue manager repo from accidentally updating itself
if FileAccess.file_exists("res://examples/test_scenes/test_scene.gd"):
if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"):
prints("You can't update the addon from within itself.")
failed.emit()
return
@@ -0,0 +1 @@
uid://kpwo418lb2t2
@@ -1,6 +1,6 @@
[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"]
[node name="DownloadUpdatePanel" type="Control"]
@@ -0,0 +1 @@
uid://nyypeje1a036
@@ -63,7 +63,7 @@ func build_menu() -> void:
func _on_new_dialog_file_selected(path: String) -> void:
editor_plugin.main_view.new_file(path)
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))
else:
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:
if is_instance_valid(resource):
editor_plugin.get_editor_interface().call_deferred("edit_resource", resource)
EditorInterface.call_deferred("edit_resource", resource)
else:
build_menu()
menu.position = get_viewport().position + Vector2i(
@@ -112,7 +112,7 @@ func _on_menu_id_pressed(id: int) -> void:
ITEM_QUICK_LOAD:
quick_selected_file = ""
files_list.files = Engine.get_meta("DialogueCache").get_files()
files_list.files = Engine.get_meta("DMCache").get_files()
if resource:
files_list.select_file(resource.resource_path)
quick_open_dialog.popup_centered()
@@ -123,13 +123,13 @@ func _on_menu_id_pressed(id: int) -> void:
open_dialog.popup_centered()
ITEM_EDIT:
editor_plugin.get_editor_interface().call_deferred("edit_resource", resource)
EditorInterface.call_deferred("edit_resource", resource)
ITEM_CLEAR:
resource_changed.emit(null)
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)
@@ -0,0 +1 @@
uid://dooe2pflnqtve
@@ -1,6 +1,6 @@
[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://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"]
@@ -0,0 +1 @@
uid://damhqta55t67c
@@ -1,6 +1,6 @@
[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"]
offset_right = 8.0
@@ -59,7 +59,7 @@ func show_error() -> void:
show()
count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() })
var error = errors[error_index]
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number + 1, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) })
if error.has("external_error"):
error_button.text += " " + DialogueConstants.get_error_message(error.external_error)
@@ -72,7 +72,7 @@ func _on_errors_panel_theme_changed() -> 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:
@@ -0,0 +1 @@
uid://d2l8nlb6hhrfp
@@ -1,8 +1,8 @@
[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_wy5pj"]
[sub_resource type="Image" id="Image_w0gko"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
@@ -12,7 +12,7 @@ data = {
}
[sub_resource type="ImageTexture" id="ImageTexture_s6fxl"]
image = SubResource("Image_wy5pj")
image = SubResource("Image_w0gko")
[node name="ErrorsPanel" type="HBoxContainer"]
visible = false
@@ -114,6 +114,8 @@ func apply_filter() -> void:
func apply_theme() -> void:
if is_instance_valid(filter_edit):
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
@@ -0,0 +1 @@
uid://dqa4a4wwoo0aa
@@ -1,6 +1,6 @@
[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"]
[node name="FilesList" type="VBoxContainer"]
@@ -9,6 +9,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
script = ExtResource("1_cytii")
icon = ExtResource("2_3ijx1")
@@ -114,7 +114,7 @@ func find_in_files() -> Dictionary:
var results: Dictionary = {}
var q: String = input.text
var cache = Engine.get_meta("DialogueCache")
var cache = Engine.get_meta("DMCache")
var file: FileAccess
for path in cache.get_files():
var path_results: Array = []
@@ -0,0 +1 @@
uid://q368fmxxa8sd
@@ -1,6 +1,6 @@
[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"]
bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137)
@@ -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 = ""
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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
@@ -114,7 +114,7 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int:
return line.findn(text, from_index)
### Signals
#region Signals
func _on_text_edit_gui_input(event: InputEvent) -> void:
@@ -177,14 +177,17 @@ func _on_replace_button_pressed() -> void:
# Replace the selection at result index
var r: Array = results[result_index]
code_edit.begin_complex_operation()
var lines: PackedStringArray = code_edit.text.split("\n")
var line: String = lines[r[0]]
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
lines[r[0]] = line
code_edit.text = "\n".join(lines)
search(input.text, result_index)
code_edit.end_complex_operation()
code_edit.text_changed.emit()
search(input.text, result_index)
func _on_replace_all_button_pressed() -> void:
if match_case_button.button_pressed:
@@ -210,3 +213,6 @@ func _on_input_focus_entered() -> void:
func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
search()
#endregion
@@ -0,0 +1 @@
uid://cijsmjkq21cdq
@@ -1,6 +1,6 @@
[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"]
visible = false
@@ -48,6 +48,8 @@ func apply_filter() -> void:
func apply_theme() -> void:
if is_instance_valid(filter_edit):
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
@@ -0,0 +1 @@
uid://d0k2wndjj0ifm
@@ -1,6 +1,6 @@
[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"]
anchors_preset = 15
@@ -86,9 +86,9 @@ func _on_update_button_pressed() -> void:
if needs_reload:
var will_refresh = on_before_refresh.call()
if will_refresh:
Engine.get_meta("DialogueManagerPlugin").get_editor_interface().restart_editor(true)
EditorInterface.restart_editor(true)
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.popup_centered()
@@ -117,7 +117,7 @@ func _on_download_update_panel_failed() -> 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:
@@ -0,0 +1 @@
uid://cr1tt12dh5ecr
@@ -1,6 +1,6 @@
[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"]
[node name="UpdateButton" type="Button"]
+62 -14
View File
@@ -1,9 +1,23 @@
extends Node
class_name DMConstants extends RefCounted
const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json"
const CACHE_PATH = "user://dialogue_manager_cache.json"
enum MutationBehaviour {
Wait,
DoNotWait,
Skip
}
enum TranslationSource {
None,
Guess,
CSV,
PO
}
# Token types
const TOKEN_FUNCTION = &"function"
@@ -23,6 +37,7 @@ const TOKEN_COMPARISON = &"comparison"
const TOKEN_ASSIGNMENT = &"assignment"
const TOKEN_OPERATOR = &"operator"
const TOKEN_COMMA = &"comma"
const TOKEN_NULL_COALESCE = &"null_coalesce"
const TOKEN_DOT = &"dot"
const TOKEN_CONDITION = &"condition"
const TOKEN_BOOL = &"bool"
@@ -33,21 +48,27 @@ const TOKEN_NUMBER = &"number"
const TOKEN_VARIABLE = &"variable"
const TOKEN_COMMENT = &"comment"
const TOKEN_VALUE = &"value"
const TOKEN_ERROR = &"error"
# 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_TITLE = &"title"
const TYPE_CONDITION = &"condition"
const TYPE_WHILE = &"while"
const TYPE_MATCH = &"match"
const TYPE_WHEN = &"when"
const TYPE_MUTATION = &"mutation"
const TYPE_GOTO = &"goto"
const TYPE_DIALOGUE = &"dialogue"
const TYPE_RANDOM = &"random"
const TYPE_ERROR = &"error"
const TYPE_ELSE = &"else"
# Line IDs
const ID_NULL = &""
@@ -94,6 +115,18 @@ const ERR_UNEXPECTED_VARIABLE = 132
const ERR_INVALID_INDEX = 133
const ERR_UNEXPECTED_ASSIGNMENT = 134
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
const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141
const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142
const ERR_MISSING_RESOURCE_FOR_AUTOSTART = 143
static var _current_locale: String = ""
static var _current_translation: Translation
## Get the error message
@@ -169,18 +202,33 @@ static func get_error_message(error: int) -> String:
return translate(&"errors.unexpected_assignment")
ERR_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")
ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE:
return translate(&"errors.unexpected_syntax_on_nested_dialogue_line")
ERR_NESTED_DIALOGUE_INVALID_JUMP:
return translate(&"errors.err_nested_dialogue_invalid_jump")
ERR_MISSING_RESOURCE_FOR_AUTOSTART:
return translate(&"errors.missing_resource_for_autostart")
return translate(&"errors.unknown")
static func translate(string: String) -> String:
var temp_node = new()
var base_path = temp_node.get_script().resource_path.get_base_dir()
temp_node.free()
var language: String = TranslationServer.get_tool_locale()
var translations_path: String = "%s/l10n/%s.po" % [base_path, language]
var fallback_translations_path: String = "%s/l10n/%s.po" % [base_path, TranslationServer.get_tool_locale().substr(0, 2)]
var en_translations_path: String = "%s/l10n/en.po" % base_path
var translations: Translation = load(translations_path if FileAccess.file_exists(translations_path) else (fallback_translations_path if FileAccess.file_exists(fallback_translations_path) else en_translations_path))
return translations.get_message(string)
var locale: String = TranslationServer.get_tool_locale()
if _current_translation == null or _current_locale != locale:
var base_path: String = new().get_script().resource_path.get_base_dir()
var translation_path: String = "%s/l10n/%s.po" % [base_path, locale]
var fallback_translation_path: String = "%s/l10n/%s.po" % [base_path, locale.substr(0, 2)]
var en_translation_path: String = "%s/l10n/en.po" % base_path
_current_translation = load(translation_path if FileAccess.file_exists(translation_path) else (fallback_translation_path if FileAccess.file_exists(fallback_translation_path) else en_translation_path))
_current_locale = locale
return _current_translation.get_message(string)
+1
View File
@@ -0,0 +1 @@
uid://b1oarbmjtyesf
+33 -42
View File
@@ -9,17 +9,20 @@ class_name DialogueLabel extends RichTextLabel
## Emitted for each letter typed out.
signal spoke(letter: String, letter_index: int, speed: float)
## Emitted when typing paused for a `[wait]`
signal paused_typing(duration: float)
## Emitted when the player skips the typing of dialogue.
signal skipped_typing()
## Emitted when typing starts
signal started_typing()
## Emitted when typing finishes.
signal finished_typing()
## [Deprecated] No longer emitted.
signal paused_typing(duration: float)
# The action to press to skip typing.
## The action to press to skip typing.
@export var skip_action: StringName = &"ui_cancel"
## The speed with which the text types out.
@@ -44,23 +47,23 @@ var _already_mutated_indices: PackedInt32Array = []
## The current line of dialogue.
var dialogue_line:
set(next_dialogue_line):
dialogue_line = next_dialogue_line
custom_minimum_size = Vector2.ZERO
text = ""
text = dialogue_line.text
set(value):
if value != dialogue_line:
dialogue_line = value
_update_text()
get:
return dialogue_line
## Whether the label is currently typing itself out.
var is_typing: bool = false:
set(value):
var is_finished: bool = is_typing != value and value == false
is_typing = value
var is_finished: bool = _is_typing != value and value == false and visible_characters == get_total_character_count()
_is_typing = value
if is_finished:
finished_typing.emit()
get:
return is_typing
return _is_typing and not _is_awaiting_mutation
var _is_typing: bool = false
var _last_wait_index: int = -1
var _last_mutation_index: int = -1
@@ -69,7 +72,7 @@ var _is_awaiting_mutation: bool = false
func _process(delta: float) -> void:
if self.is_typing:
if _is_typing:
# Type out text
if visible_ratio < 1:
# See if we are waiting
@@ -81,21 +84,18 @@ func _process(delta: float) -> void:
else:
# Make sure any mutations at the end of the line get run
_mutate_inline_mutations(get_total_character_count())
self.is_typing = false
is_typing = false
func _unhandled_input(event: InputEvent) -> void:
# Note: this will no longer be reached if using Dialogue Manager > 2.32.2. To make skip handling
# simpler (so all of mouse/keyboard/joypad are together) it is now the responsibility of the
# dialogue balloon.
if self.is_typing and visible_ratio < 1 and InputMap.has_action(skip_action) and event.is_action_pressed(skip_action):
get_viewport().set_input_as_handled()
skip_typing()
## Sets the label's text from the current dialogue line. Override if you want
## to do something more interesting in your subclass.
func _update_text() -> void:
text = dialogue_line.text
## Start typing out the text
func type_out() -> void:
text = dialogue_line.text
_update_text()
visible_characters = 0
visible_ratio = 0
_waiting_seconds = 0
@@ -103,24 +103,25 @@ func type_out() -> void:
_last_mutation_index = -1
_already_mutated_indices.clear()
self.is_typing = true
is_typing = true
started_typing.emit()
# Allow typing listeners a chance to connect
await get_tree().process_frame
if get_total_character_count() == 0:
self.is_typing = false
is_typing = false
elif seconds_per_step == 0:
_mutate_remaining_mutations()
visible_characters = get_total_character_count()
self.is_typing = false
is_typing = false
## Stop typing out the text and jump right to the end
func skip_typing() -> void:
_mutate_remaining_mutations()
visible_characters = get_total_character_count()
self.is_typing = false
is_typing = false
skipped_typing.emit()
@@ -136,17 +137,11 @@ func _type_next(delta: float, seconds_needed: float) -> void:
_mutate_inline_mutations(visible_characters)
if _is_awaiting_mutation: return
var additional_waiting_seconds: float = _get_pause(visible_characters)
# Pause on characters like "."
if _should_auto_pause():
additional_waiting_seconds += seconds_per_pause_step
# Pause at literal [wait] directives
if _last_wait_index != visible_characters and additional_waiting_seconds > 0:
var waiting_seconds: float = seconds_per_pause_step if _should_auto_pause() else 0
if _last_wait_index != visible_characters and waiting_seconds > 0:
_last_wait_index = visible_characters
_waiting_seconds += additional_waiting_seconds
paused_typing.emit(_get_pause(visible_characters))
_waiting_seconds += waiting_seconds
else:
visible_characters += 1
if visible_characters <= get_total_character_count():
@@ -159,11 +154,6 @@ func _type_next(delta: float, seconds_needed: float) -> void:
_type_next(delta, seconds_needed)
# Get the pause for the current typing position if there is one
func _get_pause(at_index: int) -> float:
return dialogue_line.pauses.get(at_index, 0)
# Get the speed for the current typing position
func _get_speed(at_index: int) -> float:
var speed: float = 1
@@ -187,12 +177,13 @@ func _mutate_inline_mutations(index: int) -> void:
if inline_mutation[0] > index:
return
if inline_mutation[0] == index and not _already_mutated_indices.has(index):
_already_mutated_indices.append(index)
_is_awaiting_mutation = true
# 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
_already_mutated_indices.append(index)
# Determine if the current autopause character at the cursor should qualify to pause typing.
func _should_auto_pause() -> bool:
@@ -0,0 +1 @@
uid://g32um0mltv5d
+1 -1
View File
@@ -1,6 +1,6 @@
[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"]
anchors_preset = 10
+20 -13
View File
@@ -2,14 +2,11 @@
class_name DialogueLine extends RefCounted
const _DialogueConstants = preload("./constants.gd")
## The ID of this line
var id: String
## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code]
var type: String = _DialogueConstants.TYPE_DIALOGUE
var type: String = DMConstants.TYPE_DIALOGUE
## The next line ID after this line.
var next_id: String = ""
@@ -29,9 +26,6 @@ var text_replacements: Array[Dictionary] = []
## The key to use for translating this line.
var translation_key: String = ""
## A map for when and for how long to pause while typing out the dialogue text.
var pauses: Dictionary = {}
## A map for speed changes when typing out the dialogue text.
var speeds: Dictionary = {}
@@ -41,6 +35,9 @@ var inline_mutations: Array[Array] = []
## A list of responses attached to this line of dialogue.
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.
var extra_game_states: Array = []
@@ -65,33 +62,43 @@ func _init(data: Dictionary = {}) -> void:
extra_game_states = data.get("extra_game_states", [])
match type:
_DialogueConstants.TYPE_DIALOGUE:
DMConstants.TYPE_DIALOGUE:
character = data.character
character_replacements = data.get("character_replacements", [] as Array[Dictionary])
text = data.text
text_replacements = data.get("text_replacements", [] as Array[Dictionary])
translation_key = data.get("translation_key", data.text)
pauses = data.get("pauses", {})
speeds = data.get("speeds", {})
inline_mutations = data.get("inline_mutations", [] as Array[Array])
time = data.get("time", "")
tags = data.get("tags", [])
concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine])
_DialogueConstants.TYPE_MUTATION:
DMConstants.TYPE_MUTATION:
mutation = data.mutation
func _to_string() -> String:
match type:
_DialogueConstants.TYPE_DIALOGUE:
DMConstants.TYPE_DIALOGUE:
return "<DialogueLine character=\"%s\" text=\"%s\">" % [character, text]
_DialogueConstants.TYPE_MUTATION:
DMConstants.TYPE_MUTATION:
return "<DialogueLine mutation>"
return ""
## Check if a dialogue line has a given tag.
func has_tag(tag_name: String) -> bool:
var wrapped: String = "%s=" % tag_name
for t in tags:
if t.begins_with(wrapped):
return true
return false
## Get the value of a tag if the tag is in the form of [code]tag=value[/code]
func get_tag_value(tag_name: String) -> String:
var wrapped := "%s=" % tag_name
var wrapped: String = "%s=" % tag_name
for t in tags:
if t.begins_with(wrapped):
return t.replace(wrapped, "").strip_edges()
@@ -0,0 +1 @@
uid://rhuq0eyf8ar2
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://c3rodes2l3gxb
@@ -0,0 +1,11 @@
class_name DMDialogueProcessor extends RefCounted
## Override to modify the incoming raw string.
func _preprocess_line(raw_line: String) -> String:
return raw_line
## Override to modify the outgoing dialogue line.
func _process_line(line: DMCompiledLine) -> void:
pass
@@ -0,0 +1 @@
uid://m3b28rmso14t
@@ -1,141 +0,0 @@
@icon("./assets/responses_menu.svg")
## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b].
class_name DialogueResponsesMenu extends Container
## Emitted when a response is selected.
signal response_selected(response)
## Optionally specify a control to duplicate for each response
@export var response_template: Control
## The action for accepting a response (is possibly overridden by parent dialogue balloon).
@export var next_action: StringName = &""
## The list of dialogue responses.
var responses: Array = []:
get:
return responses
set(value):
responses = value
# Remove any current items
for item in get_children():
if item == response_template: continue
remove_child(item)
item.queue_free()
# Add new items
if responses.size() > 0:
for response in responses:
var item: Control
if is_instance_valid(response_template):
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
item.show()
else:
item = Button.new()
item.name = "Response%d" % get_child_count()
if not response.is_allowed:
item.name = String(item.name) + "Disallowed"
item.disabled = true
# If the item has a response property then use that
if "response" in item:
item.response = response
# Otherwise assume we can just set the text
else:
item.text = response.text
item.set_meta("response", response)
add_child(item)
_configure_focus()
func _ready() -> void:
visibility_changed.connect(func():
if visible and get_menu_items().size() > 0:
get_menu_items()[0].grab_focus()
)
if is_instance_valid(response_template):
response_template.hide()
## Get the selectable items in the menu.
func get_menu_items() -> Array:
var items: Array = []
for child in get_children():
if not child.visible: continue
if "Disallowed" in child.name: continue
items.append(child)
return items
## [b]DEPRECATED[/b]. Do not use.
func set_responses(next_responses: Array) -> void:
self.responses = next_responses
#region Internal
# Prepare the menu for keyboard and mouse navigation.
func _configure_focus() -> void:
var items = get_menu_items()
for i in items.size():
var item: Control = items[i]
item.focus_mode = Control.FOCUS_ALL
item.focus_neighbor_left = item.get_path()
item.focus_neighbor_right = item.get_path()
if i == 0:
item.focus_neighbor_top = item.get_path()
item.focus_previous = item.get_path()
else:
item.focus_neighbor_top = items[i - 1].get_path()
item.focus_previous = items[i - 1].get_path()
if i == items.size() - 1:
item.focus_neighbor_bottom = item.get_path()
item.focus_next = item.get_path()
else:
item.focus_neighbor_bottom = items[i + 1].get_path()
item.focus_next = items[i + 1].get_path()
item.mouse_entered.connect(_on_response_mouse_entered.bind(item))
item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response")))
items[0].grab_focus()
#endregion
#region Signals
func _on_response_mouse_entered(item: Control) -> void:
if "Disallowed" in item.name: return
item.grab_focus()
func _on_response_gui_input(event: InputEvent, item: Control, response) -> void:
if "Disallowed" in item.name: return
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)
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)
#endregion
+1 -2
View File
@@ -5,7 +5,6 @@
class_name DialogueResource extends Resource
const _DialogueManager = preload("./dialogue_manager.gd")
const DialogueLine = preload("./dialogue_line.gd")
## 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
## be a title string or a stringified line number). Runs any mutations along the way and then returns
## the first dialogue line encountered.
func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: _DialogueManager.MutationBehaviour = _DialogueManager.MutationBehaviour.Wait) -> DialogueLine:
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)
@@ -0,0 +1 @@
uid://dbs4435dsf3ry
+5 -4
View File
@@ -2,14 +2,11 @@
class_name DialogueResponse extends RefCounted
const _DialogueConstants = preload("./constants.gd")
## The ID of this response
var id: String
## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code].
var type: String = _DialogueConstants.TYPE_RESPONSE
var type: String = DMConstants.TYPE_RESPONSE
## The next line ID to use if this response is selected by the player.
var next_id: String = ""
@@ -17,6 +14,9 @@ var next_id: String = ""
## [code]true[/code] if the condition of this line was met.
var is_allowed: bool = true
## The original condition text.
var condition_as_text: String = ""
## A character (depending on the "characters in responses" behaviour setting).
var character: String = ""
@@ -48,6 +48,7 @@ func _init(data: Dictionary = {}) -> void:
text_replacements = data.text_replacements
tags = data.tags
translation_key = data.translation_key
condition_as_text = data.condition_as_text
func _to_string() -> String:
@@ -0,0 +1 @@
uid://cm0xpfeywpqid
@@ -0,0 +1,178 @@
@icon("./assets/responses_menu.svg")
## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b].
class_name DialogueResponsesMenu extends Container
## Emitted when a response is focused.
signal response_focused(response: Control)
## Emitted when a response is selected.
signal response_selected(response: Control)
## Optionally specify a control to duplicate for each response
@export var response_template: Control
## The action for accepting a response (is possibly overridden by parent dialogue balloon).
@export var next_action: StringName = &""
## Automatically set up focus neighbours when the responses list changes.
@export var auto_configure_focus: bool = true
## Automatically focus the first item when showing.
@export var auto_focus_first_item: bool = true
## Hide any responses where [code]is_allowed[/code] is false
@export var hide_failed_responses: bool = false
## The list of dialogue responses.
var responses: Array = []:
set(value):
responses = value
_apply_responses()
get:
return responses
# The previously focused item in this menu.
var _previously_focused_item: Control = null
func _ready() -> void:
visibility_changed.connect(func():
if auto_focus_first_item and visible and get_menu_items().size() > 0:
var first_item: Control = get_menu_items()[0]
if first_item.is_inside_tree():
first_item.grab_focus()
)
if is_instance_valid(response_template):
response_template.hide()
get_viewport().gui_focus_changed.connect(_on_focus_changed)
## Get the selectable items in the menu.
func get_menu_items() -> Array:
var items: Array = []
for child in get_children():
if not child.visible: continue
if "Disallowed" in child.name: continue
items.append(child)
return items
## Prepare the menu for keyboard and mouse navigation.
func configure_focus() -> void:
var items = get_menu_items()
for i in items.size():
var item: Control = items[i]
item.focus_mode = Control.FOCUS_ALL
item.focus_neighbor_left = item.get_path()
item.focus_neighbor_right = item.get_path()
if i == 0:
item.focus_neighbor_top = item.get_path()
item.focus_neighbor_left = item.get_path()
item.focus_previous = item.get_path()
else:
item.focus_neighbor_top = items[i - 1].get_path()
item.focus_neighbor_left = items[i - 1].get_path()
item.focus_previous = items[i - 1].get_path()
if i == items.size() - 1:
item.focus_neighbor_bottom = item.get_path()
item.focus_neighbor_right = item.get_path()
item.focus_next = item.get_path()
else:
item.focus_neighbor_bottom = items[i + 1].get_path()
item.focus_neighbor_right = items[i + 1].get_path()
item.focus_next = items[i + 1].get_path()
item.mouse_entered.connect(_on_response_mouse_entered.bind(item))
item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response")))
_previously_focused_item = items[0]
if auto_focus_first_item:
items[0].grab_focus()
#region Internal
# Set up the visual side of things.
func _apply_responses() -> void:
# Remove any current items
for item in get_children():
if item == response_template: continue
remove_child(item)
item.queue_free()
# Add new items
if responses.size() > 0:
for response in responses:
if hide_failed_responses and not response.is_allowed: continue
var item: Control
if is_instance_valid(response_template):
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
item.show()
else:
item = Button.new()
item.name = "Response%d" % get_child_count()
if not response.is_allowed:
item.name = item.name + &"Disallowed"
item.disabled = true
# If the item has a response property then use that
if "response" in item:
item.response = response
# Otherwise assume we can just set the text
else:
item.text = response.text
item.set_meta("response", response)
add_child(item)
if auto_configure_focus:
configure_focus()
#endregion
#region Signals
func _on_focus_changed(control: Control) -> void:
if "Disallowed" in control.name: return
if not control in get_menu_items(): return
if _previously_focused_item != control:
_previously_focused_item = control
response_focused.emit(control)
func _on_response_mouse_entered(item: Control) -> void:
if "Disallowed" in item.name: return
item.grab_focus()
func _on_response_gui_input(event: InputEvent, item: Control, response) -> void:
if "Disallowed" in item.name: return
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)
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)
#endregion
@@ -0,0 +1 @@
uid://bb52rsfwhkxbn
@@ -1,43 +1,52 @@
extends EditorTranslationParserPlugin
class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin
const DialogueConstants = preload("./constants.gd")
const DialogueSettings = preload("./settings.gd")
const DialogueManagerParser = preload("./components/parser.gd")
const DialogueManagerParseResult = preload("./components/parse_result.gd")
## Cached result of parsing a dialogue file.
var data: DMCompilerResult
## List of characters that were added.
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 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([])
# Add all character names if settings ask for it
if DialogueSettings.get_setting("export_characters_in_translation", true):
var character_names: PackedStringArray = data.character_names
for character_name in character_names:
if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true):
translated_character_names = [] as Array[DialogueLine]
for character_name: String in data.character_names:
if character_name in known_keys: continue
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
var dialogue: Dictionary = data.lines
for key in dialogue.keys():
for key: String in dialogue.keys():
var line: Dictionary = dialogue.get(key)
if not line.type in [DialogueConstants.TYPE_DIALOGUE, DialogueConstants.TYPE_RESPONSE]: continue
if line.translation_key in known_keys: continue
if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: 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:
msgids_context_plural.append([line.text.replace('"', '\"'), "", ""])
if translation_key in known_keys: continue
known_keys.append(translation_key)
translated_lines.append(line)
if translation_key == line.text:
msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")]))
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:
@@ -0,0 +1 @@
uid://c6bya881h1egb
@@ -5,6 +5,9 @@ namespace DialogueManagerRuntime
{
public partial class ExampleBalloon : CanvasLayer
{
[Export] public Resource DialogueResource;
[Export] public string StartFromTitle = "";
[Export] public bool AutoStart = false;
[Export] public string NextAction = "ui_accept";
[Export] public string SkipAction = "ui_cancel";
@@ -13,8 +16,8 @@ namespace DialogueManagerRuntime
RichTextLabel characterLabel;
RichTextLabel dialogueLabel;
VBoxContainer responsesMenu;
Polygon2D progress;
Resource resource;
Array<Variant> temporaryGameStates = new Array<Variant>();
bool isWaitingForInput = false;
bool willHideBalloon = false;
@@ -25,21 +28,27 @@ namespace DialogueManagerRuntime
get => dialogueLine;
set
{
isWaitingForInput = false;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
// Dialogue has finished so close the balloon
if (value == null)
{
QueueFree();
if (Owner == null)
{
QueueFree();
}
else
{
Hide();
}
return;
}
dialogueLine = value;
UpdateDialogue();
ApplyDialogueLine();
}
}
Timer MutationCooldown = new Timer();
public override void _Ready()
{
@@ -47,6 +56,7 @@ namespace DialogueManagerRuntime
characterLabel = GetNode<RichTextLabel>("%CharacterLabel");
dialogueLabel = GetNode<RichTextLabel>("%DialogueLabel");
responsesMenu = GetNode<VBoxContainer>("%ResponsesMenu");
progress = GetNode<Polygon2D>("%Progress");
balloon.Hide();
@@ -88,7 +98,28 @@ namespace DialogueManagerRuntime
Next(response.NextId);
}));
// Hide the balloon when a mutation is running
MutationCooldown.Timeout += () =>
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
AddChild(MutationCooldown);
DialogueManager.Mutated += OnMutated;
if (AutoStart)
{
if (!IsInstanceValid(DialogueResource))
{
throw new System.Exception(DialogueManager.GetErrorMessage(143));
}
Start();
}
}
@@ -111,7 +142,7 @@ namespace DialogueManagerRuntime
if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel))
{
float visibleRatio = dialogueLabel.VisibleRatio;
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates);
DialogueLine = await DialogueManager.GetNextDialogueLine(DialogueResource, DialogueLine.Id, temporaryGameStates);
if (visibleRatio < 1.0f)
{
dialogueLabel.Call("skip_typing");
@@ -120,36 +151,52 @@ namespace DialogueManagerRuntime
}
public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null)
public override void _Process(double delta)
{
if (!IsNodeReady())
base._Process(delta);
if (IsInstanceValid(dialogueLine))
{
await ToSignal(this, SignalName.Ready);
progress.Visible = !(bool)dialogueLabel.Get("is_typing") && dialogueLine.Responses.Count == 0 && !dialogueLine.HasTag("voice");
}
}
public async void Start(Resource dialogueResource = null, string title = "", Array<Variant> extraGameStates = null)
{
temporaryGameStates = new Array<Variant> { this } + (extraGameStates ?? new Array<Variant>());
isWaitingForInput = false;
if (IsInstanceValid(dialogueResource))
{
DialogueResource = dialogueResource;
}
if (title != "")
{
StartFromTitle = title;
}
temporaryGameStates = extraGameStates ?? new Array<Variant>();
isWaitingForInput = false;
resource = dialogueResource;
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates);
DialogueLine = await DialogueManager.GetNextDialogueLine(DialogueResource, StartFromTitle, temporaryGameStates);
Show();
}
public async void Next(string nextId)
{
DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates);
DialogueLine = await DialogueManager.GetNextDialogueLine(DialogueResource, nextId, temporaryGameStates);
}
#region Helpers
private async void UpdateDialogue()
private async void ApplyDialogueLine()
{
if (!IsNodeReady())
{
await ToSignal(this, SignalName.Ready);
}
MutationCooldown.Stop();
isWaitingForInput = false;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
// Set up the character name
characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character);
@@ -208,14 +255,7 @@ namespace DialogueManagerRuntime
{
isWaitingForInput = false;
willHideBalloon = true;
GetTree().CreateTimer(0.1f).Timeout += () =>
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
MutationCooldown.Start(0.1f);
}
@@ -0,0 +1 @@
uid://5b3w40kwakl3
@@ -1,14 +1,25 @@
class_name DialogueManagerExampleBalloon extends CanvasLayer
#class_name DialogueManagerExampleBalloon extends CanvasLayer
## A basic dialogue balloon for use with Dialogue Manager.
extends CanvasLayer
## The dialogue resource
@export var dialogue_resource: DialogueResource
## Start from a given title when using balloon as a [Node] in a scene.
@export var start_from_title: String = ""
## If running as a [Node] in a scene then auto start the dialogue.
@export var auto_start: bool = false
## The action to use for advancing the dialogue
@export var next_action: StringName = &"ui_accept"
## The action to use to skip typing the dialogue
@export var skip_action: StringName = &"ui_cancel"
## The dialogue resource
var resource: DialogueResource
## A sound player for voice lines (if they exist).
@onready var audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer
## Temporary game states
var temporary_game_states: Array = []
@@ -26,55 +37,22 @@ var _locale: String = TranslationServer.get_locale()
## The current line
var dialogue_line: DialogueLine:
set(next_dialogue_line):
is_waiting_for_input = false
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
# 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)
set(value):
if value:
dialogue_line = value
apply_dialogue_line()
else:
is_waiting_for_input = true
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
# The dialogue has finished so close the balloon
if owner == null:
queue_free()
else:
hide()
get:
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
@@ -87,6 +65,9 @@ var dialogue_line: DialogueLine:
## The menu of responses
@onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu
## Indicator to show that player can progress dialogue.
@onready var progress: Polygon2D = %Progress
func _ready() -> void:
balloon.hide()
@@ -96,6 +77,19 @@ func _ready() -> void:
if responses_menu.next_action.is_empty():
responses_menu.next_action = next_action
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
add_child(mutation_cooldown)
if auto_start:
if not is_instance_valid(dialogue_resource):
assert(false, DMConstants.get_error_message(DMConstants.ERR_MISSING_RESOURCE_FOR_AUTOSTART))
start()
func _process(delta: float) -> void:
if is_instance_valid(dialogue_line):
progress.visible = not dialogue_label.is_typing and dialogue_line.responses.size() == 0 and not dialogue_line.has_tag("voice")
func _unhandled_input(_event: InputEvent) -> void:
# Only the balloon is allowed to handle input while it's showing
@@ -107,37 +101,88 @@ func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label):
_locale = TranslationServer.get_locale()
var visible_ratio = dialogue_label.visible_ratio
self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id)
dialogue_line = await dialogue_resource.get_next_dialogue_line(dialogue_line.id)
if visible_ratio < 1:
dialogue_label.skip_typing()
## Start some dialogue
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
if not is_node_ready():
await ready
temporary_game_states = [self] + extra_game_states
func start(with_dialogue_resource: DialogueResource = null, title: String = "", extra_game_states: Array = []) -> void:
temporary_game_states = [self] + extra_game_states
is_waiting_for_input = false
resource = dialogue_resource
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
if is_instance_valid(with_dialogue_resource):
dialogue_resource = with_dialogue_resource
if not title.is_empty():
start_from_title = title
dialogue_line = await dialogue_resource.get_next_dialogue_line(start_from_title, temporary_game_states)
show()
## Apply any changes to the balloon given a new [DialogueLine].
func apply_dialogue_line() -> void:
mutation_cooldown.stop()
progress.hide()
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 next line
if dialogue_line.has_tag("voice"):
audio_stream_player.stream = load(dialogue_line.get_tag_value("voice"))
audio_stream_player.play()
await audio_stream_player.finished
next(dialogue_line.next_id)
elif 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
func next(next_id: String) -> void:
self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states)
dialogue_line = await dialogue_resource.get_next_dialogue_line(next_id, temporary_game_states)
#region Signals
func _on_mutation_cooldown_timeout() -> void:
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
func _on_mutated(_mutation: Dictionary) -> void:
is_waiting_for_input = false
will_hide_balloon = true
get_tree().create_timer(0.1).timeout.connect(func():
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
)
if not _mutation.is_inline:
is_waiting_for_input = false
will_hide_balloon = true
mutation_cooldown.start(0.1)
func _on_balloon_gui_input(event: InputEvent) -> void:
@@ -0,0 +1 @@
uid://d1wt4ma6055l8
@@ -1,8 +1,178 @@
[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="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"]
[ext_resource type="Script" path="res://addons/dialogue_manager/dialogue_reponses_menu.gd" id="3_72ixx"]
[sub_resource type="GDScript" id="GDScript_i0vys"]
script/source = "class_name DialogueManagerExampleBalloon extends CanvasLayer
## A basic dialogue balloon for use with Dialogue Manager.
## The action to use for advancing the dialogue
@export var next_action: StringName = &\"ui_accept\"
## The action to use to skip typing the dialogue
@export var skip_action: StringName = &\"ui_cancel\"
## The dialogue resource
var resource: DialogueResource
## Temporary game states
var temporary_game_states: Array = []
## See if we are waiting for the player
var is_waiting_for_input: bool = false
## See if we are running a long mutation and should hide the balloon
var will_hide_balloon: bool = false
## A dictionary to store any ephemeral variables
var locals: Dictionary = {}
var _locale: String = TranslationServer.get_locale()
## The current line
var dialogue_line: DialogueLine:
set(next_dialogue_line):
is_waiting_for_input = false
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
# 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:
is_waiting_for_input = true
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
get:
return dialogue_line
## 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:
balloon.hide()
Engine.get_singleton(\"DialogueManager\").mutated.connect(_on_mutated)
# If the responses menu doesn't have a next action set, use this one
if responses_menu.next_action.is_empty():
responses_menu.next_action = next_action
func _unhandled_input(_event: InputEvent) -> void:
# Only the balloon is allowed to handle input while it's showing
get_viewport().set_input_as_handled()
func _notification(what: int) -> void:
## Detect a change of locale and update the current dialogue line to show the new language
if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label):
_locale = TranslationServer.get_locale()
var visible_ratio = dialogue_label.visible_ratio
self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id)
if visible_ratio < 1:
dialogue_label.skip_typing()
## Start some dialogue
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
if not is_node_ready():
await ready
temporary_game_states = [self] + extra_game_states
is_waiting_for_input = false
resource = dialogue_resource
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
## Go to the next line
func next(next_id: String) -> void:
self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states)
#region Signals
func _on_mutated(_mutation: Dictionary) -> void:
is_waiting_for_input = false
will_hide_balloon = true
get_tree().create_timer(0.1).timeout.connect(func():
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
)
func _on_balloon_gui_input(event: InputEvent) -> void:
# See if we need to skip typing of the dialogue
if dialogue_label.is_typing:
var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed()
var skip_button_was_pressed: bool = event.is_action_pressed(skip_action)
if mouse_was_clicked or skip_button_was_pressed:
get_viewport().set_input_as_handled()
dialogue_label.skip_typing()
return
if not is_waiting_for_input: return
if dialogue_line.responses.size() > 0: return
# When there are no response options the balloon itself is the clickable thing
get_viewport().set_input_as_handled()
if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
next(dialogue_line.next_id)
elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon:
next(dialogue_line.next_id)
func _on_responses_menu_response_selected(response: DialogueResponse) -> void:
next(response.next_id)
#endregion
"
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"]
bg_color = Color(0, 0, 0, 1)
@@ -63,9 +233,154 @@ MarginContainer/constants/margin_right = 30
MarginContainer/constants/margin_top = 15
Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5")
[sub_resource type="GDScript" id="GDScript_v0j00"]
script/source = "@icon(\"./assets/responses_menu.svg\")
## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b].
class_name DialogueResponsesMenu extends Container
## Emitted when a response is selected.
signal response_selected(response)
## Optionally specify a control to duplicate for each response
@export var response_template: Control
## The action for accepting a response (is possibly overridden by parent dialogue balloon).
@export var next_action: StringName = &\"\"
## The list of dialogue responses.
var responses: Array = []:
get:
return responses
set(value):
responses = value
# Remove any current items
for item in get_children():
if item == response_template: continue
remove_child(item)
item.queue_free()
# Add new items
if responses.size() > 0:
for response in responses:
var item: Control
if is_instance_valid(response_template):
item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS)
item.show()
else:
item = Button.new()
item.name = \"Response%d\" % get_child_count()
if not response.is_allowed:
item.name = String(item.name) + \"Disallowed\"
item.disabled = true
# If the item has a response property then use that
if \"response\" in item:
item.response = response
# Otherwise assume we can just set the text
else:
item.text = response.text
item.set_meta(\"response\", response)
add_child(item)
_configure_focus()
func _ready() -> void:
visibility_changed.connect(func():
if visible and get_menu_items().size() > 0:
get_menu_items()[0].grab_focus()
)
if is_instance_valid(response_template):
response_template.hide()
## Get the selectable items in the menu.
func get_menu_items() -> Array:
var items: Array = []
for child in get_children():
if not child.visible: continue
if \"Disallowed\" in child.name: continue
items.append(child)
return items
## [b]DEPRECATED[/b]. Do not use.
func set_responses(next_responses: Array) -> void:
self.responses = next_responses
#region Internal
# Prepare the menu for keyboard and mouse navigation.
func _configure_focus() -> void:
var items = get_menu_items()
for i in items.size():
var item: Control = items[i]
item.focus_mode = Control.FOCUS_ALL
item.focus_neighbor_left = item.get_path()
item.focus_neighbor_right = item.get_path()
if i == 0:
item.focus_neighbor_top = item.get_path()
item.focus_previous = item.get_path()
else:
item.focus_neighbor_top = items[i - 1].get_path()
item.focus_previous = items[i - 1].get_path()
if i == items.size() - 1:
item.focus_neighbor_bottom = item.get_path()
item.focus_next = item.get_path()
else:
item.focus_neighbor_bottom = items[i + 1].get_path()
item.focus_next = items[i + 1].get_path()
item.mouse_entered.connect(_on_response_mouse_entered.bind(item))
item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta(\"response\")))
items[0].grab_focus()
#endregion
#region Signals
func _on_response_mouse_entered(item: Control) -> void:
if \"Disallowed\" in item.name: return
item.grab_focus()
func _on_response_gui_input(event: InputEvent, item: Control, response) -> void:
if \"Disallowed\" in item.name: return
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)
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)
#endregion
"
[node name="ExampleBalloon" type="CanvasLayer"]
layer = 100
script = ExtResource("1_36de5")
script = SubResource("GDScript_i0vys")
[node name="Balloon" type="Control" parent="."]
unique_name_in_owner = true
@@ -132,7 +447,7 @@ unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 8
theme_override_constants/separation = 2
script = ExtResource("3_72ixx")
script = SubResource("GDScript_v0j00")
response_template = NodePath("ResponseExample")
[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"]
@@ -140,4 +455,3 @@ layout_mode = 2
text = "Response example"
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
@@ -1,8 +1,8 @@
[gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"]
[gd_scene load_steps=12 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="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"]
content_margin_left = 6.0
@@ -66,7 +66,7 @@ corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"]
bg_color = Color(0, 0, 0, 1)
border_width_left = 1
border_width_top = 1
@@ -87,7 +87,28 @@ MarginContainer/constants/margin_bottom = 4
MarginContainer/constants/margin_left = 8
MarginContainer/constants/margin_right = 8
MarginContainer/constants/margin_top = 4
Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5")
PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm")
[sub_resource type="Animation" id="Animation_i6nbm"]
resource_name = "progress"
loop_mode = 1
step = 0.1
tracks/0/type = "bezier"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Progress:position:y")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"handle_modes": PackedInt32Array(0, 0, 0, 0, 0),
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 0, -0.25, 0, 0.2, -0.0157438, -1, -0.2, 0.0112069, 0.25, 0, -1, -0.25, 0, 0.25, 0, 0, -0.2, 0.00299701, 0.25, 0),
"times": PackedFloat32Array(0, 0.1, 0.5, 0.6, 1)
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_6b6c8"]
_data = {
&"progress": SubResource("Animation_i6nbm")
}
[node name="ExampleBalloon" type="CanvasLayer"]
layer = 100
@@ -103,31 +124,32 @@ grow_horizontal = 2
grow_vertical = 2
theme = SubResource("Theme_qq3yp")
[node name="Panel" type="Panel" parent="Balloon"]
[node name="MarginContainer" type="MarginContainer" parent="Balloon"]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 3.0
offset_top = -62.0
offset_right = -4.0
offset_bottom = -4.0
offset_top = -71.0
grow_horizontal = 2
grow_vertical = 0
[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"]
clip_children = 2
layout_mode = 2
mouse_filter = 1
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"]
[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"]
layout_mode = 2
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"]
[node name="HBoxContainer" type="HBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2
@@ -137,37 +159,52 @@ text = "Character"
fit_content = true
scroll_active = false
[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_hfvdi")]
[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/VBoxContainer" instance=ExtResource("2_hfvdi")]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
text = "Dialogue..."
skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex")
[node name="Responses" type="MarginContainer" parent="Balloon"]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -124.0
offset_top = -218.0
offset_right = 125.0
offset_bottom = -50.0
grow_horizontal = 2
grow_vertical = 0
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses"]
unique_name_in_owner = true
[node name="Control" type="Control" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer"]
custom_minimum_size = Vector2(5, 3)
layout_mode = 2
size_flags_vertical = 8
[node name="Progress" type="Polygon2D" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/Control"]
unique_name_in_owner = true
position = Vector2(0, -0.455998)
polygon = PackedVector2Array(0, 0, 2.5, 3, 5, 0)
[node name="AnimationPlayer" type="AnimationPlayer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/HBoxContainer/Control"]
libraries = {
&"": SubResource("AnimationLibrary_6b6c8")
}
autoplay = "progress"
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -116.5
offset_top = -9.0
offset_right = 116.5
offset_bottom = 9.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 8
theme_override_constants/separation = 2
script = ExtResource("3_1j1j0")
[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"]
[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"]
layout_mode = 2
text = "Response Example"
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"]
[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"]
+26
View File
@@ -0,0 +1,26 @@
class_name DMExportPlugin extends EditorExportPlugin
const IGNORED_PATHS = [
"/assets",
"/components",
"/views",
"inspector_plugin",
"test_scene"
]
func _get_name() -> String:
return "Dialogue Manager Export Plugin"
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path()
# 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()
@@ -0,0 +1 @@
uid://sa55ra11ji2q
+22 -28
View File
@@ -1,20 +1,19 @@
@tool
extends EditorImportPlugin
class_name DMImportPlugin extends EditorImportPlugin
signal compiled_resource(resource: Resource)
const DialogueResource = preload("./dialogue_resource.gd")
const DialogueManagerParser = preload("./components/parser.gd")
const DialogueManagerParseResult = preload("./components/parse_result.gd")
const compiler_version = 13
const COMPILER_VERSION = 15
func _get_importer_name() -> String:
# NOTE: A change to this forces a re-import of all dialogue
return "dialogue_manager_compiler_%s" % compiler_version
return "dialogue_manager"
func _get_format_version() -> int:
return COMPILER_VERSION
func _get_visible_name() -> String:
@@ -63,7 +62,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:
var cache = Engine.get_meta("DialogueCache")
var cache = Engine.get_meta("DMCache")
# Get the raw file contents
if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND
@@ -73,17 +72,12 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo
cache.file_content_changed.emit(source_file, raw_text)
# Parse the text
var parser: DialogueManagerParser = DialogueManagerParser.new()
var err: Error = parser.parse(raw_text, source_file)
var data: DialogueManagerParseResult = parser.get_data()
var errors: Array[Dictionary] = parser.get_errors()
parser.free()
if err != OK:
printerr("%d errors found in %s" % [errors.size(), source_file])
cache.add_errors_to_file(source_file, errors)
return err
# Compile the text
var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file)
if result.errors.size() > 0:
printerr("%d errors found in %s" % [result.errors.size(), source_file])
cache.add_errors_to_file(source_file, result.errors)
return OK
# Get the current addon version
var config: ConfigFile = ConfigFile.new()
@@ -94,17 +88,17 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo
var resource: DialogueResource = DialogueResource.new()
resource.set_meta("dialogue_manager_version", version)
resource.using_states = data.using_states
resource.titles = data.titles
resource.first_title = data.first_title
resource.character_names = data.character_names
resource.lines = data.lines
resource.raw_text = data.raw_text
resource.using_states = result.using_states
resource.titles = result.titles
resource.first_title = result.first_title
resource.character_names = result.character_names
resource.lines = result.lines
resource.raw_text = result.raw_text
# 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)
@@ -0,0 +1 @@
uid://dhwpj6ed8soyq
+2 -2
View File
@@ -1,5 +1,5 @@
@tool
extends EditorInspectorPlugin
class_name DMInspectorPlugin extends EditorInspectorPlugin
const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd")
@@ -7,7 +7,7 @@ const DialogueEditorProperty = preload("./components/editor_property/editor_prop
func _can_handle(object) -> bool:
if object is GDScript: return false
if not object is Node: return false
if not object is Node and not object is Resource: return false
if "name" in object and object.name == "Dialogue Manager": return false
return true
@@ -0,0 +1 @@
uid://0x31sbqbikov
+52 -85
View File
@@ -5,7 +5,7 @@ msgstr ""
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@@ -33,11 +33,17 @@ msgstr "Clear recent files"
msgid "save_all_files"
msgstr "Save all files"
msgid "all"
msgstr "All"
msgid "find_in_files"
msgstr "Find in files..."
msgstr "Find in 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"
msgstr "Search for text"
@@ -48,9 +54,6 @@ msgstr "Insert"
msgid "translations"
msgstr "Translations"
msgid "settings"
msgstr "Settings"
msgid "sponsor"
msgstr "Sponsor"
@@ -105,6 +108,15 @@ msgstr "End dialogue"
msgid "generate_line_ids"
msgstr "Generate line IDs"
msgid "use_uuid_only_for_ids"
msgstr "Use UUID only for IDs"
msgid "set_id_prefix_length"
msgstr "Set ID prefix length"
msgid "id_prefix_length"
msgstr "ID prefix length:"
msgid "save_characters_to_csv"
msgstr "Save character names to CSV..."
@@ -144,87 +156,12 @@ msgstr "Copy file path"
msgid "buffer.show_in_filesystem"
msgstr "Show in FileSystem"
msgid "settings.invalid_test_scene"
msgstr "\"{path}\" does not extend BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Revert to default test scene"
msgid "settings.default_balloon_hint"
msgstr "Custom balloon to use when calling \"DialogueManager.show_balloon()\""
msgid "settings.revert_to_default_balloon"
msgstr "Revert to default balloon"
msgid "settings.default_balloon_path"
msgstr "<example balloon>"
msgid "settings.autoload"
msgstr "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"
msgstr "{index} of {total}"
msgid "search.find_in_dialogue"
msgstr "Find in Dialogue"
msgid "search.find"
msgstr "Find:"
@@ -384,6 +321,30 @@ msgstr "Invalid index."
msgid "errors.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.unexpected_syntax_on_nested_dialogue_line"
msgstr "Nested dialogue lines may only contain dialogue."
msgid "errors.err_nested_dialogue_invalid_jump"
msgstr "Only the last line of nested dialogue is allowed to include a jump."
msgid "errors.missing_resource_for_autostart"
msgstr "You need to specify a dialogue resource when using auto-start."
msgid "errors.unknown"
msgstr "Unknown syntax."
@@ -478,4 +439,10 @@ msgid "runtime.unsupported_array_type"
msgstr "Array[{type}] isn't supported in mutations. Use Array as a type instead."
msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "Your dialogue balloon is missing a \"start\" or \"Start\" method."
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"
+9 -79
View File
@@ -41,9 +41,6 @@ msgstr "Insertar"
msgid "translations"
msgstr "Traducciones"
msgid "settings"
msgstr "Ajustes"
msgid "show_support"
msgstr "Contribuye con Dialogue Manager"
@@ -95,6 +92,15 @@ msgstr "Finalizar diálogo"
msgid "generate_line_ids"
msgstr "Generar IDs de línea"
msgid "use_uuid_only_for_ids"
msgstr "Usar solo UUID como ID"
msgid "set_id_prefix_length"
msgstr "Establecer la longitud del prefijo de ID"
msgid "id_prefix_length"
msgstr "Longitud del prefijo de ID:"
msgid "save_characters_to_csv"
msgstr "Guardar los nombres de los personajes en un archivo CSV..."
@@ -134,82 +140,6 @@ msgstr "Copiar la ruta del archivo"
msgid "buffer.show_in_filesystem"
msgstr "Mostrar en el sistema de archivos"
msgid "settings.invalid_test_scene"
msgstr "\"{path}\" no extiende BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Revertir a la escena de prueba por defecto"
msgid "settings.default_balloon_hint"
msgstr ""
"Globo personalizado para usar al llamar a \"DialogueManager.show_balloon()\""
msgid "settings.revert_to_default_balloon"
msgstr "Volver al globo predeterminado"
msgid "settings.default_balloon_path"
msgstr "<globo de ejemplo>"
msgid "settings.autoload"
msgstr "Autocarga"
msgid "settings.path"
msgstr "Ruta"
msgid "settings.new_template"
msgstr "Los nuevos archivos de diálogo empezarán con una plantilla"
msgid "settings.missing_keys"
msgstr "Tratar las claves de traducción faltantes como errores"
msgid "settings.missing_keys_hint"
msgstr "Si estás utilizando claves de traducción estáticas, tener esta opción habilitada te ayudará a encontrar cualquier línea a la que aún no le hayas añadido una clave."
msgid "settings.characters_translations"
msgstr "Exportar nombres de personajes en archivos de traducción"
msgid "settings.wrap_long_lines"
msgstr "Romper líneas largas"
msgid "settings.include_failed_responses"
msgstr "Incluir respuestas con condiciones fallidas"
msgid "settings.ignore_missing_state_values"
msgstr "Omitir errores de valores de estado faltantes (no recomendado)"
msgid "settings.custom_test_scene"
msgstr "Escena de prueba personalizada (debe extender BaseDialogueTestScene)"
msgid "settings.default_csv_locale"
msgstr "Localización CSV por defecto"
msgid "settings.states_shortcuts"
msgstr "Atajos de teclado"
msgid "settings.states_message"
msgstr "Si un autoload está habilitado aquí, puedes referirte a sus propiedades y métodos sin tener que usar su nombre."
msgid "settings.states_hint"
msgstr "ie. En lugar de \"SomeState.some_property\" podría simplemente usar \"some_property\""
msgid "settings.recompile_warning"
msgstr "Cambiar estos ajustes obligará a recompilar todo el diálogo. Hazlo solo si sabes lo que estás haciendo."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Crear línea de diálogo para respuestas con nombres de personajes dentro."
msgid "settings.open_in_external_editor"
msgstr "Abrir archivos de diálogo en el editor externo"
msgid "settings.external_editor_warning"
msgstr "Nota: El resaltado de sintaxis y la verificación detallada de errores no están soportados en editores externos."
msgid "settings.include_characters_in_translations"
msgstr "Incluir nombres de personajes en las exportaciones de traducción"
msgid "settings.include_notes_in_translations"
msgstr "Incluir notas (## comentarios) en las exportaciones de traducción"
msgid "n_of_n"
msgstr "{index} de {total}"
+48 -81
View File
@@ -26,12 +26,18 @@ msgstr ""
msgid "save_all_files"
msgstr ""
msgid "all"
msgstr ""
msgid "find_in_files"
msgstr ""
msgid "test_dialogue"
msgstr ""
msgid "test_dialogue_from_line"
msgstr ""
msgid "search_for_text"
msgstr ""
@@ -41,9 +47,6 @@ msgstr ""
msgid "translations"
msgstr ""
msgid "settings"
msgstr ""
msgid "sponsor"
msgstr ""
@@ -98,6 +101,15 @@ msgstr ""
msgid "generate_line_ids"
msgstr ""
msgid "use_uuid_only_for_ids"
msgstr ""
msgid "set_id_prefix_length"
msgstr ""
msgid "id_prefix_length"
msgstr ""
msgid "save_to_csv"
msgstr ""
@@ -134,87 +146,12 @@ msgstr ""
msgid "buffer.show_in_filesystem"
msgstr ""
msgid "settings.invalid_test_scene"
msgstr ""
msgid "settings.revert_to_default_test_scene"
msgstr ""
msgid "settings.default_balloon_hint"
msgstr ""
msgid "settings.revert_to_default_balloon"
msgstr ""
msgid "settings.default_balloon_path"
msgstr ""
msgid "settings.autoload"
msgstr ""
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"
msgstr ""
msgid "search.find_in_dialogue"
msgstr ""
msgid "search.find"
msgstr ""
@@ -374,6 +311,30 @@ msgstr ""
msgid "errors.unexpected_assignment"
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.unexpected_syntax_on_nested_dialogue_line"
msgstr ""
msgid "errors.err_nested_dialogue_invalid_jump"
msgstr ""
msgid "errors.missing_resource_for_autostart"
msgstr ""
msgid "errors.unknown"
msgstr ""
@@ -468,4 +429,10 @@ msgid "runtime.unsupported_array_type"
msgstr ""
msgid "runtime.dialogue_balloon_missing_start_method"
msgstr ""
msgid "runtime.top_level_states_share_name"
msgstr ""
msgid "translation_plugin.character_name"
msgstr ""
+50 -125
View File
@@ -3,13 +3,13 @@ msgstr ""
"Project-Id-Version: Dialogue Manager\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: veydzh3r <veydzherdgswift008@gmail.com>\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\n"
"X-Generator: Poedit 3.2.2\n"
msgid "start_a_new_file"
msgstr "Створити новий файл"
@@ -36,7 +36,10 @@ msgid "find_in_files"
msgstr "Знайти у файлах..."
msgid "test_dialogue"
msgstr "Відтворити діалог"
msgstr "Протестувати діалог з початку файлу"
msgid "test_dialogue_from_line"
msgstr "Протестувати діалог з поточного рядка"
msgid "search_for_text"
msgstr "Пошук тексту"
@@ -47,9 +50,6 @@ msgstr "Вставити"
msgid "translations"
msgstr "Переклади"
msgid "settings"
msgstr "Налаштування"
msgid "sponsor"
msgstr "Спонсор"
@@ -104,6 +104,15 @@ msgstr "Кінець діалогу"
msgid "generate_line_ids"
msgstr "Згенерувати ідентифікатори рядків"
msgid "use_uuid_only_for_ids"
msgstr "Використовувати лише UUID як ID"
msgid "set_id_prefix_length"
msgstr "Встановити довжину префікса ID"
msgid "id_prefix_length"
msgstr "Довжина префікса ID:"
msgid "save_characters_to_csv"
msgstr "Зберегти імена персонажів в CSV..."
@@ -143,97 +152,6 @@ msgstr "Копіювати шлях файлу"
msgid "buffer.show_in_filesystem"
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"
msgstr "{index} з {total}"
@@ -289,8 +207,7 @@ msgid "errors_in_script"
msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз."
msgid "errors_with_build"
msgstr ""
"Вам потрібно виправити помилки в діалогах, перш ніж ви зможете запустити гру."
msgstr "Вам потрібно виправити помилки в діалогах, перш ніж ви зможете запустити гру."
msgid "errors.import_errors"
msgstr "В імпортованому файлі є помилки."
@@ -397,6 +314,27 @@ msgstr "Недійсний індекс."
msgid "errors.unexpected_assignment"
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.unexpected_syntax_on_nested_dialogue_line"
msgstr "Вкладені рядки діалогу можуть містити лише діалог."
msgid "errors.err_nested_dialogue_invalid_jump"
msgstr "Лише останній рядок вкладеного діалогу може містити перехід."
msgid "errors.unknown"
msgstr "Невідомий синтаксис."
@@ -446,9 +384,7 @@ msgid "runtime.error_detail"
msgstr "Рядок {line}: {message}"
msgid "runtime.errors_see_details"
msgstr ""
"У тексті діалогу було виявлено помилки ({count}). Див. детальніше у розділі "
"«Вивід»."
msgstr "У тексті діалогу було виявлено помилки ({count}). Див. детальніше у розділі «Вивід»."
msgid "runtime.invalid_expression"
msgstr "«{expression}» не є допустимим виразом: {error}"
@@ -463,29 +399,16 @@ msgid "runtime.key_not_found"
msgstr "Ключ «{key}» не знайдено у словнику «{dictionary}»"
msgid "runtime.property_not_found"
msgstr ""
"«{property}» не знайдено. Стани з безпосередньо доступними властивостями/"
"методами/сигналами включають {states}. На автозавантаження потрібно "
"посилатися за їхніми назвами для використання їхніх властивостей."
msgstr "«{property}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.property_not_found_missing_export"
msgstr ""
"«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». "
"Стани з безпосередньо доступними властивостями/методами/сигналами включають "
"{states}. На автозавантаження потрібно посилатися за їхніми назвами для "
"використання їхніх властивостей."
msgstr "«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.method_not_found"
msgstr ""
"Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/"
"методами/сигналами включають {states}. На автозавантаження потрібно "
"посилатися за їхніми назвами для використання їхніх властивостей."
msgstr "Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.signal_not_found"
msgstr ""
"Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними "
"властивостями/методами/сигналами включають {states}. На автозавантаження "
"потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgstr "Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.method_not_callable"
msgstr "«{method}» не є методом, який можна викликати в «{object}»"
@@ -500,14 +423,16 @@ msgid "runtime.something_went_wrong"
msgstr "Щось пішло не так."
msgid "runtime.expected_n_got_n_args"
msgstr ""
"«{method}» було викликано з аргументами «{received}», але воно має лише "
"«{expected}»."
msgstr "«{method}» було викликано з аргументами «{received}», але воно має лише «{expected}»."
msgid "runtime.unsupported_array_type"
msgstr ""
"Array[{type}] не підтримується у модифікаціях. Натомість використовуйте "
"Array як тип."
msgstr "Array[{type}] не підтримується у модифікаціях. Натомість використовуйте Array як тип."
msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "У вашій кулі діалогу відсутній метод «start» або «Start»."
msgid "runtime.top_level_states_share_name"
msgstr "Кілька станів верхнього рівня ({states}) мають спільну назву методу/властивості/сигналу «{key}». Для діалогу доступний лише перший випадок."
msgid "translation_plugin.character_name"
msgstr "Ім’я персонажа"
+9 -66
View File
@@ -44,9 +44,6 @@ msgstr "插入"
msgid "translations"
msgstr "翻译"
msgid "settings"
msgstr "设置"
msgid "show_support"
msgstr "支持 Dialogue Manager"
@@ -98,6 +95,15 @@ msgstr "结束对话"
msgid "generate_line_ids"
msgstr "生成行 ID"
msgid "use_uuid_only_for_ids"
msgstr "仅使用UUID作为ID"
msgid "set_id_prefix_length"
msgstr "设置ID前缀长度"
msgid "id_prefix_length"
msgstr "ID前缀长度:"
msgid "save_characters_to_csv"
msgstr "保存角色到 CSV"
@@ -137,69 +143,6 @@ msgstr "复制文件路径"
msgid "buffer.show_in_filesystem"
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"
msgstr "第{index}个,共{total}个"
+9 -66
View File
@@ -44,9 +44,6 @@ msgstr "插入"
msgid "translations"
msgstr "翻譯"
msgid "settings"
msgstr "設定"
msgid "show_support"
msgstr "支援 Dialogue Manager"
@@ -98,6 +95,15 @@ msgstr "結束對話"
msgid "generate_line_ids"
msgstr "生成行 ID"
msgid "use_uuid_only_for_ids"
msgstr "僅使用 UUID 作為 ID"
msgid "set_id_prefix_length"
msgstr "設定 ID 前綴長度"
msgid "id_prefix_length"
msgstr "ID 前綴長度:"
msgid "save_characters_to_csv"
msgstr "保存角色到 CSV"
@@ -137,69 +143,6 @@ msgstr "複製檔案位置"
msgid "buffer.show_in_filesystem"
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"
msgstr "第{index}個,共{total}個"
+2 -2
View File
@@ -1,7 +1,7 @@
[plugin]
name="Dialogue Manager"
description="A simple but powerful branching dialogue system"
description="A powerful nonlinear dialogue system"
author="Nathan Hoad"
version="2.45.0"
version="3.9.0"
script="plugin.gd"
+1
View File
@@ -0,0 +1 @@
uid://hrny2utekhei

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