更新 dialogue manager v3.4.0 for Godot 4.4

This commit is contained in:
cakipaul 2025-03-10 20:58:01 +08:00
parent 996d826877
commit a0792afb81
201 changed files with 3985 additions and 4335 deletions

View File

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

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://cggqb75a8w8r"] [gd_scene load_steps=3 format=3 uid="uid://cggqb75a8w8r"]
[ext_resource type="Script" path="res://addons/debug_menu/debug_menu.gd" id="1_p440y"] [ext_resource type="Script" uid="uid://pjsl6kq3jfwh" path="res://addons/debug_menu/debug_menu.gd" id="1_p440y"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ki0n8"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ki0n8"]
bg_color = Color(0, 0, 0, 0.25098) bg_color = Color(0, 0, 0, 0.25098)

View File

@ -0,0 +1 @@
uid://7hcv3wxdt3h0

View File

@ -16,13 +16,15 @@ namespace DialogueManagerRuntime
PO 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 PassedTitleEventHandler(string title);
public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); public delegate void GotDialogueEventHandler(DialogueLine dialogueLine);
public delegate void MutatedEventHandler(Dictionary mutation); public delegate void MutatedEventHandler(Dictionary mutation);
public delegate void DialogueEndedEventHandler(Resource dialogueResource); public delegate void DialogueEndedEventHandler(Resource dialogueResource);
public static DialogueStartedEventHandler? DialogueStarted;
public static PassedTitleEventHandler? PassedTitle; public static PassedTitleEventHandler? PassedTitle;
public static GotDialogueEventHandler? GotDialogue; public static GotDialogueEventHandler? GotDialogue;
public static MutatedEventHandler? Mutated; public static MutatedEventHandler? Mutated;
@ -38,6 +40,7 @@ namespace DialogueManagerRuntime
if (instance == null) if (instance == null)
{ {
instance = Engine.GetSingleton("DialogueManager"); instance = Engine.GetSingleton("DialogueManager");
instance.Connect("bridge_dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource)));
} }
return instance; return instance;
} }
@ -86,11 +89,6 @@ namespace DialogueManagerRuntime
instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource))); instance.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource)));
} }
public void Prepare()
{
Prepare(Instance);
}
public static async Task<GodotObject> GetSingleton() public static async Task<GodotObject> GetSingleton()
{ {
@ -170,16 +168,32 @@ namespace DialogueManagerRuntime
} }
public bool ThingHasMethod(GodotObject thing, string method) public bool ThingHasMethod(GodotObject thing, string method, Array<Variant> args)
{ {
MethodInfo? info = thing.GetType().GetMethod(method, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
return info != null; foreach (var methodInfo in methodInfos)
{
if (methodInfo.Name == method && args.Count == methodInfo.GetParameters().Length)
{
return true;
}
}
return false;
} }
public async void ResolveThingMethod(GodotObject thing, string method, Array<Variant> args) 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().Length)
{
info = methodInfo;
}
}
if (info == null) return; if (info == null) return;
@ -216,18 +230,15 @@ namespace DialogueManagerRuntime
if (result is Task taskResult) if (result is Task taskResult)
{ {
// await Tasks and handle result if it is a Task<T>
await taskResult; await taskResult;
var taskType = taskResult.GetType(); try
if (taskType.IsGenericType && taskType.GetGenericTypeDefinition() == typeof(Task<>))
{ {
var resultProperty = taskType.GetProperty("Result"); Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult);
var taskResultValue = resultProperty.GetValue(taskResult); EmitSignal(SignalName.Resolved, value);
EmitSignal(SignalName.Resolved, (Variant)taskResultValue);
} }
else catch (Exception err)
{ {
EmitSignal(SignalName.Resolved, null); EmitSignal(SignalName.Resolved);
} }
} }
else else
@ -313,6 +324,12 @@ namespace DialogueManagerRuntime
get => inline_mutations; 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>(); private Array<Variant> extra_game_states = new Array<Variant>();
public Array<Variant> ExtraGameStates public Array<Variant> ExtraGameStates
{ {
@ -338,6 +355,11 @@ namespace DialogueManagerRuntime
time = (string)data.Get("time"); time = (string)data.Get("time");
tags = (Array<string>)data.Get("tags"); 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")) foreach (var response in (Array<RefCounted>)data.Get("responses"))
{ {
responses.Add(new DialogueResponse(response)); responses.Add(new DialogueResponse(response));

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
@tool @tool
extends CodeEdit class_name DMCodeEdit extends CodeEdit
signal active_title_change(title: String) signal active_title_change(title: String)
@ -7,10 +7,6 @@ signal error_clicked(line_number: int)
signal external_file_requested(path: String, title: String) signal external_file_requested(path: String, title: String)
const DialogueManagerParser = preload("./parser.gd")
const DialogueSyntaxHighlighter = preload("./code_edit_syntax_highlighter.gd")
# A link back to the owner `MainView` # A link back to the owner `MainView`
var main_view var main_view
@ -19,7 +15,7 @@ var theme_overrides: Dictionary:
set(value): set(value):
theme_overrides = value theme_overrides = value
syntax_highlighter = DialogueSyntaxHighlighter.new() syntax_highlighter = DMSyntaxHighlighter.new()
# General UI # General UI
add_theme_color_override("font_color", theme_overrides.text_color) add_theme_color_override("font_color", theme_overrides.text_color)
@ -67,7 +63,7 @@ func _ready() -> void:
if not has_comment_delimiter("#"): if not has_comment_delimiter("#"):
add_comment_delimiter("#", "", true) add_comment_delimiter("#", "", true)
syntax_highlighter = DialogueSyntaxHighlighter.new() syntax_highlighter = DMSyntaxHighlighter.new()
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
@ -162,17 +158,18 @@ func _request_code_completion(force: bool) -> void:
add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons")) add_code_completion_option(CodeEdit.KIND_CLASS, "END!", "END!".substr(prompt.length()), theme_overrides.text_color, get_theme_icon("Stop", "EditorIcons"))
# Get all titles, including those in imports # Get all titles, including those in imports
var parser: DialogueManagerParser = DialogueManagerParser.new() for title: String in DMCompiler.get_titles_in_text(text, main_view.current_file_path):
parser.prepare(text, main_view.current_file_path, false) # Ignore any imported titles that aren't resolved to human readable.
for title in parser.titles: if title.to_int() > 0:
if "/" in title: continue
elif "/" in title:
var bits = title.split("/") var bits = title.split("/")
if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]): if matches_prompt(prompt, bits[0]) or matches_prompt(prompt, bits[1]):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons")) add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons"))
elif matches_prompt(prompt, title): elif matches_prompt(prompt, title):
add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons")) add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons"))
update_code_completion_options(true) update_code_completion_options(true)
parser.free()
return return
var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "") var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "")
@ -232,6 +229,7 @@ func get_titles() -> PackedStringArray:
for line in lines: for line in lines:
if line.strip_edges().begins_with("~ "): if line.strip_edges().begins_with("~ "):
titles.append(line.strip_edges().substr(2)) titles.append(line.strip_edges().substr(2))
return titles return titles
@ -270,6 +268,11 @@ func get_character_names(beginning_with: String) -> PackedStringArray:
# Mark a line as an error or not # Mark a line as an error or not
func mark_line_as_error(line_number: int, is_error: bool) -> void: func mark_line_as_error(line_number: int, is_error: bool) -> void:
# Lines display counting from 1 but are actually indexed from 0
line_number -= 1
if line_number < 0: return
if is_error: if is_error:
set_line_background_color(line_number, theme_overrides.error_line_color) set_line_background_color(line_number, theme_overrides.error_line_color)
set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons")) set_line_gutter_icon(line_number, 0, get_theme_icon("StatusError", "EditorIcons"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"] [gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"]
[ext_resource type="Script" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] [ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"]
[ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"] [ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"]
[node name="FilesList" type="VBoxContainer"] [node name="FilesList" type="VBoxContainer"]

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,23 @@
extends Node class_name DMConstants extends RefCounted
const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json" const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json"
const CACHE_PATH = "user://dialogue_manager_cache.json" const CACHE_PATH = "user://dialogue_manager_cache.json"
enum MutationBehaviour {
Wait,
DoNotWait,
Skip
}
enum TranslationSource {
None,
Guess,
CSV,
PO
}
# Token types # Token types
const TOKEN_FUNCTION = &"function" const TOKEN_FUNCTION = &"function"
@ -33,21 +47,26 @@ const TOKEN_NUMBER = &"number"
const TOKEN_VARIABLE = &"variable" const TOKEN_VARIABLE = &"variable"
const TOKEN_COMMENT = &"comment" const TOKEN_COMMENT = &"comment"
const TOKEN_VALUE = &"value"
const TOKEN_ERROR = &"error" const TOKEN_ERROR = &"error"
# Line types # Line types
const TYPE_UNKNOWN = &"unknown" const TYPE_UNKNOWN = &""
const TYPE_IMPORT = &"import"
const TYPE_COMMENT = &"comment"
const TYPE_RESPONSE = &"response" const TYPE_RESPONSE = &"response"
const TYPE_TITLE = &"title" const TYPE_TITLE = &"title"
const TYPE_CONDITION = &"condition" const TYPE_CONDITION = &"condition"
const TYPE_WHILE = &"while"
const TYPE_MATCH = &"match"
const TYPE_WHEN = &"when"
const TYPE_MUTATION = &"mutation" const TYPE_MUTATION = &"mutation"
const TYPE_GOTO = &"goto" const TYPE_GOTO = &"goto"
const TYPE_DIALOGUE = &"dialogue" const TYPE_DIALOGUE = &"dialogue"
const TYPE_RANDOM = &"random"
const TYPE_ERROR = &"error" const TYPE_ERROR = &"error"
const TYPE_ELSE = &"else"
# Line IDs # Line IDs
const ID_NULL = &"" const ID_NULL = &""
@ -94,6 +113,11 @@ const ERR_UNEXPECTED_VARIABLE = 132
const ERR_INVALID_INDEX = 133 const ERR_INVALID_INDEX = 133
const ERR_UNEXPECTED_ASSIGNMENT = 134 const ERR_UNEXPECTED_ASSIGNMENT = 134
const ERR_UNKNOWN_USING = 135 const ERR_UNKNOWN_USING = 135
const ERR_EXPECTED_WHEN_OR_ELSE = 136
const ERR_ONLY_ONE_ELSE_ALLOWED = 137
const ERR_WHEN_MUST_BELONG_TO_MATCH = 138
const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139
const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140
## Get the error message ## Get the error message
@ -169,14 +193,22 @@ static func get_error_message(error: int) -> String:
return translate(&"errors.unexpected_assignment") return translate(&"errors.unexpected_assignment")
ERR_UNKNOWN_USING: ERR_UNKNOWN_USING:
return translate(&"errors.unknown_using") return translate(&"errors.unknown_using")
ERR_EXPECTED_WHEN_OR_ELSE:
return translate(&"errors.expected_when_or_else")
ERR_ONLY_ONE_ELSE_ALLOWED:
return translate(&"errors.only_one_else_allowed")
ERR_WHEN_MUST_BELONG_TO_MATCH:
return translate(&"errors.when_must_belong_to_match")
ERR_CONCURRENT_LINE_WITHOUT_ORIGIN:
return translate(&"errors.concurrent_line_without_origin")
ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES:
return translate(&"errors.goto_not_allowed_on_concurrect_lines")
return translate(&"errors.unknown") return translate(&"errors.unknown")
static func translate(string: String) -> String: static func translate(string: String) -> String:
var temp_node = new() var base_path = new().get_script().resource_path.get_base_dir()
var base_path = temp_node.get_script().resource_path.get_base_dir()
temp_node.free()
var language: String = TranslationServer.get_tool_locale() var language: String = TranslationServer.get_tool_locale()
var translations_path: String = "%s/l10n/%s.po" % [base_path, language] var translations_path: String = "%s/l10n/%s.po" % [base_path, language]

View File

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

View File

@ -187,12 +187,13 @@ func _mutate_inline_mutations(index: int) -> void:
if inline_mutation[0] > index: if inline_mutation[0] > index:
return return
if inline_mutation[0] == index and not _already_mutated_indices.has(index): if inline_mutation[0] == index and not _already_mutated_indices.has(index):
_already_mutated_indices.append(index)
_is_awaiting_mutation = true _is_awaiting_mutation = true
# The DialogueManager can't be referenced directly here so we need to get it by its path # The DialogueManager can't be referenced directly here so we need to get it by its path
await Engine.get_singleton("DialogueManager").mutate(inline_mutation[1], dialogue_line.extra_game_states, true) await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true)
_is_awaiting_mutation = false _is_awaiting_mutation = false
_already_mutated_indices.append(index)
# Determine if the current autopause character at the cursor should qualify to pause typing. # Determine if the current autopause character at the cursor should qualify to pause typing.
func _should_auto_pause() -> bool: func _should_auto_pause() -> bool:

View File

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

View File

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

17
addons/dialogue_manager/dialogue_line.gd Normal file → Executable file
View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -5,7 +5,6 @@
class_name DialogueResource extends Resource class_name DialogueResource extends Resource
const _DialogueManager = preload("./dialogue_manager.gd")
const DialogueLine = preload("./dialogue_line.gd") const DialogueLine = preload("./dialogue_line.gd")
## A list of state shortcuts ## A list of state shortcuts
@ -30,7 +29,7 @@ const DialogueLine = preload("./dialogue_line.gd")
## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can ## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can
## be a title string or a stringified line number). Runs any mutations along the way and then returns ## be a title string or a stringified line number). Runs any mutations along the way and then returns
## the first dialogue line encountered. ## the first dialogue line encountered.
func get_next_dialogue_line(title: String, extra_game_states: Array = [], mutation_behaviour: _DialogueManager.MutationBehaviour = _DialogueManager.MutationBehaviour.Wait) -> DialogueLine: func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine:
return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour) return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour)
@ -39,23 +38,5 @@ func get_titles() -> PackedStringArray:
return titles.keys() return titles.keys()
func get_ordered_titles() -> Array:
var splitted = raw_text.split("\n")
var ordered_titles = []
for line in splitted:
if line.begins_with("~ "):
ordered_titles.append(line.substr(2).strip_edges())
# # check ordered_titles consistency to titles
# for title in ordered_titles:
# if not titles.has(title):
# printerr("Title %s not found in titles" % title)
# ordered_titles.remove(title)
# for title in titles.keys():
# if not ordered_titles.has(title):
# printerr("Title %s not found in ordered_titles" % title)
# ordered_titles.append(title)
return ordered_titles
func _to_string() -> String: func _to_string() -> String:
return "<DialogueResource titles=\"%s\">" % [",".join(titles.keys())] return "<DialogueResource titles=\"%s\">" % [",".join(titles.keys())]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,10 +25,6 @@ namespace DialogueManagerRuntime
get => dialogueLine; get => dialogueLine;
set set
{ {
isWaitingForInput = false;
balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
if (value == null) if (value == null)
{ {
QueueFree(); QueueFree();
@ -36,10 +32,12 @@ namespace DialogueManagerRuntime
} }
dialogueLine = value; dialogueLine = value;
UpdateDialogue(); ApplyDialogueLine();
} }
} }
Timer MutationCooldown = new Timer();
public override void _Ready() public override void _Ready()
{ {
@ -88,6 +86,18 @@ namespace DialogueManagerRuntime
Next(response.NextId); Next(response.NextId);
})); }));
// Hide the balloon when a mutation is running
MutationCooldown.Timeout += () =>
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
AddChild(MutationCooldown);
DialogueManager.Mutated += OnMutated; DialogueManager.Mutated += OnMutated;
} }
@ -122,12 +132,7 @@ namespace DialogueManagerRuntime
public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null) public async void Start(Resource dialogueResource, string title, Array<Variant> extraGameStates = null)
{ {
if (!IsNodeReady()) temporaryGameStates = new Array<Variant> { this } + (extraGameStates ?? new Array<Variant>());
{
await ToSignal(this, SignalName.Ready);
}
temporaryGameStates = extraGameStates ?? new Array<Variant>();
isWaitingForInput = false; isWaitingForInput = false;
resource = dialogueResource; resource = dialogueResource;
@ -144,12 +149,13 @@ namespace DialogueManagerRuntime
#region Helpers #region Helpers
private async void UpdateDialogue() private async void ApplyDialogueLine()
{ {
if (!IsNodeReady()) MutationCooldown.Stop();
{
await ToSignal(this, SignalName.Ready); isWaitingForInput = false;
} balloon.FocusMode = Control.FocusModeEnum.All;
balloon.GrabFocus();
// Set up the character name // Set up the character name
characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character);
@ -208,14 +214,7 @@ namespace DialogueManagerRuntime
{ {
isWaitingForInput = false; isWaitingForInput = false;
willHideBalloon = true; willHideBalloon = true;
GetTree().CreateTimer(0.1f).Timeout += () => MutationCooldown.Start(0.1f);
{
if (willHideBalloon)
{
willHideBalloon = false;
balloon.Hide();
}
};
} }

View File

@ -0,0 +1 @@
uid://5b3w40kwakl3

View File

@ -26,55 +26,19 @@ var _locale: String = TranslationServer.get_locale()
## The current line ## The current line
var dialogue_line: DialogueLine: var dialogue_line: DialogueLine:
set(next_dialogue_line): set(value):
is_waiting_for_input = false if value:
balloon.focus_mode = Control.FOCUS_ALL dialogue_line = value
balloon.grab_focus() apply_dialogue_line()
# The dialogue has finished so close the balloon
if not next_dialogue_line:
queue_free()
return
# If the node isn't ready yet then none of the labels will be ready yet either
if not is_node_ready():
await ready
dialogue_line = next_dialogue_line
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
dialogue_label.hide()
dialogue_label.dialogue_line = dialogue_line
responses_menu.hide()
responses_menu.set_responses(dialogue_line.responses)
# Show our balloon
balloon.show()
will_hide_balloon = false
dialogue_label.show()
if not dialogue_line.text.is_empty():
dialogue_label.type_out()
await dialogue_label.finished_typing
# Wait for input
if dialogue_line.responses.size() > 0:
balloon.focus_mode = Control.FOCUS_NONE
responses_menu.show()
elif dialogue_line.time != "":
var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
await get_tree().create_timer(time).timeout
next(dialogue_line.next_id)
else: else:
is_waiting_for_input = true # The dialogue has finished so close the balloon
balloon.focus_mode = Control.FOCUS_ALL queue_free()
balloon.grab_focus()
get: get:
return dialogue_line return dialogue_line
## A cooldown timer for delaying the balloon hide when encountering a mutation.
var mutation_cooldown: Timer = Timer.new()
## The base balloon anchor ## The base balloon anchor
@onready var balloon: Control = %Balloon @onready var balloon: Control = %Balloon
@ -96,6 +60,9 @@ func _ready() -> void:
if responses_menu.next_action.is_empty(): if responses_menu.next_action.is_empty():
responses_menu.next_action = next_action responses_menu.next_action = next_action
mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout)
add_child(mutation_cooldown)
func _unhandled_input(_event: InputEvent) -> void: func _unhandled_input(_event: InputEvent) -> void:
# Only the balloon is allowed to handle input while it's showing # Only the balloon is allowed to handle input while it's showing
@ -114,14 +81,52 @@ func _notification(what: int) -> void:
## Start some dialogue ## Start some dialogue
func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void:
if not is_node_ready(): temporary_game_states = [self] + extra_game_states
await ready
temporary_game_states = [self] + extra_game_states
is_waiting_for_input = false is_waiting_for_input = false
resource = dialogue_resource resource = dialogue_resource
self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states)
## Apply any changes to the balloon given a new [DialogueLine].
func apply_dialogue_line() -> void:
mutation_cooldown.stop()
is_waiting_for_input = false
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
character_label.visible = not dialogue_line.character.is_empty()
character_label.text = tr(dialogue_line.character, "dialogue")
dialogue_label.hide()
dialogue_label.dialogue_line = dialogue_line
responses_menu.hide()
responses_menu.responses = dialogue_line.responses
# Show our balloon
balloon.show()
will_hide_balloon = false
dialogue_label.show()
if not dialogue_line.text.is_empty():
dialogue_label.type_out()
await dialogue_label.finished_typing
# Wait for input
if dialogue_line.responses.size() > 0:
balloon.focus_mode = Control.FOCUS_NONE
responses_menu.show()
elif dialogue_line.time != "":
var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float()
await get_tree().create_timer(time).timeout
next(dialogue_line.next_id)
else:
is_waiting_for_input = true
balloon.focus_mode = Control.FOCUS_ALL
balloon.grab_focus()
## Go to the next line ## Go to the next line
func next(next_id: String) -> void: func next(next_id: String) -> void:
self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states)
@ -130,14 +135,16 @@ func next(next_id: String) -> void:
#region Signals #region Signals
func _on_mutation_cooldown_timeout() -> void:
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
func _on_mutated(_mutation: Dictionary) -> void: func _on_mutated(_mutation: Dictionary) -> void:
is_waiting_for_input = false is_waiting_for_input = false
will_hide_balloon = true will_hide_balloon = true
get_tree().create_timer(0.1).timeout.connect(func(): mutation_cooldown.start(0.1)
if will_hide_balloon:
will_hide_balloon = false
balloon.hide()
)
func _on_balloon_gui_input(event: InputEvent) -> void: func _on_balloon_gui_input(event: InputEvent) -> void:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,9 @@ msgstr ""
msgid "test_dialogue" msgid "test_dialogue"
msgstr "" msgstr ""
msgid "test_dialogue_from_line"
msgstr ""
msgid "search_for_text" msgid "search_for_text"
msgstr "" msgstr ""
@ -41,9 +44,6 @@ msgstr ""
msgid "translations" msgid "translations"
msgstr "" msgstr ""
msgid "settings"
msgstr ""
msgid "sponsor" msgid "sponsor"
msgstr "" msgstr ""
@ -134,84 +134,6 @@ msgstr ""
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "" msgstr ""
msgid "settings.invalid_test_scene"
msgstr ""
msgid "settings.revert_to_default_test_scene"
msgstr ""
msgid "settings.default_balloon_hint"
msgstr ""
msgid "settings.revert_to_default_balloon"
msgstr ""
msgid "settings.default_balloon_path"
msgstr ""
msgid "settings.autoload"
msgstr ""
msgid "settings.path"
msgstr ""
msgid "settings.new_template"
msgstr ""
msgid "settings.missing_keys"
msgstr ""
msgid "settings.missing_keys_hint"
msgstr ""
msgid "settings.characters_translations"
msgstr ""
msgid "settings.wrap_long_lines"
msgstr ""
msgid "settings.include_failed_responses"
msgstr ""
msgid "settings.ignore_missing_state_values"
msgstr ""
msgid "settings.custom_test_scene"
msgstr ""
msgid "settings.default_csv_locale"
msgstr ""
msgid "settings.states_shortcuts"
msgstr ""
msgid "settings.states_message"
msgstr ""
msgid "settings.states_hint"
msgstr ""
msgid "settings.recompile_warning"
msgstr ""
msgid "settings.create_lines_for_responses_with_characters"
msgstr ""
msgid "settings.open_in_external_editor"
msgstr ""
msgid "settings.external_editor_warning"
msgstr ""
msgid "settings.include_characters_in_translations"
msgstr ""
msgid "settings.include_notes_in_translations"
msgstr ""
msgid "settings.check_for_updates"
msgstr ""
msgid "n_of_n" msgid "n_of_n"
msgstr "" msgstr ""
@ -374,6 +296,21 @@ msgstr ""
msgid "errors.unexpected_assignment" msgid "errors.unexpected_assignment"
msgstr "" msgstr ""
msgid "errors.expected_when_or_else"
msgstr ""
msgid "errors.only_one_else_allowed"
msgstr ""
msgid "errors.when_must_belong_to_match"
msgstr ""
msgid "errors.concurrent_line_without_origin"
msgstr ""
msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr ""
msgid "errors.unknown" msgid "errors.unknown"
msgstr "" msgstr ""
@ -468,4 +405,10 @@ msgid "runtime.unsupported_array_type"
msgstr "" msgstr ""
msgid "runtime.dialogue_balloon_missing_start_method" msgid "runtime.dialogue_balloon_missing_start_method"
msgstr ""
msgid "runtime.top_level_states_share_name"
msgstr ""
msgid "translation_plugin.character_name"
msgstr "" msgstr ""

View File

@ -3,13 +3,13 @@ msgstr ""
"Project-Id-Version: Dialogue Manager\n" "Project-Id-Version: Dialogue Manager\n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: veydzh3r <veydzherdgswift008@gmail.com>\n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: uk\n" "Language: uk\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\n" "X-Generator: Poedit 3.2.2\n"
msgid "start_a_new_file" msgid "start_a_new_file"
msgstr "Створити новий файл" msgstr "Створити новий файл"
@ -36,7 +36,10 @@ msgid "find_in_files"
msgstr "Знайти у файлах..." msgstr "Знайти у файлах..."
msgid "test_dialogue" msgid "test_dialogue"
msgstr "Відтворити діалог" msgstr "Протестувати діалог з початку файлу"
msgid "test_dialogue_from_line"
msgstr "Протестувати діалог з поточного рядка"
msgid "search_for_text" msgid "search_for_text"
msgstr "Пошук тексту" msgstr "Пошук тексту"
@ -47,9 +50,6 @@ msgstr "Вставити"
msgid "translations" msgid "translations"
msgstr "Переклади" msgstr "Переклади"
msgid "settings"
msgstr "Налаштування"
msgid "sponsor" msgid "sponsor"
msgstr "Спонсор" msgstr "Спонсор"
@ -143,97 +143,6 @@ msgstr "Копіювати шлях файлу"
msgid "buffer.show_in_filesystem" msgid "buffer.show_in_filesystem"
msgstr "Показати у файловій системі" msgstr "Показати у файловій системі"
msgid "settings.invalid_test_scene"
msgstr "«{path}» не у розширенні BaseDialogueTestScene."
msgid "settings.revert_to_default_test_scene"
msgstr "Повернути до типової тестової сцени"
msgid "settings.default_balloon_hint"
msgstr ""
"Нестандартна куля для використання під час виклику «DialogueManager."
"show_balloon()»"
msgid "settings.revert_to_default_balloon"
msgstr "Повернути до типової кулі"
msgid "settings.default_balloon_path"
msgstr "<приклад кулі>"
msgid "settings.autoload"
msgstr "Автозавантаження"
msgid "settings.path"
msgstr "Шлях"
msgid "settings.new_template"
msgstr "Нові файли діалогів починатимуться з шаблонного тексту"
msgid "settings.missing_keys"
msgstr "Вважати відсутні ключі перекладу як помилками"
msgid "settings.missing_keys_hint"
msgstr ""
"Якщо ви використовуєте статичні ключі перекладу, увімкнення цього параметра "
"допоможе вам знайти рядки, до яких ви ще не додали ключ."
msgid "settings.characters_translations"
msgstr "Експортовувати імена персонажів у файли перекладу"
msgid "settings.wrap_long_lines"
msgstr "Переносити довгі рядки"
msgid "settings.include_failed_responses"
msgstr "Включати відповіді з невдалими умовами"
msgid "settings.ignore_missing_state_values"
msgstr "Пропускати помилки пропущених значень стану (не рекомендується)"
msgid "settings.custom_test_scene"
msgstr ""
"Користувацька тестова сцена (має мати розширення «BaseDialogueTestScene»)"
msgid "settings.default_csv_locale"
msgstr "Типова мова файлу CSV"
msgid "settings.states_shortcuts"
msgstr "Скорочення станів"
msgid "settings.states_message"
msgstr ""
"Якщо автозавантаження увімкнено, ви можете звертатися до його властивостей і "
"методів без необхідності використовувати його назву."
msgid "settings.states_hint"
msgstr ""
"Тобто, замість «ЯкийсьСтан.якась_властивість» ви можете просто "
"використовувати «якась_властивість»"
msgid "settings.recompile_warning"
msgstr ""
"Зміна цих параметрів призведе до перекомпіляції усіх діалогів. Змінюйте їх, "
"тільки якщо ви знаєте, що робите."
msgid "settings.create_lines_for_responses_with_characters"
msgstr "Створювати дочірній рядок діалогу для відповідей з іменами персонажів"
msgid "settings.open_in_external_editor"
msgstr "Відкрити файли діалогів у зовнішньому редакторі"
msgid "settings.external_editor_warning"
msgstr ""
"Примітка: підсвічування синтаксису та детальна перевірка помилок не "
"підтримуються у зовнішніх редакторах."
msgid "settings.include_characters_in_translations"
msgstr "Включати імена персонажів до експорту перекладу"
msgid "settings.include_notes_in_translations"
msgstr "Включати примітки (## коментарі) до експорту перекладу"
msgid "settings.check_for_updates"
msgstr "Перевіряти наявність оновлень"
msgid "n_of_n" msgid "n_of_n"
msgstr "{index} з {total}" msgstr "{index} з {total}"
@ -289,8 +198,7 @@ msgid "errors_in_script"
msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз." msgstr "У вашому скрипті є помилки. Виправте їх і спробуйте ще раз."
msgid "errors_with_build" msgid "errors_with_build"
msgstr "" msgstr "Вам потрібно виправити помилки в діалогах, перш ніж ви зможете запустити гру."
"Вам потрібно виправити помилки в діалогах, перш ніж ви зможете запустити гру."
msgid "errors.import_errors" msgid "errors.import_errors"
msgstr "В імпортованому файлі є помилки." msgstr "В імпортованому файлі є помилки."
@ -397,6 +305,21 @@ msgstr "Недійсний індекс."
msgid "errors.unexpected_assignment" msgid "errors.unexpected_assignment"
msgstr "Несподіване призначення." msgstr "Несподіване призначення."
msgid "errors.expected_when_or_else"
msgstr "Очікувався випадок «when» або «else»."
msgid "errors.only_one_else_allowed"
msgstr "Для кожного «match» допускається лише один випадок «else»."
msgid "errors.when_must_belong_to_match"
msgstr "Оператори «when» можуть з’являтися лише як дочірні операторів «match»."
msgid "errors.concurrent_line_without_origin"
msgstr "Паралельні рядки потребують початкового рядка, який не починається з «|»."
msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr "У паралельних діалогових рядках не допускаються Goto посилання."
msgid "errors.unknown" msgid "errors.unknown"
msgstr "Невідомий синтаксис." msgstr "Невідомий синтаксис."
@ -446,9 +369,7 @@ msgid "runtime.error_detail"
msgstr "Рядок {line}: {message}" msgstr "Рядок {line}: {message}"
msgid "runtime.errors_see_details" msgid "runtime.errors_see_details"
msgstr "" msgstr "У тексті діалогу було виявлено помилки ({count}). Див. детальніше у розділі «Вивід»."
"У тексті діалогу було виявлено помилки ({count}). Див. детальніше у розділі "
"«Вивід»."
msgid "runtime.invalid_expression" msgid "runtime.invalid_expression"
msgstr "«{expression}» не є допустимим виразом: {error}" msgstr "«{expression}» не є допустимим виразом: {error}"
@ -463,29 +384,16 @@ msgid "runtime.key_not_found"
msgstr "Ключ «{key}» не знайдено у словнику «{dictionary}»" msgstr "Ключ «{key}» не знайдено у словнику «{dictionary}»"
msgid "runtime.property_not_found" msgid "runtime.property_not_found"
msgstr "" msgstr "«{property}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
"«{property}» не знайдено. Стани з безпосередньо доступними властивостями/"
"методами/сигналами включають {states}. На автозавантаження потрібно "
"посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.property_not_found_missing_export" msgid "runtime.property_not_found_missing_export"
msgstr "" msgstr "«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
"«{property}» не знайдено. Можливо, вам слід додати декоратор «[Export]». "
"Стани з безпосередньо доступними властивостями/методами/сигналами включають "
"{states}. На автозавантаження потрібно посилатися за їхніми назвами для "
"використання їхніх властивостей."
msgid "runtime.method_not_found" msgid "runtime.method_not_found"
msgstr "" msgstr "Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
"Метод «{method}» не знайдено. Стани з безпосередньо доступними властивостями/"
"методами/сигналами включають {states}. На автозавантаження потрібно "
"посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.signal_not_found" msgid "runtime.signal_not_found"
msgstr "" msgstr "Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними властивостями/методами/сигналами включають {states}. На автозавантаження потрібно посилатися за їхніми назвами для використання їхніх властивостей."
"Сигнал «{signal_name}» не знайдено. Стани з безпосередньо доступними "
"властивостями/методами/сигналами включають {states}. На автозавантаження "
"потрібно посилатися за їхніми назвами для використання їхніх властивостей."
msgid "runtime.method_not_callable" msgid "runtime.method_not_callable"
msgstr "«{method}» не є методом, який можна викликати в «{object}»" msgstr "«{method}» не є методом, який можна викликати в «{object}»"
@ -500,14 +408,16 @@ msgid "runtime.something_went_wrong"
msgstr "Щось пішло не так." msgstr "Щось пішло не так."
msgid "runtime.expected_n_got_n_args" msgid "runtime.expected_n_got_n_args"
msgstr "" msgstr "«{method}» було викликано з аргументами «{received}», але воно має лише «{expected}»."
"«{method}» було викликано з аргументами «{received}», але воно має лише "
"«{expected}»."
msgid "runtime.unsupported_array_type" msgid "runtime.unsupported_array_type"
msgstr "" msgstr "Array[{type}] не підтримується у модифікаціях. Натомість використовуйте Array як тип."
"Array[{type}] не підтримується у модифікаціях. Натомість використовуйте "
"Array як тип."
msgid "runtime.dialogue_balloon_missing_start_method" msgid "runtime.dialogue_balloon_missing_start_method"
msgstr "У вашій кулі діалогу відсутній метод «start» або «Start»." msgstr "У вашій кулі діалогу відсутній метод «start» або «Start»."
msgid "runtime.top_level_states_share_name"
msgstr "Кілька станів верхнього рівня ({states}) мають спільну назву методу/властивості/сигналу «{key}». Для діалогу доступний лише перший випадок."
msgid "translation_plugin.character_name"
msgstr "Ім’я персонажа"

View File

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

View File

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

View File

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

View File

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

View File

@ -2,21 +2,14 @@
extends EditorPlugin extends EditorPlugin
const DialogueConstants = preload("./constants.gd")
const DialogueImportPlugin = preload("./import_plugin.gd")
const DialogueInspectorPlugin = preload("./inspector_plugin.gd")
const DialogueTranslationParserPlugin = preload("./editor_translation_parser_plugin.gd")
const DialogueSettings = preload("./settings.gd")
const DialogueCache = preload("./components/dialogue_cache.gd")
const MainView = preload("./views/main_view.tscn") const MainView = preload("./views/main_view.tscn")
const DialogueResource = preload("./dialogue_resource.gd")
var import_plugin: DialogueImportPlugin var import_plugin: DMImportPlugin
var inspector_plugin: DialogueInspectorPlugin var inspector_plugin: DMInspectorPlugin
var translation_parser_plugin: DialogueTranslationParserPlugin var translation_parser_plugin: DMTranslationParserPlugin
var main_view var main_view
var dialogue_cache: DialogueCache var dialogue_cache: DMCache
func _enter_tree() -> void: func _enter_tree() -> void:
@ -25,63 +18,87 @@ func _enter_tree() -> void:
if Engine.is_editor_hint(): if Engine.is_editor_hint():
Engine.set_meta("DialogueManagerPlugin", self) Engine.set_meta("DialogueManagerPlugin", self)
DialogueSettings.prepare() DMSettings.prepare()
dialogue_cache = DialogueCache.new() dialogue_cache = DMCache.new()
Engine.set_meta("DialogueCache", dialogue_cache) Engine.set_meta("DMCache", dialogue_cache)
import_plugin = DialogueImportPlugin.new() import_plugin = DMImportPlugin.new()
add_import_plugin(import_plugin) add_import_plugin(import_plugin)
inspector_plugin = DialogueInspectorPlugin.new() inspector_plugin = DMInspectorPlugin.new()
add_inspector_plugin(inspector_plugin) add_inspector_plugin(inspector_plugin)
translation_parser_plugin = DialogueTranslationParserPlugin.new() translation_parser_plugin = DMTranslationParserPlugin.new()
add_translation_parser_plugin(translation_parser_plugin) add_translation_parser_plugin(translation_parser_plugin)
main_view = MainView.instantiate() main_view = MainView.instantiate()
get_editor_interface().get_editor_main_screen().add_child(main_view) EditorInterface.get_editor_main_screen().add_child(main_view)
_make_visible(false) _make_visible(false)
main_view.add_child(dialogue_cache) main_view.add_child(dialogue_cache)
_update_localization() _update_localization()
get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved) EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved)
get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed) EditorInterface.get_file_system_dock().file_removed.connect(_on_file_removed)
add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon) add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon)
# Make sure the current balloon has a UID unique from the example balloon's # Automatically swap the script on the example balloon depending on if dotnet is being used.
var balloon_path: String = DialogueSettings.get_setting("balloon_path", "") if not FileAccess.file_exists("res://tests/test_basic_dialogue.gd"):
var plugin_path: String = get_plugin_path()
var balloon_file_names: PackedStringArray = ["example_balloon.tscn", "small_example_balloon.tscn"]
for balloon_file_name: String in balloon_file_names:
var balloon_path: String = plugin_path + "/example_balloon/" + balloon_file_name
var balloon_content: String = FileAccess.get_file_as_string(balloon_path)
if "example_balloon.gd" in balloon_content and DMSettings.check_for_dotnet_solution():
balloon_content = balloon_content \
# Replace script path with the C# one
.replace("example_balloon.gd", "ExampleBalloon.cs") \
# Replace script UID with the C# one
.replace(ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd")), ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/ExampleBalloon.cs")))
var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE)
balloon_file.store_string(balloon_content)
balloon_file.close()
elif "ExampleBalloon.cs" in balloon_content and not DMSettings.check_for_dotnet_solution():
balloon_content = balloon_content \
# Replace script path with the GDScript one
.replace("ExampleBalloon.cs", "example_balloon.gd") \
# Replace script UID with the GDScript one
.replace(ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/ExampleBalloon.cs")), ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd")))
var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE)
balloon_file.store_string(balloon_content)
balloon_file.close()
# Automatically make any changes to the known custom balloon if there is one.
var balloon_path: String = DMSettings.get_setting(DMSettings.BALLOON_PATH, "")
if balloon_path != "" and FileAccess.file_exists(balloon_path): if balloon_path != "" and FileAccess.file_exists(balloon_path):
var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400
var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn"
var example_balloon_path: String = get_plugin_path() + "/example_balloon/" + example_balloon_file_name var example_balloon_path: String = get_plugin_path() + "/example_balloon/" + example_balloon_file_name
var contents: String = FileAccess.get_file_as_string(balloon_path)
var has_changed: bool = false
# Make sure the current balloon has a UID unique from the example balloon's
var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path)) var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path))
var balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(balloon_path)) var balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(balloon_path))
if example_balloon_uid == balloon_uid: if example_balloon_uid == balloon_uid:
var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id()) var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id())
var contents: String = FileAccess.get_file_as_string(balloon_path)
contents = contents.replace(example_balloon_uid, new_balloon_uid) contents = contents.replace(example_balloon_uid, new_balloon_uid)
has_changed = true
# Make sure the example balloon copy has the correct renaming of the responses menu
if "reponses" in contents:
contents = contents.replace("reponses", "responses")
has_changed = true
# Save any changes
if has_changed:
var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE) var balloon_file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE)
balloon_file.store_string(contents) balloon_file.store_string(contents)
balloon_file.close() balloon_file.close()
# Prevent the project from showing as unsaved even though it was only just opened
if DialogueSettings.get_setting("try_suppressing_startup_unsaved_indicator", false) \
and Engine.get_physics_frames() == 0 \
and get_editor_interface().has_method("save_all_scenes"):
var timer: Timer = Timer.new()
var suppress_unsaved_marker: Callable
suppress_unsaved_marker = func():
if Engine.get_frames_per_second() >= 10:
timer.stop()
get_editor_interface().call("save_all_scenes")
timer.queue_free()
timer.timeout.connect(suppress_unsaved_marker)
add_child(timer)
timer.start(0.1)
func _exit_tree() -> void: func _exit_tree() -> void:
remove_autoload_singleton("DialogueManager") remove_autoload_singleton("DialogueManager")
@ -99,10 +116,10 @@ func _exit_tree() -> void:
main_view.queue_free() main_view.queue_free()
Engine.remove_meta("DialogueManagerPlugin") Engine.remove_meta("DialogueManagerPlugin")
Engine.remove_meta("DialogueCache") Engine.remove_meta("DMCache")
get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved) EditorInterface.get_file_system_dock().files_moved.disconnect(_on_files_moved)
get_editor_interface().get_file_system_dock().file_removed.disconnect(_on_file_removed) EditorInterface.get_file_system_dock().file_removed.disconnect(_on_file_removed)
remove_tool_menu_item("Create copy of dialogue example balloon...") remove_tool_menu_item("Create copy of dialogue example balloon...")
@ -125,10 +142,10 @@ func _get_plugin_icon() -> Texture2D:
func _handles(object) -> bool: func _handles(object) -> bool:
var editor_settings: EditorSettings = get_editor_interface().get_editor_settings() var editor_settings: EditorSettings = EditorInterface.get_editor_settings()
var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path") var external_editor: String = editor_settings.get_setting("text_editor/external/exec_path")
var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != "" var use_external_editor: bool = editor_settings.get_setting("text_editor/external/use_external_editor") and external_editor != ""
if object is DialogueResource and use_external_editor and DialogueSettings.get_user_value("open_in_external_editor", false): if object is DialogueResource and use_external_editor and DMSettings.get_user_value("open_in_external_editor", false):
var project_path: String = ProjectSettings.globalize_path("res://") var project_path: String = ProjectSettings.globalize_path("res://")
var file_path: String = ProjectSettings.globalize_path(object.resource_path) var file_path: String = ProjectSettings.globalize_path(object.resource_path)
OS.create_process(external_editor, [project_path, file_path]) OS.create_process(external_editor, [project_path, file_path])
@ -150,10 +167,10 @@ func _apply_changes() -> void:
func _build() -> bool: func _build() -> bool:
# If this is the dotnet Godot then we need to check if the solution file exists # If this is the dotnet Godot then we need to check if the solution file exists
DialogueSettings.check_for_dotnet_solution() DMSettings.check_for_dotnet_solution()
# Ignore errors in other files if we are just running the test scene # Ignore errors in other files if we are just running the test scene
if DialogueSettings.get_user_value("is_running_test_scene", true): return true if DMSettings.get_user_value("is_running_test_scene", true): return true
if dialogue_cache != null: if dialogue_cache != null:
dialogue_cache.reimport_files() dialogue_cache.reimport_files()
@ -162,7 +179,7 @@ func _build() -> bool:
if files_with_errors.size() > 0: if files_with_errors.size() > 0:
for dialogue_file in files_with_errors: for dialogue_file in files_with_errors:
push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path]) push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path])
get_editor_interface().edit_resource(load(files_with_errors[0].path)) EditorInterface.edit_resource(load(files_with_errors[0].path))
main_view.show_build_error_dialog() main_view.show_build_error_dialog()
return false return false
@ -209,7 +226,7 @@ func get_editor_shortcuts() -> Dictionary:
] ]
} }
var paths = get_editor_interface().get_editor_paths() var paths = EditorInterface.get_editor_paths()
var settings var settings
if FileAccess.file_exists(paths.get_config_dir() + "/editor_settings-4.3.tres"): if FileAccess.file_exists(paths.get_config_dir() + "/editor_settings-4.3.tres"):
settings = load(paths.get_config_dir() + "/editor_settings-4.3.tres") settings = load(paths.get_config_dir() + "/editor_settings-4.3.tres")
@ -283,7 +300,7 @@ func update_import_paths(from_path: String, to_path: String) -> void:
# Update the live buffer # Update the live buffer
if main_view.current_file_path == dependent.path: if main_view.current_file_path == dependent.path:
main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path) main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path)
main_view.pristine_text = main_view.code_edit.text main_view.open_buffers[main_view.current_file_path].pristine_text = main_view.code_edit.text
# Open the file and update the path # Open the file and update the path
var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ) var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ)
@ -323,7 +340,7 @@ func _update_localization() -> void:
func _copy_dialogue_balloon() -> void: func _copy_dialogue_balloon() -> void:
var scale: float = get_editor_interface().get_editor_scale() var scale: float = EditorInterface.get_editor_scale()
var directory_dialog: FileDialog = FileDialog.new() var directory_dialog: FileDialog = FileDialog.new()
var label: Label = Label.new() var label: Label = Label.new()
label.text = "Dialogue balloon files will be copied into chosen directory." label.text = "Dialogue balloon files will be copied into chosen directory."
@ -332,8 +349,8 @@ func _copy_dialogue_balloon() -> void:
directory_dialog.min_size = Vector2(600, 500) * scale directory_dialog.min_size = Vector2(600, 500) * scale
directory_dialog.dir_selected.connect(func(path): directory_dialog.dir_selected.connect(func(path):
var plugin_path: String = get_plugin_path() var plugin_path: String = get_plugin_path()
var is_dotnet: bool = DMSettings.check_for_dotnet_solution()
var is_dotnet: bool = DialogueSettings.check_for_dotnet_solution()
var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn") var balloon_path: String = path + ("/Balloon.tscn" if is_dotnet else "/balloon.tscn")
var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd") var balloon_script_path: String = path + ("/DialogueBalloon.cs" if is_dotnet else "/balloon.gd")
@ -342,19 +359,12 @@ func _copy_dialogue_balloon() -> void:
var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn" var example_balloon_file_name: String = "small_example_balloon.tscn" if is_small_window else "example_balloon.tscn"
var example_balloon_path: String = plugin_path + "/example_balloon/" + example_balloon_file_name var example_balloon_path: String = plugin_path + "/example_balloon/" + example_balloon_file_name
var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd" var example_balloon_script_file_name: String = "ExampleBalloon.cs" if is_dotnet else "example_balloon.gd"
var file_contents: String = FileAccess.get_file_as_string(example_balloon_path).replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path) var example_balloon_script_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(plugin_path + "/example_balloon/example_balloon.gd"))
# Give the balloon a unique UID
var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path)) var example_balloon_uid: String = ResourceUID.id_to_text(ResourceLoader.get_resource_uid(example_balloon_path))
var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id())
file_contents = file_contents.replace(example_balloon_uid, new_balloon_uid)
# Save the new balloon
var file: FileAccess = FileAccess.open(balloon_path, FileAccess.WRITE)
file.store_string(file_contents)
file.close()
# Copy the script file # Copy the script file
file = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ) var file: FileAccess = FileAccess.open(plugin_path + "/example_balloon/" + example_balloon_script_file_name, FileAccess.READ)
file_contents = file.get_as_text() var file_contents: String = file.get_as_text()
if is_dotnet: if is_dotnet:
file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon") file_contents = file_contents.replace("class ExampleBalloon", "class DialogueBalloon")
else: else:
@ -362,15 +372,30 @@ func _copy_dialogue_balloon() -> void:
file = FileAccess.open(balloon_script_path, FileAccess.WRITE) file = FileAccess.open(balloon_script_path, FileAccess.WRITE)
file.store_string(file_contents) file.store_string(file_contents)
file.close() file.close()
var new_balloon_script_uid_raw: int = ResourceUID.create_id()
ResourceUID.add_id(new_balloon_script_uid_raw, balloon_script_path)
var new_balloon_script_uid: String = ResourceUID.id_to_text(new_balloon_script_uid_raw)
get_editor_interface().get_resource_filesystem().scan() # Save the new balloon
get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", balloon_path) file_contents = FileAccess.get_file_as_string(example_balloon_path)
if "example_balloon.gd" in file_contents:
file_contents = file_contents.replace(plugin_path + "/example_balloon/example_balloon.gd", balloon_script_path)
else:
file_contents = file_contents.replace(plugin_path + "/example_balloon/ExampleBalloon.cs", balloon_script_path)
var new_balloon_uid: String = ResourceUID.id_to_text(ResourceUID.create_id())
file_contents = file_contents.replace(example_balloon_uid, new_balloon_uid).replace(example_balloon_script_uid, new_balloon_script_uid)
file = FileAccess.open(balloon_path, FileAccess.WRITE)
file.store_string(file_contents)
file.close()
DialogueSettings.set_setting("balloon_path", balloon_path) EditorInterface.get_resource_filesystem().scan()
EditorInterface.get_file_system_dock().call_deferred("navigate_to_path", balloon_path)
DMSettings.set_setting(DMSettings.BALLOON_PATH, balloon_path)
directory_dialog.queue_free() directory_dialog.queue_free()
) )
get_editor_interface().get_base_control().add_child(directory_dialog) EditorInterface.get_base_control().add_child(directory_dialog)
directory_dialog.popup_centered() directory_dialog.popup_centered()
@ -379,7 +404,7 @@ func _copy_dialogue_balloon() -> void:
func _on_files_moved(old_file: String, new_file: String) -> void: func _on_files_moved(old_file: String, new_file: String) -> void:
update_import_paths(old_file, new_file) update_import_paths(old_file, new_file)
DialogueSettings.move_recent_file(old_file, new_file) DMSettings.move_recent_file(old_file, new_file)
func _on_file_removed(file: String) -> void: func _on_file_removed(file: String) -> void:

View File

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

View File

@ -1,91 +1,199 @@
@tool @tool
extends Node class_name DMSettings extends Node
const DialogueConstants = preload("./constants.gd") #region Editor
### Editor config
const DEFAULT_SETTINGS = { ## Wrap lines in the dialogue editor.
states = [], const WRAP_LONG_LINES = "editor/wrap_long_lines"
missing_translations_are_errors = false, ## The template to start new dialogue files with.
export_characters_in_translation = true, const NEW_FILE_TEMPLATE = "editor/new_file_template"
wrap_lines = false,
new_with_template = true, ## Show lines without statis IDs as errors.
new_template = "~ this_is_a_node_title\nNathan: [[Hi|Hello|Howdy]], this is some dialogue.\nNathan: Here are some choices.\n- First one\n\tNathan: You picked the first one.\n- Second one\n\tNathan: You picked the second one.\n- Start again => this_is_a_node_title\n- End the conversation => END\nNathan: For more information see the online documentation.\n=> END", const MISSING_TRANSLATIONS_ARE_ERRORS = "editor/translations/missing_translations_are_errors"
include_all_responses = false, ## Include character names in the list of translatable strings.
ignore_missing_state_values = false, const INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST = "editor/translations/include_characters_in_translatable_strings_list"
custom_test_scene_path = preload("./test_scene.tscn").resource_path, ## The default locale to use when exporting CSVs
default_csv_locale = "en", const DEFAULT_CSV_LOCALE = "editor/translations/default_csv_locale"
balloon_path = "", ## Any extra CSV locales to append to the exported translation CSV
create_lines_for_responses_with_characters = true, const EXTRA_CSV_LOCALES = "editor/translations/extra_csv_locales"
include_character_in_translation_exports = false, ## Includes a "_character" column in CSV exports.
include_notes_in_translation_exports = false, const INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS = "editor/translations/include_character_in_translation_exports"
uses_dotnet = false, ## Includes a "_notes" column in CSV exports
try_suppressing_startup_unsaved_indicator = false const INCLUDE_NOTES_IN_TRANSLATION_EXPORTS = "editor/translations/include_notes_in_translation_exports"
## A custom test scene to use when testing dialogue.
const CUSTOM_TEST_SCENE_PATH = "editor/advanced/custom_test_scene_path"
## The custom balloon for this game.
const BALLOON_PATH = "runtime/balloon_path"
## The names of any autoloads to shortcut into all dialogue files (so you don't have to write `using SomeGlobal` in each file).
const STATE_AUTOLOAD_SHORTCUTS = "runtime/state_autoload_shortcuts"
## Check for possible naming conflicts in state shortcuts.
const WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS = "runtime/warn_about_method_property_or_signal_name_conflicts"
## Bypass any missing state when running dialogue.
const IGNORE_MISSING_STATE_VALUES = "runtime/advanced/ignore_missing_state_values"
## Whether or not the project is utilising dotnet.
const USES_DOTNET = "runtime/advanced/uses_dotnet"
const SETTINGS_CONFIGURATION = {
WRAP_LONG_LINES: {
value = false,
type = TYPE_BOOL,
},
NEW_FILE_TEMPLATE: {
value = "~ start\nNathan: [[Hi|Hello|Howdy]], this is some dialogue.\nNathan: Here are some choices.\n- First one\n\tNathan: You picked the first one.\n- Second one\n\tNathan: You picked the second one.\n- Start again => start\n- End the conversation => END\nNathan: For more information see the online documentation.\n=> END",
type = TYPE_STRING,
hint = PROPERTY_HINT_MULTILINE_TEXT,
},
MISSING_TRANSLATIONS_ARE_ERRORS: {
value = false,
type = TYPE_BOOL,
is_advanced = true
},
INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST: {
value = true,
type = TYPE_BOOL,
},
DEFAULT_CSV_LOCALE: {
value = "en",
type = TYPE_STRING,
hint = PROPERTY_HINT_LOCALE_ID,
},
EXTRA_CSV_LOCALES: {
value = [],
type = TYPE_PACKED_STRING_ARRAY,
is_advanced = true
},
INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: {
value = false,
type = TYPE_BOOL,
is_advanced = true
},
INCLUDE_NOTES_IN_TRANSLATION_EXPORTS: {
value = false,
type = TYPE_BOOL,
is_advanced = true
},
CUSTOM_TEST_SCENE_PATH: {
value = preload("./test_scene.tscn").resource_path,
type = TYPE_STRING,
hint = PROPERTY_HINT_FILE,
is_advanced = true
},
BALLOON_PATH: {
value = "",
type = TYPE_STRING,
hint = PROPERTY_HINT_FILE,
},
STATE_AUTOLOAD_SHORTCUTS: {
value = [],
type = TYPE_PACKED_STRING_ARRAY,
},
WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: {
value = false,
type = TYPE_BOOL,
is_advanced = true
},
IGNORE_MISSING_STATE_VALUES: {
value = false,
type = TYPE_BOOL,
is_advanced = true
},
USES_DOTNET: {
value = false,
type = TYPE_BOOL,
is_hidden = true
}
} }
static func prepare() -> void: static func prepare() -> void:
# Migrate previous keys var should_save_settings: bool = false
for key in [
"states", # Remap any old settings into their new keys
"missing_translations_are_errors", var legacy_map: Dictionary = {
"export_characters_in_translation", states = STATE_AUTOLOAD_SHORTCUTS,
"wrap_lines", missing_translations_are_errors = MISSING_TRANSLATIONS_ARE_ERRORS,
"new_with_template", export_characters_in_translation = INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST,
"include_all_responses", wrap_lines = WRAP_LONG_LINES,
"custom_test_scene_path" new_with_template = null,
]: new_template = NEW_FILE_TEMPLATE,
if ProjectSettings.has_setting("dialogue_manager/%s" % key): include_all_responses = null,
var value = ProjectSettings.get_setting("dialogue_manager/%s" % key) ignore_missing_state_values = IGNORE_MISSING_STATE_VALUES,
ProjectSettings.set_setting("dialogue_manager/%s" % key, null) custom_test_scene_path = CUSTOM_TEST_SCENE_PATH,
set_setting(key, value) default_csv_locale = DEFAULT_CSV_LOCALE,
balloon_path = BALLOON_PATH,
create_lines_for_responses_with_characters = null,
include_character_in_translation_exports = INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS,
include_notes_in_translation_exports = INCLUDE_NOTES_IN_TRANSLATION_EXPORTS,
uses_dotnet = USES_DOTNET,
try_suppressing_startup_unsaved_indicator = null
}
for legacy_key: String in legacy_map:
if ProjectSettings.has_setting("dialogue_manager/general/%s" % legacy_key):
should_save_settings = true
# Remove the old setting
var value = ProjectSettings.get_setting("dialogue_manager/general/%s" % legacy_key)
ProjectSettings.set_setting("dialogue_manager/general/%s" % legacy_key, null)
if legacy_map.get(legacy_key) != null:
prints("Migrating Dialogue Manager setting %s to %s with value %s" % [legacy_key, legacy_map.get(legacy_key), str(value)])
ProjectSettings.set_setting("dialogue_manager/%s" % [legacy_map.get(legacy_key)], value)
# Set up initial settings # Set up initial settings
for setting in DEFAULT_SETTINGS: for key: String in SETTINGS_CONFIGURATION:
var setting_name: String = "dialogue_manager/general/%s" % setting var setting_config: Dictionary = SETTINGS_CONFIGURATION[key]
var setting_name: String = "dialogue_manager/%s" % key
if not ProjectSettings.has_setting(setting_name): if not ProjectSettings.has_setting(setting_name):
set_setting(setting, DEFAULT_SETTINGS[setting]) ProjectSettings.set_setting(setting_name, setting_config.value)
ProjectSettings.set_initial_value(setting_name, DEFAULT_SETTINGS[setting]) ProjectSettings.set_initial_value(setting_name, setting_config.value)
if setting.ends_with("_path"): ProjectSettings.add_property_info({
ProjectSettings.add_property_info({ "name" = setting_name,
"name": setting_name, "type" = setting_config.type,
"type": TYPE_STRING, "hint" = setting_config.get("hint", PROPERTY_HINT_NONE),
"hint": PROPERTY_HINT_FILE, "hint_string" = setting_config.get("hint_string", "")
}) })
ProjectSettings.set_as_basic(setting_name, not setting_config.has("is_advanced"))
ProjectSettings.set_as_internal(setting_name, setting_config.has("is_hidden"))
# Some settings shouldn't be edited directly in the Project Settings window if should_save_settings:
ProjectSettings.set_as_internal("dialogue_manager/general/states", true) ProjectSettings.save()
ProjectSettings.set_as_internal("dialogue_manager/general/custom_test_scene_path", true)
ProjectSettings.set_as_internal("dialogue_manager/general/uses_dotnet", true)
ProjectSettings.save()
static func set_setting(key: String, value) -> void: static func set_setting(key: String, value) -> void:
ProjectSettings.set_setting("dialogue_manager/general/%s" % key, value) if get_setting(key, value) != value:
ProjectSettings.set_initial_value("dialogue_manager/general/%s" % key, DEFAULT_SETTINGS[key]) ProjectSettings.set_setting("dialogue_manager/%s" % key, value)
ProjectSettings.save() ProjectSettings.set_initial_value("dialogue_manager/%s" % key, SETTINGS_CONFIGURATION[key].value)
ProjectSettings.save()
static func get_setting(key: String, default): static func get_setting(key: String, default):
if ProjectSettings.has_setting("dialogue_manager/general/%s" % key): if ProjectSettings.has_setting("dialogue_manager/%s" % key):
return ProjectSettings.get_setting("dialogue_manager/general/%s" % key) return ProjectSettings.get_setting("dialogue_manager/%s" % key)
else: else:
return default return default
static func get_settings(only_keys: PackedStringArray = []) -> Dictionary: static func get_settings(only_keys: PackedStringArray = []) -> Dictionary:
var settings: Dictionary = {} var settings: Dictionary = {}
for key in DEFAULT_SETTINGS.keys(): for key in SETTINGS_CONFIGURATION.keys():
if only_keys.is_empty() or key in only_keys: if only_keys.is_empty() or key in only_keys:
settings[key] = get_setting(key, DEFAULT_SETTINGS[key]) settings[key] = get_setting(key, SETTINGS_CONFIGURATION[key].value)
return settings return settings
### User config #endregion
#region User
static func get_user_config() -> Dictionary: static func get_user_config() -> Dictionary:
@ -103,15 +211,15 @@ static func get_user_config() -> Dictionary:
open_in_external_editor = false open_in_external_editor = false
} }
if FileAccess.file_exists(DialogueConstants.USER_CONFIG_PATH): if FileAccess.file_exists(DMConstants.USER_CONFIG_PATH):
var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.READ) var file: FileAccess = FileAccess.open(DMConstants.USER_CONFIG_PATH, FileAccess.READ)
user_config.merge(JSON.parse_string(file.get_as_text()), true) user_config.merge(JSON.parse_string(file.get_as_text()), true)
return user_config return user_config
static func save_user_config(user_config: Dictionary) -> void: static func save_user_config(user_config: Dictionary) -> void:
var file: FileAccess = FileAccess.open(DialogueConstants.USER_CONFIG_PATH, FileAccess.WRITE) var file: FileAccess = FileAccess.open(DMConstants.USER_CONFIG_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(user_config)) file.store_string(JSON.stringify(user_config))
@ -182,7 +290,10 @@ static func check_for_dotnet_solution() -> bool:
var directory: String = ProjectSettings.get("dotnet/project/solution_directory") var directory: String = ProjectSettings.get("dotnet/project/solution_directory")
var file_name: String = ProjectSettings.get("dotnet/project/assembly_name") var file_name: String = ProjectSettings.get("dotnet/project/assembly_name")
has_dotnet_solution = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name]) has_dotnet_solution = FileAccess.file_exists("res://%s/%s.sln" % [directory, file_name])
set_setting("uses_dotnet", has_dotnet_solution) set_setting(DMSettings.USES_DOTNET, has_dotnet_solution)
return has_dotnet_solution return has_dotnet_solution
return get_setting("uses_dotnet", false) return get_setting(DMSettings.USES_DOTNET, false)
#endregion

View File

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

View File

@ -10,23 +10,34 @@ const DialogueResource = preload("./dialogue_resource.gd")
func _ready(): func _ready():
var screen_index: int = DisplayServer.get_primary_screen() # Is this running in Godot >=4.4?
DisplayServer.window_set_position(Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - DisplayServer.window_get_size()) * 0.5) if Engine.has_method("is_embedded_in_editor"):
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) if not Engine.call("is_embedded_in_editor"):
var window: Window = get_viewport()
var screen_index: int = DisplayServer.get_primary_screen()
window.position = Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - window.size) * 0.5
window.mode = Window.MODE_WINDOWED
else:
var screen_index: int = DisplayServer.get_primary_screen()
DisplayServer.window_set_position(Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - DisplayServer.window_get_size()) * 0.5)
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
# Normally you can just call DialogueManager directly but doing so before the plugin has been # Normally you can just call DialogueManager directly but doing so before the plugin has been
# enabled in settings will throw a compiler error here so I'm using `get_singleton` instead. # enabled in settings will throw a compiler error here so I'm using `get_singleton` instead.
var dialogue_manager = Engine.get_singleton("DialogueManager") var dialogue_manager = Engine.get_singleton("DialogueManager")
dialogue_manager.dialogue_ended.connect(_on_dialogue_ended) dialogue_manager.dialogue_ended.connect(_on_dialogue_ended)
dialogue_manager.show_dialogue_balloon(resource, title) dialogue_manager.show_dialogue_balloon(resource, title if not title.is_empty() else resource.first_title)
func _enter_tree() -> void: func _enter_tree() -> void:
DialogueSettings.set_user_value("is_running_test_scene", false) DialogueSettings.set_user_value("is_running_test_scene", false)
### Signals #region Signals
func _on_dialogue_ended(_resource: DialogueResource): func _on_dialogue_ended(_resource: DialogueResource):
get_tree().quit() get_tree().quit()
#endregion

View File

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

View File

@ -1,7 +1,6 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=2 format=3 uid="uid://ugd552efvil0"]
[ext_resource type="Script" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"]
[ext_resource type="Script" uid="uid://c8e16qdgu40wo" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"]
[node name="TestScene" type="Node2D"] [node name="TestScene" type="Node2D"]
script = ExtResource("1_yupoh") script = ExtResource("1_yupoh")

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