diff --git a/addons/dialogue_manager/DialogueManager.cs b/addons/dialogue_manager/DialogueManager.cs index 20351c08..04e29440 100644 --- a/addons/dialogue_manager/DialogueManager.cs +++ b/addons/dialogue_manager/DialogueManager.cs @@ -1,6 +1,7 @@ using Godot; using Godot.Collections; using System; +using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -161,6 +162,18 @@ namespace DialogueManagerRuntime } + public static Array StaticIdToLineIds(Resource dialogueResource, string staticId) + { + return (Array)Instance.Call("static_id_to_line_ids", dialogueResource, staticId); + } + + + public static string StaticIdToLineId(Resource dialogueResource, string staticId) + { + return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId); + } + + public static async void Mutate(Dictionary mutation, Array? extraGameStates = null, bool isInlineMutation = false) { Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array(), isInlineMutation); @@ -168,12 +181,105 @@ namespace DialogueManagerRuntime } + public static Array GetMembersForAutoload(Script script) + { + Array members = new Array(); + + string typeName = script.ResourcePath.GetFile().GetBaseName(); + var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName); + foreach (var matchingType in matchingTypes) + { + var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var memberInfo in memberInfos) + { + string type; + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + FieldInfo fieldInfo = memberInfo as FieldInfo; + + if (fieldInfo.FieldType.ToString().Contains("EventHandler")) + { + type = "signal"; + } + else if (fieldInfo.IsLiteral) + { + type = "constant"; + } + else + { + type = "property"; + } + break; + case MemberTypes.Method: + type = "method"; + break; + + default: + continue; + } + + members.Add(new Dictionary() { + { "name", memberInfo.Name }, + { "type", type } + }); + } + } + + return members; + } + + + public bool ThingHasConstant(GodotObject thing, string property) + { + var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var fieldInfo in fieldInfos) + { + if (fieldInfo.Name == property && fieldInfo.IsLiteral) + { + return true; + } + } + + return false; + } + + + public Variant ResolveThingConstant(GodotObject thing, string property) + { + var fieldInfos = thing.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + foreach (var fieldInfo in fieldInfos) + { + if (fieldInfo.Name == property && fieldInfo.IsLiteral) + { + try + { + Variant value = fieldInfo.GetValue(thing) switch + { + int v => Variant.From((long)v), + float v => Variant.From((double)v), + System.String v => Variant.From((string)v), + _ => Variant.From(fieldInfo.GetValue(thing)) + }; + return value; + } + catch (Exception) + { + throw new Exception($"Constant {property} of type ${fieldInfo.GetValue(thing).GetType()} is not supported by Variant."); + } + } + } + + throw new Exception($"{property} is not a public constant on {thing}"); + } + + public bool ThingHasMethod(GodotObject thing, string method, Array args) { var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); foreach (var methodInfo in methodInfos) { - if (methodInfo.Name == method && args.Count == methodInfo.GetParameters().Length) + if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count()) { return true; } @@ -189,7 +295,7 @@ namespace DialogueManagerRuntime 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) + if (methodInfo.Name == method && args.Count >= methodInfo.GetParameters().Where(p => !p.HasDefaultValue).Count()) { info = methodInfo; } @@ -236,7 +342,7 @@ namespace DialogueManagerRuntime Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); EmitSignal(SignalName.Resolved, value); } - catch (Exception err) + catch (Exception) { EmitSignal(SignalName.Resolved); } @@ -344,6 +450,7 @@ namespace DialogueManagerRuntime public DialogueLine(RefCounted data) { + id = (string)data.Get("id"); type = (string)data.Get("type"); next_id = (string)data.Get("next_id"); character = (string)data.Get("character"); @@ -411,6 +518,13 @@ namespace DialogueManagerRuntime set => is_allowed = value; } + private string condition_as_text = ""; + public string ConditionAsText + { + get => condition_as_text; + set => condition_as_text = value; + } + private string text = ""; public string Text { diff --git a/addons/dialogue_manager/compiler/compilation.gd b/addons/dialogue_manager/compiler/compilation.gd index 32b4ffb5..b55b247e 100644 --- a/addons/dialogue_manager/compiler/compilation.gd +++ b/addons/dialogue_manager/compiler/compilation.gd @@ -168,12 +168,13 @@ func import_content(path: String, prefix: String, imported_line_map: Dictionary, for i in range(0, content.size()): var line = content[i] if line.strip_edges().begins_with("~ "): + var indent: String = "\t".repeat(get_indent(line)) var title = line.strip_edges().substr(2) if "/" in line: var bits = title.split("/") - content[i] = "~ %s/%s" % [_imported_titles[bits[0]], bits[1]] + content[i] = "%s~ %s/%s" % [indent, _imported_titles[bits[0]], bits[1]] else: - content[i] = "~ %s/%s" % [str(path.hash()), title] + content[i] = "%s~ %s/%s" % [indent, str(path.hash()), title] elif "=>< " in line: var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() @@ -227,12 +228,12 @@ func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine: tree_line.text = raw_line.strip_edges() # Handle any "using" directives. - if raw_line.begins_with("using "): + if tree_line.type == DMConstants.TYPE_USING: var using_match: RegExMatch = regex.USING_REGEX.search(raw_line) if "state" in using_match.names: var using_state: String = using_match.strings[using_match.names.state].strip_edges() if not using_state in autoload_names: - add_error(i, 0, DMConstants.ERR_UNKNOWN_USING) + add_error(tree_line.line_number, 0, DMConstants.ERR_UNKNOWN_USING) elif not using_state in using_states: using_states.append(using_state) continue @@ -249,18 +250,22 @@ func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine: tree_line.notes = "\n".join(doc_comments) doc_comments.clear() - # Empty lines are only kept so that we can work out groupings of things (eg. responses and - # randomised lines). Therefore we only need to keep one empty line in a row even if there + # Empty lines are only kept so that we can work out groupings of things (eg. randomised + # lines). Therefore we only need to keep one empty line in a row even if there # are multiple. The indent of an empty line is assumed to be the same as the non-empty line # following it. That way, grouping calculations should work. if tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and raw_lines.size() > i + 1: var next_line = raw_lines[i + 1] - if previous_line and previous_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT]: + if get_line_type(next_line) in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT]: continue else: tree_line.type = DMConstants.TYPE_UNKNOWN tree_line.indent = get_indent(next_line) + # Nothing should be more than a single indent past its parent. + if tree_line.indent > parent_chain.size(): + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_INDENTATION) + # Check for indentation changes if tree_line.indent > parent_chain.size() - 1: parent_chain.append(previous_line) @@ -481,6 +486,7 @@ func parse_match_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: Arr # Check that all children are when or else. for child in tree_line.children: if child.type == DMConstants.TYPE_WHEN: continue + if child.type == DMConstants.TYPE_UNKNOWN: continue if child.type == DMConstants.TYPE_CONDITION and child.text.begins_with("else"): continue result = add_error(child.line_number, child.indent, DMConstants.ERR_EXPECTED_WHEN_OR_ELSE) @@ -569,11 +575,15 @@ func parse_response_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: result = add_error(tree_line.line_number, condition.index, condition.error) else: line.expression = condition + # Extract just the raw condition text + var found: RegExMatch = regex.WRAPPED_CONDITION_REGEX.search(tree_line.text) + line.expression_text = found.strings[found.names.expression] + tree_line.text = regex.WRAPPED_CONDITION_REGEX.sub(tree_line.text, "").strip_edges() # Find the original response in this group of responses. var original_response: DMTreeLine = tree_line - for i in range(sibling_index - 1, 0, -1): + for i in range(sibling_index - 1, -1, -1): if siblings[i].type == DMConstants.TYPE_RESPONSE: original_response = siblings[i] elif siblings[i].type != DMConstants.TYPE_UNKNOWN: @@ -690,14 +700,32 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: for i in range(0, tree_line.children.size()): var child: DMTreeLine = tree_line.children[i] if child.type == DMConstants.TYPE_DIALOGUE: + # Nested dialogue lines cannot have further nested dialogue. + if child.children.size() > 0: + add_error(child.children[0].line_number, child.children[0].indent, DMConstants.ERR_INVALID_INDENTATION) + # Mark this as a dialogue child of another dialogue line. + child.is_nested_dialogue = true + var child_line = DMCompiledLine.new("", DMConstants.TYPE_DIALOGUE) + parse_character_and_dialogue(child, child_line, [], 0, parent) + var child_static_line_id: String = extract_static_line_id(child.text) + if child_line.character != "" or child_static_line_id != "": + add_error(child.line_number, child.indent, DMConstants.ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE) + # Check that only the last child (or none) has a jump reference + if i < tree_line.children.size() - 1 and " =>" in child.text: + add_error(child.line_number, child.indent, DMConstants.ERR_NESTED_DIALOGUE_INVALID_JUMP) + if i == 0 and " =>" in tree_line.text: + add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_NESTED_DIALOGUE_INVALID_JUMP) + tree_line.text += "\n" + child.text + elif child.type == DMConstants.TYPE_UNKNOWN: + tree_line.text += "\n" else: result = add_error(child.line_number, child.indent, DMConstants.ERR_INVALID_INDENTATION) # Extract the static line ID var static_line_id: String = extract_static_line_id(tree_line.text) if static_line_id: - tree_line.text = tree_line.text.replace("[ID:%s]" % [static_line_id], "") + tree_line.text = tree_line.text.replace(" [ID:", "[ID:").replace("[ID:%s]" % [static_line_id], "") line.translation_key = static_line_id # Check for simultaneous lines @@ -730,7 +758,7 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings: if expression.size() == 0: add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_EXPRESSION) elif expression[0].type == DMConstants.TYPE_ERROR: - add_error(tree_line.line_number, tree_line.indent + expression[0].index, expression[0].value) + add_error(tree_line.line_number, tree_line.indent + expression[0].i, expression[0].value) # If the line isn't part of a weighted random group then make it point to the next # available sibling. @@ -817,9 +845,14 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s # Replace any newlines. text = text.replace("\\n", "\n").strip_edges() - # If there was no manual translation key then just use the text itself - if line.translation_key == "": - line.translation_key = text + # If there was no manual translation key then just use the text itself (unless this is a + # child dialogue below another dialogue line). + if not tree_line.is_nested_dialogue and line.translation_key == "": + # Show an error if missing translations is enabled + if DMSettings.get_setting(DMSettings.MISSING_TRANSLATIONS_ARE_ERRORS, false): + result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_MISSING_ID) + else: + line.translation_key = text line.text = text @@ -829,9 +862,6 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_DUPLICATE_ID) else: _known_translation_keys[line.translation_key] = line.text - # Show an error if missing translations is enabled - elif DMSettings.get_setting(DMSettings.MISSING_TRANSLATIONS_ARE_ERRORS, false): - result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_MISSING_ID) return result @@ -916,6 +946,9 @@ func get_line_type(raw_line: String) -> String: if text.begins_with("import "): return DMConstants.TYPE_IMPORT + if text.begins_with("using "): + return DMConstants.TYPE_USING + if text.begins_with("#"): return DMConstants.TYPE_COMMENT @@ -1006,7 +1039,7 @@ func extract_condition(text: String, is_wrapped: bool, index: int) -> Dictionary } elif expression[0].type == DMConstants.TYPE_ERROR: return { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: @@ -1034,7 +1067,7 @@ func extract_mutation(text: String) -> Dictionary: } elif expression[0].type == DMConstants.TYPE_ERROR: return { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: diff --git a/addons/dialogue_manager/compiler/compiled_line.gd b/addons/dialogue_manager/compiler/compiled_line.gd index 29863548..9e34f098 100644 --- a/addons/dialogue_manager/compiler/compiled_line.gd +++ b/addons/dialogue_manager/compiler/compiled_line.gd @@ -26,6 +26,8 @@ var concurrent_lines: PackedStringArray = [] var tags: PackedStringArray = [] ## The condition or mutation expression for this line. var expression: Dictionary = {} +## The express as the raw text that was given. +var expression_text: String = "" ## The next sequential line to go to after this line. var next_id: String = "" ## The next line to go to after this line if it is unknown and compile time. @@ -110,7 +112,6 @@ func to_data() -> Dictionary: d.siblings = siblings DMConstants.TYPE_RESPONSE: - # d.text = text.replace("
", "\n") d.text = text if not responses.is_empty(): @@ -130,9 +131,10 @@ func to_data() -> Dictionary: d.tags = tags if not notes.is_empty(): d.notes = notes + if not expression_text.is_empty(): + d.condition_as_text = expression_text DMConstants.TYPE_DIALOGUE: - # d.text = text.replace("
", "\n") d.text = text if translation_key != text: diff --git a/addons/dialogue_manager/compiler/compiler.gd b/addons/dialogue_manager/compiler/compiler.gd index fe9c7f37..a370ef6a 100644 --- a/addons/dialogue_manager/compiler/compiler.gd +++ b/addons/dialogue_manager/compiler/compiler.gd @@ -14,7 +14,6 @@ static func compile_string(text: String, path: String) -> DMCompilerResult: result.titles = compilation.titles result.first_title = compilation.first_title result.errors = compilation.errors - # result.lines = compilation.lines result.lines = compilation.data result.raw_text = text diff --git a/addons/dialogue_manager/compiler/compiler_regex.gd b/addons/dialogue_manager/compiler/compiler_regex.gd index ead998ba..ca104135 100644 --- a/addons/dialogue_manager/compiler/compiler_regex.gd +++ b/addons/dialogue_manager/compiler/compiler_regex.gd @@ -38,6 +38,7 @@ var TOKEN_DEFINITIONS: Dictionary = { DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), + DMConstants.TOKEN_NULL_COALESCE: RegEx.create_from_string("^\\?\\."), DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."), DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"), DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), diff --git a/addons/dialogue_manager/compiler/expression_parser.gd b/addons/dialogue_manager/compiler/expression_parser.gd index 384340fc..7dfb2afc 100644 --- a/addons/dialogue_manager/compiler/expression_parser.gd +++ b/addons/dialogue_manager/compiler/expression_parser.gd @@ -2,6 +2,9 @@ class_name DMExpressionParser extends RefCounted +var include_comments: bool = false + + # Reference to the common [RegEx] that the parser needs. var regex: DMCompilerRegEx = DMCompilerRegEx.new() @@ -25,7 +28,7 @@ func tokenise(text: String, line_type: String, index: int) -> Array: index += 1 text = text.substr(1) else: - return _build_token_tree_error(DMConstants.ERR_INVALID_EXPRESSION, index) + return _build_token_tree_error([], DMConstants.ERR_INVALID_EXPRESSION, index) return _build_token_tree(tokens, line_type, "")[0] @@ -59,7 +62,7 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]: } elif expression[0].type == DMConstants.TYPE_ERROR: replacement = { - index = expression[0].index, + index = expression[0].i, error = expression[0].value } else: @@ -76,8 +79,13 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]: # Create a token that represents an error. -func _build_token_tree_error(error: int, index: int) -> Array: - return [{ type = DMConstants.TOKEN_ERROR, value = error, index = index }] +func _build_token_tree_error(tree: Array, error: int, index: int) -> Array: + tree.insert(0, { + type = DMConstants.TOKEN_ERROR, + value = error, + i = index + }) + return tree # Convert a list of tokens into an abstract syntax tree. @@ -91,14 +99,22 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var error = _check_next_token(token, tokens, line_type, expected_close_token) if error != OK: var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token - return [_build_token_tree_error(error, error_token.index), tokens] + return [_build_token_tree_error(tree, error, error_token.index), tokens] match token.type: + DMConstants.TOKEN_COMMENT: + if include_comments: + tree.append({ + type = DMConstants.TOKEN_COMMENT, + value = token.value, + i = token.index + }) + DMConstants.TOKEN_FUNCTION: var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] tree.append({ type = DMConstants.TOKEN_FUNCTION, @@ -113,11 +129,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var args = _tokens_to_list(sub_tree[0]) if args.size() != 1: - return [_build_token_tree_error(DMConstants.ERR_INVALID_INDEX, token.index), tokens] + return [_build_token_tree_error(tree, DMConstants.ERR_INVALID_INDEX, token.index), tokens] tree.append({ type = DMConstants.TOKEN_DICTIONARY_REFERENCE, @@ -132,7 +148,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACE_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var t = sub_tree[0] for i in range(0, t.size() - 2): @@ -154,7 +170,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_BRACKET_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] var type = DMConstants.TOKEN_ARRAY var value = _tokens_to_list(sub_tree[0]) @@ -177,7 +193,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) if sub_tree[0].size() > 0 and sub_tree[0][0].type == DMConstants.TOKEN_ERROR: - return [_build_token_tree_error(sub_tree[0][0].value, sub_tree[0][0].index), tokens] + return [_build_token_tree_error(tree, sub_tree[0][0].value, sub_tree[0][0].i), tokens] tree.append({ type = DMConstants.TOKEN_GROUP, @@ -190,7 +206,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl DMConstants.TOKEN_BRACE_CLOSE, \ DMConstants.TOKEN_BRACKET_CLOSE: if token.type != expected_close_token: - return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CLOSING_BRACKET, token.index), tokens] tree.append({ type = token.type, @@ -211,10 +227,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_COLON, \ - DMConstants.TOKEN_DOT: + DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE: tree.append({ type = token.type, - i = token.index + i = token.index }) DMConstants.TOKEN_COMPARISON, \ @@ -230,7 +247,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl tree.append({ type = token.type, value = value, - i = token.index + i = token.index }) DMConstants.TOKEN_STRING: @@ -248,7 +265,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl }) DMConstants.TOKEN_CONDITION: - return [_build_token_tree_error(DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] + return [_build_token_tree_error(tree, DMConstants.ERR_UNEXPECTED_CONDITION, token.index), token] DMConstants.TOKEN_BOOL: tree.append({ @@ -280,8 +297,8 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl }) if expected_close_token != "": - var index: int = tokens[0].index if tokens.size() > 0 else 0 - return [_build_token_tree_error(DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] + var index: int = tokens[0].i if tokens.size() > 0 else 0 + return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] return [tree, tokens] @@ -347,8 +364,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_COMPARISON, \ DMConstants.TOKEN_OPERATOR, \ - DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_DOT, \ + DMConstants.TOKEN_NULL_COALESCE, \ DMConstants.TOKEN_NOT, \ DMConstants.TOKEN_AND_OR, \ DMConstants.TOKEN_DICTIONARY_REFERENCE: @@ -366,6 +383,20 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_DOT ] + DMConstants.TOKEN_COMMA: + unexpected_token_types = [ + null, + DMConstants.TOKEN_COMMA, + DMConstants.TOKEN_COLON, + DMConstants.TOKEN_ASSIGNMENT, + DMConstants.TOKEN_OPERATOR, + DMConstants.TOKEN_AND_OR, + DMConstants.TOKEN_PARENS_CLOSE, + DMConstants.TOKEN_BRACE_CLOSE, + DMConstants.TOKEN_BRACKET_CLOSE, + DMConstants.TOKEN_DOT + ] + DMConstants.TOKEN_COLON: unexpected_token_types = [ DMConstants.TOKEN_COMMA, @@ -409,7 +440,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t DMConstants.TOKEN_BRACKET_OPEN ] - if (expected_token_types.size() > 0 and not next_token.type in expected_token_types or unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types): + if (expected_token_types.size() > 0 and not next_token.type in expected_token_types) \ + or (unexpected_token_types.size() > 0 and next_token.type in unexpected_token_types): match next_token.type: null: return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION diff --git a/addons/dialogue_manager/compiler/tree_line.gd b/addons/dialogue_manager/compiler/tree_line.gd index f172a8a4..667daad1 100644 --- a/addons/dialogue_manager/compiler/tree_line.gd +++ b/addons/dialogue_manager/compiler/tree_line.gd @@ -22,6 +22,8 @@ var text: String = "" var children: Array[DMTreeLine] = [] ## Any doc comments attached to this line. var notes: String = "" +## Is this a dialogue line that is the child of another dialogue line? +var is_nested_dialogue: bool = false func _init(initial_id: String) -> void: diff --git a/addons/dialogue_manager/components/code_edit.gd b/addons/dialogue_manager/components/code_edit.gd index e180af44..6b714de5 100644 --- a/addons/dialogue_manager/components/code_edit.gd +++ b/addons/dialogue_manager/components/code_edit.gd @@ -53,6 +53,10 @@ var font_size: int: var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s") +var compiler_regex: DMCompilerRegEx = DMCompilerRegEx.new() +var _autoloads: Dictionary[String, String] = {} +var _autoload_member_cache: Dictionary[String, Dictionary] = {} + func _ready() -> void: # Add error gutter @@ -65,6 +69,10 @@ func _ready() -> void: syntax_highlighter = DMSyntaxHighlighter.new() + # Keep track of any autoloads + ProjectSettings.settings_changed.connect(_on_project_settings_changed) + _on_project_settings_changed() + func _gui_input(event: InputEvent) -> void: # Handle shortcuts that come from the editor @@ -133,10 +141,7 @@ func _drop_data(at_position: Vector2, data) -> void: if cursor.x > -1 and cursor.y > -1: set_cursor(cursor) remove_secondary_carets() - if has_method("insert_text"): - call("insert_text", "\"%s\"" % file, cursor.y, cursor.x) - else: - call("insert_text_at_cursor", "\"%s\"" % file) + insert_text("\"%s\"" % file, cursor.y, cursor.x) grab_focus() @@ -144,6 +149,7 @@ func _request_code_completion(force: bool) -> void: var cursor: Vector2 = get_cursor() var current_line: String = get_line(cursor.y) + # Match jumps if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")): var prompt: String = current_line.split("=>")[1] if prompt.begins_with("< "): @@ -169,9 +175,8 @@ func _request_code_completion(force: bool) -> void: add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("CombineLines", "EditorIcons")) elif matches_prompt(prompt, title): add_code_completion_option(CodeEdit.KIND_CLASS, title, title.substr(prompt.length()), theme_overrides.text_color, get_theme_icon("ArrowRight", "EditorIcons")) - update_code_completion_options(true) - return + # Match character names var name_so_far: String = WEIGHTED_RANDOM_PREFIX.sub(current_line.strip_edges(), "") if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]: # Only show names starting with that character @@ -179,9 +184,72 @@ func _request_code_completion(force: bool) -> void: if names.size() > 0: for name in names: add_code_completion_option(CodeEdit.KIND_CLASS, name + ": ", name.substr(name_so_far.length()) + ": ", theme_overrides.text_color, get_theme_icon("Sprite2D", "EditorIcons")) - update_code_completion_options(true) - else: - cancel_code_completion() + + # Match autoloads on mutation lines + for prefix in ["do ", "set ", "if ", "elif ", "else if ", "match ", "when ", "using "]: + if (current_line.strip_edges().begins_with(prefix) and (cursor.x > current_line.find(prefix))): + var expression: String = current_line.substr(0, cursor.x).strip_edges().substr(3) + # Find the last couple of tokens + var possible_prompt: String = expression.reverse() + possible_prompt = possible_prompt.substr(0, possible_prompt.find(" ")) + possible_prompt = possible_prompt.substr(0, possible_prompt.find("(")) + possible_prompt = possible_prompt.reverse() + var segments: PackedStringArray = possible_prompt.split(".").slice(-2) + var auto_completes: Array[Dictionary] = [] + + # Autoloads and state shortcuts + if segments.size() == 1: + var prompt: String = segments[0] + for autoload in _autoloads.keys(): + if matches_prompt(prompt, autoload): + auto_completes.append({ + prompt = prompt, + text = autoload, + type = "script" + }) + for autoload in get_state_shortcuts(): + for member: Dictionary in get_members_for_autoload(autoload): + if matches_prompt(prompt, member.name): + auto_completes.append({ + prompt = prompt, + text = member.name, + type = member.type + }) + + # Members of an autoload + elif segments[0] in _autoloads.keys() and not current_line.strip_edges().begins_with("using "): + var prompt: String = segments[1] + for member: Dictionary in get_members_for_autoload(segments[0]): + if matches_prompt(prompt, member.name): + auto_completes.append({ + prompt = prompt, + text = member.name, + type = member.type + }) + + auto_completes.sort_custom(func(a, b): return a.text < b.text) + + for auto_complete in auto_completes: + var icon: Texture2D + var text: String = auto_complete.text + match auto_complete.type: + "script": + icon = get_theme_icon("Script", "EditorIcons") + "property": + icon = get_theme_icon("MemberProperty", "EditorIcons") + "method": + icon = get_theme_icon("MemberMethod", "EditorIcons") + text += "()" + "signal": + icon = get_theme_icon("MemberSignal", "EditorIcons") + "constant": + icon = get_theme_icon("MemberConstant", "EditorIcons") + var insert: String = text.substr(auto_complete.prompt.length()) + add_code_completion_option(CodeEdit.KIND_CLASS, text, insert, theme_overrides.text_color, icon) + + update_code_completion_options(true) + if get_code_completion_options().size() == 0: + cancel_code_completion() func _filter_code_completion_candidates(candidates: Array) -> Array: @@ -193,17 +261,21 @@ func _confirm_code_completion(replace: bool) -> void: var completion = get_code_completion_option(get_code_completion_selected_index()) begin_complex_operation() # Delete any part of the text that we've already typed - for i in range(0, completion.display_text.length() - completion.insert_text.length()): - backspace() + if completion.insert_text.length() > 0: + for i in range(0, completion.display_text.length() - completion.insert_text.length()): + backspace() # Insert the whole match insert_text_at_caret(completion.display_text) end_complex_operation() + if completion.display_text.ends_with("()"): + set_cursor(get_cursor() - Vector2.RIGHT) + # Close the autocomplete menu on the next tick call_deferred("cancel_code_completion") -### Helpers +#region Helpers # Get the current caret as a Vector2 @@ -222,6 +294,69 @@ func matches_prompt(prompt: String, matcher: String) -> bool: return prompt.length() < matcher.length() and matcher.to_lower().begins_with(prompt.to_lower()) +func get_state_shortcuts() -> PackedStringArray: + # Get any shortcuts defined in settings + var shortcuts: PackedStringArray = DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, []) + # Check for "using" clauses + for line: String in text.split("\n"): + var found: RegExMatch = compiler_regex.USING_REGEX.search(line) + if found: + shortcuts.append(found.strings[found.names.state]) + # Check for any other script sources + for extra_script_source in DMSettings.get_setting(DMSettings.EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES, []): + shortcuts.append(extra_script_source) + + return shortcuts + + +func get_members_for_autoload(autoload_name: String) -> Array[Dictionary]: + # Debounce method list lookups + if _autoload_member_cache.has(autoload_name) and _autoload_member_cache.get(autoload_name).get("at") > Time.get_ticks_msec() - 5000: + return _autoload_member_cache.get(autoload_name).get("members") + + if not _autoloads.has(autoload_name) and not autoload_name.begins_with("res://") and not autoload_name.begins_with("uid://"): return [] + + var autoload = load(_autoloads.get(autoload_name, autoload_name)) + var script: Script = autoload if autoload is Script else autoload.get_script() + + if not is_instance_valid(script): return [] + + var members: Array[Dictionary] = [] + if script.resource_path.ends_with(".gd"): + for m: Dictionary in script.get_script_method_list(): + if not m.name.begins_with("@"): + members.append({ + name = m.name, + type = "method" + }) + for m: Dictionary in script.get_script_property_list(): + members.append({ + name = m.name, + type = "property" + }) + for m: Dictionary in script.get_script_signal_list(): + members.append({ + name = m.name, + type = "signal" + }) + for c: String in script.get_script_constant_map(): + members.append({ + name = c, + type = "constant" + }) + elif script.resource_path.ends_with(".cs"): + var dotnet = load(Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + "/DialogueManager.cs").new() + for m: Dictionary in dotnet.GetMembersForAutoload(script): + members.append(m) + + _autoload_member_cache[autoload_name] = { + at = Time.get_ticks_msec(), + members = members + } + + return members + + ## Get a list of titles from the current text func get_titles() -> PackedStringArray: var titles = PackedStringArray([]) @@ -420,7 +555,18 @@ func move_line(offset: int) -> void: scroll_vertical = starting_scroll + offset -### Signals +#endregion + +#region Signals + + +func _on_project_settings_changed() -> void: + _autoloads = {} + var project = ConfigFile.new() + project.load("res://project.godot") + for autoload in project.get_section_keys("autoload"): + if autoload != "DialogueManager": + _autoloads[autoload] = project.get_value("autoload", autoload).substr(1) func _on_code_edit_symbol_validate(symbol: String) -> void: @@ -459,3 +605,6 @@ func _on_code_edit_gutter_clicked(line: int, gutter: int) -> void: var line_errors = errors.filter(func(error): return error.line_number == line) if line_errors.size() > 0: error_clicked.emit(line) + + +#endregion diff --git a/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd index 6f73794c..3f4e0d93 100644 --- a/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd +++ b/addons/dialogue_manager/components/code_edit_syntax_highlighter.gd @@ -14,6 +14,8 @@ func _clear_highlighting_cache() -> void: func _get_line_syntax_highlighting(line: int) -> Dictionary: + expression_parser.include_comments = true + var colors: Dictionary = {} var text_edit: TextEdit = get_text_edit() var text: String = text_edit.get_line(line) @@ -31,6 +33,18 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: var index: int = 0 match DMCompiler.get_line_type(text): + DMConstants.TYPE_USING: + colors[index] = { color = theme.conditions_color } + colors[index + "using ".length()] = { color = theme.text_color } + + DMConstants.TYPE_IMPORT: + colors[index] = { color = theme.conditions_color } + var import: RegExMatch = regex.IMPORT_REGEX.search(text) + if import: + colors[index + import.get_start("path") - 1] = { color = theme.strings_color } + colors[index + import.get_end("path") + 1] = { color = theme.conditions_color } + colors[index + import.get_start("prefix")] = { color = theme.text_color } + DMConstants.TYPE_COMMENT: colors[index] = { color = theme.comments_color } @@ -42,7 +56,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: index = text.find(" ") if index > -1: var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0) - if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + if expression.size() == 0: colors[index] = { color = theme.critical_color } else: _highlight_expression(expression, colors, index) @@ -51,7 +65,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: colors[0] = { color = theme.mutations_color } index = text.find(" ") var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0) - if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: + if expression.size() == 0: colors[index] = { color = theme.critical_color } else: _highlight_expression(expression, colors, index) @@ -73,9 +87,13 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary: var dialogue_text: String = text.substr(index, text.find("=>")) - # Highlight character name + # Highlight character name (but ignore ":" within line ID reference) var split_index: int = dialogue_text.replace("\\:", "??").find(":") - colors[index + split_index + 1] = { color = theme.text_color } + if text.substr(split_index - 3, 3) != "[ID": + colors[index + split_index + 1] = { color = theme.text_color } + else: + # If there's no character name then just highlight the text as dialogue. + colors[index] = { color = theme.text_color } # Interpolation var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text) @@ -144,6 +162,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int for token: Dictionary in tokens: last_index = token.i match token.type: + DMConstants.TOKEN_COMMENT: + colors[index + token.i] = { color = theme.comments_color } + DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: colors[index + token.i] = { color = theme.conditions_color } @@ -153,7 +174,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int else: colors[index + token.i] = { color = theme.members_color } - DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, DMConstants.TOKEN_COMMA, DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: + DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \ + DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \ + DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: colors[index + token.i] = { color = theme.symbols_color } DMConstants.TOKEN_STRING: diff --git a/addons/dialogue_manager/components/files_list.gd b/addons/dialogue_manager/components/files_list.gd index 21a4415b..31f61584 100644 --- a/addons/dialogue_manager/components/files_list.gd +++ b/addons/dialogue_manager/components/files_list.gd @@ -114,6 +114,8 @@ func apply_filter() -> void: func apply_theme() -> void: if is_instance_valid(filter_edit): filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") + if is_instance_valid(list): + list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) ### Signals diff --git a/addons/dialogue_manager/components/files_list.tscn b/addons/dialogue_manager/components/files_list.tscn index e135e608..c9e862b1 100644 --- a/addons/dialogue_manager/components/files_list.tscn +++ b/addons/dialogue_manager/components/files_list.tscn @@ -9,6 +9,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +size_flags_vertical = 3 script = ExtResource("1_cytii") icon = ExtResource("2_3ijx1") diff --git a/addons/dialogue_manager/components/find_in_files.gd b/addons/dialogue_manager/components/find_in_files.gd index 2614ecaa..de64b717 100644 --- a/addons/dialogue_manager/components/find_in_files.gd +++ b/addons/dialogue_manager/components/find_in_files.gd @@ -99,7 +99,7 @@ func update_results_view() -> void: var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]" else: - highlight = "[bgcolor=" + colors.symbols_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" + highlight = "[bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length()) result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text] result_label.gui_input.connect(func(event): diff --git a/addons/dialogue_manager/components/search_and_replace.gd b/addons/dialogue_manager/components/search_and_replace.gd index e91574e9..ceb40280 100644 --- a/addons/dialogue_manager/components/search_and_replace.gd +++ b/addons/dialogue_manager/components/search_and_replace.gd @@ -114,7 +114,7 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int: return line.findn(text, from_index) -### Signals +#region Signals func _on_text_edit_gui_input(event: InputEvent) -> void: @@ -177,14 +177,17 @@ func _on_replace_button_pressed() -> void: # Replace the selection at result index var r: Array = results[result_index] + code_edit.begin_complex_operation() var lines: PackedStringArray = code_edit.text.split("\n") var line: String = lines[r[0]] line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2]) lines[r[0]] = line code_edit.text = "\n".join(lines) - search(input.text, result_index) + code_edit.end_complex_operation() code_edit.text_changed.emit() + search(input.text, result_index) + func _on_replace_all_button_pressed() -> void: if match_case_button.button_pressed: @@ -210,3 +213,6 @@ func _on_input_focus_entered() -> void: func _on_match_case_check_box_toggled(button_pressed: bool) -> void: search() + + +#endregion diff --git a/addons/dialogue_manager/components/title_list.gd b/addons/dialogue_manager/components/title_list.gd index ee7cd139..67cdcf0f 100644 --- a/addons/dialogue_manager/components/title_list.gd +++ b/addons/dialogue_manager/components/title_list.gd @@ -48,6 +48,8 @@ func apply_filter() -> void: func apply_theme() -> void: if is_instance_valid(filter_edit): filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") + if is_instance_valid(list): + list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) ### Signals diff --git a/addons/dialogue_manager/constants.gd b/addons/dialogue_manager/constants.gd index ea2844e7..fe85eccc 100644 --- a/addons/dialogue_manager/constants.gd +++ b/addons/dialogue_manager/constants.gd @@ -37,6 +37,7 @@ const TOKEN_COMPARISON = &"comparison" const TOKEN_ASSIGNMENT = &"assignment" const TOKEN_OPERATOR = &"operator" const TOKEN_COMMA = &"comma" +const TOKEN_NULL_COALESCE = &"null_coalesce" const TOKEN_DOT = &"dot" const TOKEN_CONDITION = &"condition" const TOKEN_BOOL = &"bool" @@ -54,6 +55,7 @@ const TOKEN_ERROR = &"error" const TYPE_UNKNOWN = &"" const TYPE_IMPORT = &"import" +const TYPE_USING = &"using" const TYPE_COMMENT = &"comment" const TYPE_RESPONSE = &"response" const TYPE_TITLE = &"title" @@ -118,6 +120,8 @@ const ERR_ONLY_ONE_ELSE_ALLOWED = 137 const ERR_WHEN_MUST_BELONG_TO_MATCH = 138 const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139 const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140 +const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141 +const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142 ## Get the error message @@ -203,6 +207,10 @@ static func get_error_message(error: int) -> String: return translate(&"errors.concurrent_line_without_origin") ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: return translate(&"errors.goto_not_allowed_on_concurrect_lines") + ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE: + return translate(&"errors.unexpected_syntax_on_nested_dialogue_line") + ERR_NESTED_DIALOGUE_INVALID_JUMP: + return translate(&"errors.err_nested_dialogue_invalid_jump") return translate(&"errors.unknown") diff --git a/addons/dialogue_manager/dialogue_manager.gd b/addons/dialogue_manager/dialogue_manager.gd index 537e4a0e..3857f319 100644 --- a/addons/dialogue_manager/dialogue_manager.gd +++ b/addons/dialogue_manager/dialogue_manager.gd @@ -185,6 +185,7 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) continue elif await _check_case_value(value, case, extra_game_states): next_id = case.next_id + break # Nothing matched so check for else case if next_id == "": if not else_case.is_empty(): @@ -206,16 +207,6 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) else: cummulative_weight += sibling.weight - # Find any simultaneously said lines. - var concurrent_lines: Array[DialogueLine] = [] - if data.has(&"concurrent_lines"): - # If the list includes this line then it isn't the origin line so ignore it. - if not data.concurrent_lines.has(data.id): - for concurrent_id: String in data.concurrent_lines: - var concurrent_line: DialogueLine = await get_line(resource, concurrent_id, extra_game_states) - if concurrent_line: - concurrent_lines.append(concurrent_line) - # If this line is blank and it's the last line then check for returning snippets. if data.type in [DMConstants.TYPE_COMMENT, DMConstants.TYPE_UNKNOWN]: if data.next_id in [DMConstants.ID_END, DMConstants.ID_NULL, null]: @@ -252,11 +243,18 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array) # Set up a line object. var line: DialogueLine = await create_dialogue_line(data, extra_game_states) - line.concurrent_lines = concurrent_lines # If the jump point somehow has no content then just end. if not line: return null + # Find any simultaneously said lines. + if data.has(&"concurrent_lines"): + # If the list includes this line then it isn't the origin line so ignore it. + if not data.concurrent_lines.has(data.id): + # Resolve IDs to their actual lines. + for line_id: String in data.concurrent_lines: + line.concurrent_lines.append(await get_line(resource, line_id, extra_game_states)) + # If we are the first of a list of responses then get the other ones. if data.type == DMConstants.TYPE_RESPONSE: # Note: For some reason C# has occasional issues with using the responses property directly @@ -442,10 +440,21 @@ func show_dialogue_balloon_scene(balloon_scene, resource: DialogueResource, titl return balloon +## Resolve a static line ID to an actual line ID +func static_id_to_line_id(resource: DialogueResource, static_id: String) -> String: + var ids = static_id_to_line_ids(resource, static_id) + if ids.size() == 0: return "" + return ids[0] + + +## Resolve a static line ID to any actual line IDs that match +func static_id_to_line_ids(resource: DialogueResource, static_id: String) -> PackedStringArray: + return resource.lines.values().filter(func(l): return l.get(&"translation_key", "") == static_id).map(func(l): return l.id) + + # Call "start" on the given balloon. func _start_balloon(balloon: Node, resource: DialogueResource, title: String, extra_game_states: Array) -> void: - if balloon.get_parent() == null: - get_current_scene.call().add_child(balloon) + get_current_scene.call().add_child(balloon) if balloon.has_method(&"start"): balloon.start(resource, title, extra_game_states) @@ -525,7 +534,7 @@ func show_error_for_missing_state_value(message: String, will_show: bool = true) # Translate a string func translate(data: Dictionary) -> String: - if translation_source == DMConstants.TranslationSource.None: + if TranslationServer.get_loaded_locales().size() == 0 or translation_source == DMConstants.TranslationSource.None: return data.text var translation_key: String = data.get(&"translation_key", data.text) @@ -606,12 +615,13 @@ func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResp type = DMConstants.TYPE_RESPONSE, next_id = data.next_id, is_allowed = data.is_allowed, + condition_as_text = data.get(&"condition_as_text", ""), character = await get_resolved_character(data, extra_game_states), character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]), text = resolved_data.text, text_replacements = data.get(&"text_replacements", [] as Array[Dictionary]), tags = data.get(&"tags", []), - translation_key = data.get(&"translation_key", data.text) + translation_key = data.get(&"translation_key", data.text), }) @@ -630,7 +640,7 @@ func _get_game_states(extra_game_states: Array) -> Array: game_states = [_autoloads] # Add any other state shortcuts from settings for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): - var state: Node = Engine.get_main_loop().root.get_node_or_null(node_name) + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) if state: game_states.append(state) @@ -662,21 +672,31 @@ func _check_case_value(match_value: Variant, data: Dictionary, extra_game_states var expression: Array[Dictionary] = data.condition.expression.duplicate(true) - # If the when is a comparison when insert the match value as the first value to compare to - var already_compared: bool = false - if expression[0].type == DMConstants.TOKEN_COMPARISON: - expression.insert(0, { - type = DMConstants.TOKEN_VALUE, - value = match_value - }) - already_compared = true + # Check for multiple values + var expressions_to_check: Array = [] + var previous_comma_index: int = 0 + for i in range(0, expression.size()): + if expression[i].type == DMConstants.TOKEN_COMMA: + expressions_to_check.append(expression.slice(previous_comma_index, i)) + previous_comma_index = i + 1 + elif i == expression.size() - 1: + expressions_to_check.append(expression.slice(previous_comma_index)) - var resolved_value = await _resolve(expression, extra_game_states) + for expression_to_check in expressions_to_check: + # If the when is a comparison when insert the match value as the first value to compare to + var already_compared: bool = false + if expression_to_check[0].type == DMConstants.TOKEN_COMPARISON: + expression_to_check.insert(0, { + type = DMConstants.TOKEN_VALUE, + value = match_value + }) + already_compared = true - if already_compared: - return resolved_value - else: - return match_value == resolved_value + var resolved_value = await _resolve(expression_to_check, extra_game_states) + if (already_compared and resolved_value) or match_value == resolved_value: + return true + + return false # Make a change to game state or run a method @@ -758,6 +778,13 @@ func _get_state_value(property: String, extra_game_states: Array): if state.has(property): return state.get(property) else: + # Try for a C# constant first + if state.get_script() \ + and state.get_script().resource_path.ends_with(".cs") \ + and _get_dotnet_dialogue_manager().ThingHasConstant(state, property): + return _get_dotnet_dialogue_manager().ResolveThingConstant(state, property) + + # Otherwise just let Godot try and resolve it. var result = expression.execute([], state, false) if not expression.has_execute_failed(): return result @@ -783,7 +810,7 @@ func _warn_about_state_name_collisions(target_key: String, extra_game_states: Ar # Get the list of state shortcuts. var state_shortcuts: Array = [] for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): - var state: Node = Engine.get_main_loop().root.get_node_or_null(node_name) + var state: Node = Engine.get_main_loop().root.get_node_or_null(NodePath(node_name)) if state: state_shortcuts.append(state) @@ -868,7 +895,18 @@ func _resolve(tokens: Array, extra_game_states: Array): limit += 1 var token: Dictionary = tokens[i] - if token.type == DMConstants.TOKEN_FUNCTION: + if token.type == DMConstants.TOKEN_NULL_COALESCE: + var caller: Dictionary = tokens[i - 1] + if caller.value == null: + # If the caller is null then the method/property is also null + caller.type = DMConstants.TOKEN_VALUE + caller.value = null + tokens.remove_at(i + 1) + tokens.remove_at(i) + else: + token.type = DMConstants.TOKEN_DOT + + elif token.type == DMConstants.TOKEN_FUNCTION: var function_name: String = token.function var args = await _resolve_each(token.value, extra_game_states) if tokens[i - 1].type == DMConstants.TOKEN_DOT: @@ -1331,6 +1369,9 @@ func _is_valid(line: DialogueLine) -> bool: # Check that a thing has a given method. func _thing_has_method(thing, method: String, args: Array) -> bool: + if not is_instance_valid(thing): + return false + if Builtins.is_supported(thing, method): return thing != _autoloads elif thing is Dictionary: @@ -1345,7 +1386,7 @@ func _thing_has_method(thing, method: String, args: Array) -> bool: if thing.has_method(method): return true - if method.to_snake_case() != method and DMSettings.check_for_dotnet_solution(): + if thing.get_script() and thing.get_script().resource_path.ends_with(".cs"): # If we get this far then the method might be a C# method with a Task return type return _get_dotnet_dialogue_manager().ThingHasMethod(thing, method, args) @@ -1364,6 +1405,10 @@ func _thing_has_property(thing: Object, property: String) -> bool: if p.name == property: return true + if thing.get_script() and thing.get_script().resource_path.ends_with(".cs"): + # If we get this far then the property might be a C# constant. + return _get_dotnet_dialogue_manager().ThingHasConstant(thing, property) + return false diff --git a/addons/dialogue_manager/dialogue_response.gd b/addons/dialogue_manager/dialogue_response.gd index 701ce926..479b81cf 100644 --- a/addons/dialogue_manager/dialogue_response.gd +++ b/addons/dialogue_manager/dialogue_response.gd @@ -14,6 +14,9 @@ var next_id: String = "" ## [code]true[/code] if the condition of this line was met. var is_allowed: bool = true +## The original condition text. +var condition_as_text: String = "" + ## A character (depending on the "characters in responses" behaviour setting). var character: String = "" @@ -45,6 +48,7 @@ func _init(data: Dictionary = {}) -> void: text_replacements = data.text_replacements tags = data.tags translation_key = data.translation_key + condition_as_text = data.condition_as_text func _to_string() -> String: diff --git a/addons/dialogue_manager/dialogue_responses_menu.gd b/addons/dialogue_manager/dialogue_responses_menu.gd index cd66ae5b..0ae2c50d 100644 --- a/addons/dialogue_manager/dialogue_responses_menu.gd +++ b/addons/dialogue_manager/dialogue_responses_menu.gd @@ -100,16 +100,20 @@ func _configure_focus() -> void: if i == 0: item.focus_neighbor_top = item.get_path() + item.focus_neighbor_left = item.get_path() item.focus_previous = item.get_path() else: item.focus_neighbor_top = items[i - 1].get_path() + item.focus_neighbor_left = items[i - 1].get_path() item.focus_previous = items[i - 1].get_path() if i == items.size() - 1: item.focus_neighbor_bottom = item.get_path() + item.focus_neighbor_right = item.get_path() item.focus_next = item.get_path() else: item.focus_neighbor_bottom = items[i + 1].get_path() + item.focus_neighbor_right = items[i + 1].get_path() item.focus_next = items[i + 1].get_path() item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) diff --git a/addons/dialogue_manager/example_balloon/example_balloon.tscn b/addons/dialogue_manager/example_balloon/example_balloon.tscn index 91d8a7df..d990dd9d 100644 --- a/addons/dialogue_manager/example_balloon/example_balloon.tscn +++ b/addons/dialogue_manager/example_balloon/example_balloon.tscn @@ -40,7 +40,7 @@ corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"] bg_color = Color(0, 0, 0, 1) border_width_left = 3 border_width_top = 3 @@ -61,7 +61,7 @@ MarginContainer/constants/margin_bottom = 15 MarginContainer/constants/margin_left = 30 MarginContainer/constants/margin_right = 30 MarginContainer/constants/margin_top = 15 -Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt") [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 @@ -77,33 +77,28 @@ grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") -[node name="Panel" type="Panel" parent="Balloon"] -clip_children = 2 +[node name="MarginContainer" type="MarginContainer" parent="Balloon"] layout_mode = 1 anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 21.0 -offset_top = -183.0 -offset_right = -19.0 -offset_bottom = -19.0 +offset_top = -219.0 grow_horizontal = 2 grow_vertical = 0 + +[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] +clip_children = 2 +layout_mode = 2 mouse_filter = 1 -[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] +[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] layout_mode = 2 -[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 @@ -113,37 +108,35 @@ text = "Character" fit_content = true scroll_active = false -[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_a8ve6")] +[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_a8ve6")] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 text = "Dialogue..." -[node name="Responses" type="MarginContainer" parent="Balloon"] -layout_mode = 1 -anchors_preset = 7 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -147.0 -offset_top = -558.0 -offset_right = 494.0 -offset_bottom = -154.0 -grow_horizontal = 2 -grow_vertical = 0 - -[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses" node_paths=PackedStringArray("response_template")] +[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -290.5 +offset_top = -35.0 +offset_right = 290.5 +offset_bottom = 35.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 +alignment = 1 script = ExtResource("3_72ixx") response_template = NodePath("ResponseExample") -[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] +[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] layout_mode = 2 text = "Response example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] -[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] +[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/addons/dialogue_manager/example_balloon/small_example_balloon.tscn b/addons/dialogue_manager/example_balloon/small_example_balloon.tscn index c4d2145f..03b4773c 100644 --- a/addons/dialogue_manager/example_balloon/small_example_balloon.tscn +++ b/addons/dialogue_manager/example_balloon/small_example_balloon.tscn @@ -66,7 +66,7 @@ corner_radius_top_right = 3 corner_radius_bottom_right = 3 corner_radius_bottom_left = 3 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uy0d5"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"] bg_color = Color(0, 0, 0, 1) border_width_left = 1 border_width_top = 1 @@ -87,7 +87,7 @@ MarginContainer/constants/margin_bottom = 4 MarginContainer/constants/margin_left = 8 MarginContainer/constants/margin_right = 8 MarginContainer/constants/margin_top = 4 -Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm") [node name="ExampleBalloon" type="CanvasLayer"] layer = 100 @@ -103,33 +103,28 @@ grow_horizontal = 2 grow_vertical = 2 theme = SubResource("Theme_qq3yp") -[node name="Panel" type="Panel" parent="Balloon"] -clip_children = 2 +[node name="MarginContainer" type="MarginContainer" parent="Balloon"] layout_mode = 1 anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 3.0 -offset_top = -62.0 -offset_right = -4.0 -offset_bottom = -4.0 +offset_top = -71.0 grow_horizontal = 2 grow_vertical = 0 + +[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] +clip_children = 2 +layout_mode = 2 mouse_filter = 1 -[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/Panel/Dialogue"] +[node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] layout_mode = 2 -[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/Panel/Dialogue/VBoxContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] unique_name_in_owner = true modulate = Color(1, 1, 1, 0.501961) layout_mode = 2 @@ -139,36 +134,33 @@ text = "Character" fit_content = true scroll_active = false -[node name="DialogueLabel" parent="Balloon/Panel/Dialogue/VBoxContainer" instance=ExtResource("2_hfvdi")] +[node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_hfvdi")] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 text = "Dialogue..." -[node name="Responses" type="MarginContainer" parent="Balloon"] -layout_mode = 1 -anchors_preset = 7 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -124.0 -offset_top = -218.0 -offset_right = 125.0 -offset_bottom = -50.0 -grow_horizontal = 2 -grow_vertical = 0 - -[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses"] +[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"] unique_name_in_owner = true -layout_mode = 2 +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -116.5 +offset_top = -9.0 +offset_right = 116.5 +offset_bottom = 9.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 8 theme_override_constants/separation = 2 script = ExtResource("3_1j1j0") -[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] +[node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] layout_mode = 2 text = "Response Example" [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] -[connection signal="response_selected" from="Balloon/Responses/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] +[connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] diff --git a/addons/dialogue_manager/export_plugin.gd b/addons/dialogue_manager/export_plugin.gd new file mode 100644 index 00000000..ef65500a --- /dev/null +++ b/addons/dialogue_manager/export_plugin.gd @@ -0,0 +1,26 @@ +class_name DMExportPlugin extends EditorExportPlugin + +const IGNORED_PATHS = [ + "/assets", + "/components", + "/views", + "inspector_plugin", + "test_scene" +] + + +func _get_name() -> String: + return "Dialogue Manager Export Plugin" + + +func _export_file(path: String, type: String, features: PackedStringArray) -> void: + var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path() + + # Ignore any editor stuff + for ignored_path: String in IGNORED_PATHS: + if path.begins_with(plugin_path + ignored_path): + skip() + + # Ignore C# stuff it not using dotnet + if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"): + skip() diff --git a/addons/dialogue_manager/export_plugin.gd.uid b/addons/dialogue_manager/export_plugin.gd.uid new file mode 100644 index 00000000..efaa0c6e --- /dev/null +++ b/addons/dialogue_manager/export_plugin.gd.uid @@ -0,0 +1 @@ +uid://sa55ra11ji2q diff --git a/addons/dialogue_manager/import_plugin.gd b/addons/dialogue_manager/import_plugin.gd index 345fe844..9c4ad78b 100644 --- a/addons/dialogue_manager/import_plugin.gd +++ b/addons/dialogue_manager/import_plugin.gd @@ -5,12 +5,15 @@ class_name DMImportPlugin extends EditorImportPlugin signal compiled_resource(resource: Resource) -const COMPILER_VERSION = 14 +const COMPILER_VERSION = 15 func _get_importer_name() -> String: - # NOTE: A change to this forces a re-import of all dialogue - return "dialogue_manager_compiler_%s" % COMPILER_VERSION + return "dialogue_manager" + + +func _get_format_version() -> int: + return COMPILER_VERSION func _get_visible_name() -> String: @@ -74,7 +77,7 @@ func _import(source_file: String, save_path: String, options: Dictionary, platfo if result.errors.size() > 0: printerr("%d errors found in %s" % [result.errors.size(), source_file]) cache.add_errors_to_file(source_file, result.errors) - return ERR_PARSE_ERROR + return OK # Get the current addon version var config: ConfigFile = ConfigFile.new() diff --git a/addons/dialogue_manager/l10n/en.mo b/addons/dialogue_manager/l10n/en.mo deleted file mode 100644 index 2ab4fdfd..00000000 Binary files a/addons/dialogue_manager/l10n/en.mo and /dev/null differ diff --git a/addons/dialogue_manager/l10n/en.po b/addons/dialogue_manager/l10n/en.po index bd9a7952..9ada34e7 100644 --- a/addons/dialogue_manager/l10n/en.po +++ b/addons/dialogue_manager/l10n/en.po @@ -5,7 +5,7 @@ msgstr "" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" -"Language: de\n" +"Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -33,6 +33,9 @@ msgstr "Clear recent files" msgid "save_all_files" msgstr "Save all files" +msgid "all" +msgstr "All" + msgid "find_in_files" msgstr "Find in files..." @@ -321,6 +324,12 @@ msgstr "Concurrent lines need an origin line that doesn't start with \"| \"." msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "Goto references are not allowed on concurrent dialogue lines." +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "Nested dialogue lines may only contain dialogue." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Only the last line of nested dialogue is allowed to include a jump." + msgid "errors.unknown" msgstr "Unknown syntax." diff --git a/addons/dialogue_manager/l10n/translations.pot b/addons/dialogue_manager/l10n/translations.pot index 795b4724..e49728d7 100644 --- a/addons/dialogue_manager/l10n/translations.pot +++ b/addons/dialogue_manager/l10n/translations.pot @@ -26,6 +26,9 @@ msgstr "" msgid "save_all_files" msgstr "" +msgid "all" +msgstr "" + msgid "find_in_files" msgstr "" @@ -311,6 +314,12 @@ msgstr "" msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "" +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "" + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "" + msgid "errors.unknown" msgstr "" diff --git a/addons/dialogue_manager/l10n/uk.po b/addons/dialogue_manager/l10n/uk.po index 8cd41ac4..1c1ae123 100644 --- a/addons/dialogue_manager/l10n/uk.po +++ b/addons/dialogue_manager/l10n/uk.po @@ -320,6 +320,12 @@ msgstr "Паралельні рядки потребують початково msgid "errors.goto_not_allowed_on_concurrect_lines" msgstr "У паралельних діалогових рядках не допускаються Goto посилання." +msgid "errors.unexpected_syntax_on_nested_dialogue_line" +msgstr "Вкладені рядки діалогу можуть містити лише діалог." + +msgid "errors.err_nested_dialogue_invalid_jump" +msgstr "Лише останній рядок вкладеного діалогу може містити перехід." + msgid "errors.unknown" msgstr "Невідомий синтаксис." diff --git a/addons/dialogue_manager/plugin.cfg b/addons/dialogue_manager/plugin.cfg index 9b558dbb..8de0f83f 100644 --- a/addons/dialogue_manager/plugin.cfg +++ b/addons/dialogue_manager/plugin.cfg @@ -3,5 +3,5 @@ name="Dialogue Manager" description="A powerful nonlinear dialogue system" author="Nathan Hoad" -version="3.4.0" +version="3.6.3" script="plugin.gd" diff --git a/addons/dialogue_manager/plugin.gd b/addons/dialogue_manager/plugin.gd index 992ea3d5..a5d607fa 100644 --- a/addons/dialogue_manager/plugin.gd +++ b/addons/dialogue_manager/plugin.gd @@ -6,15 +6,22 @@ const MainView = preload("./views/main_view.tscn") var import_plugin: DMImportPlugin +var export_plugin: DMExportPlugin var inspector_plugin: DMInspectorPlugin var translation_parser_plugin: DMTranslationParserPlugin var main_view var dialogue_cache: DMCache -func _enter_tree() -> void: +func _enable_plugin() -> void: add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd") + +func _disable_plugin() -> void: + remove_autoload_singleton("DialogueManager") + + +func _enter_tree() -> void: if Engine.is_editor_hint(): Engine.set_meta("DialogueManagerPlugin", self) @@ -26,6 +33,9 @@ func _enter_tree() -> void: import_plugin = DMImportPlugin.new() add_import_plugin(import_plugin) + export_plugin = DMExportPlugin.new() + add_export_plugin(export_plugin) + inspector_plugin = DMInspectorPlugin.new() add_inspector_plugin(inspector_plugin) @@ -101,11 +111,12 @@ func _enter_tree() -> void: func _exit_tree() -> void: - remove_autoload_singleton("DialogueManager") - remove_import_plugin(import_plugin) import_plugin = null + remove_export_plugin(export_plugin) + export_plugin = null + remove_inspector_plugin(inspector_plugin) inspector_plugin = null @@ -165,6 +176,11 @@ func _apply_changes() -> void: _update_localization() +func _save_external_data() -> void: + if dialogue_cache != null: + dialogue_cache.reimport_files() + + func _build() -> bool: # If this is the dotnet Godot then we need to check if the solution file exists DMSettings.check_for_dotnet_solution() @@ -313,6 +329,9 @@ func update_import_paths(from_path: String, to_path: String) -> void: func _update_localization() -> void: + if not DMSettings.get_setting(DMSettings.UPDATE_POT_FILES_AUTOMATICALLY, true): + return + var dialogue_files = dialogue_cache.get_files() # Add any new files to POT generation diff --git a/addons/dialogue_manager/settings.gd b/addons/dialogue_manager/settings.gd index 0a0c12f2..758261df 100644 --- a/addons/dialogue_manager/settings.gd +++ b/addons/dialogue_manager/settings.gd @@ -23,9 +23,13 @@ const EXTRA_CSV_LOCALES = "editor/translations/extra_csv_locales" const INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS = "editor/translations/include_character_in_translation_exports" ## Includes a "_notes" column in CSV exports const INCLUDE_NOTES_IN_TRANSLATION_EXPORTS = "editor/translations/include_notes_in_translation_exports" +## Automatically update the project's list of translatable files when dialogue files are added or removed +const UPDATE_POT_FILES_AUTOMATICALLY = "editor/translations/update_pot_files_automatically" ## A custom test scene to use when testing dialogue. const CUSTOM_TEST_SCENE_PATH = "editor/advanced/custom_test_scene_path" +## Extra script files to include in the auto-complete-able list +const EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES = "editor/advanced/extra_auto_complete_script_sources" ## The custom balloon for this game. const BALLOON_PATH = "runtime/balloon_path" @@ -40,7 +44,7 @@ const IGNORE_MISSING_STATE_VALUES = "runtime/advanced/ignore_missing_state_value const USES_DOTNET = "runtime/advanced/uses_dotnet" -const SETTINGS_CONFIGURATION = { +static var SETTINGS_CONFIGURATION = { WRAP_LONG_LINES: { value = false, type = TYPE_BOOL, @@ -68,6 +72,8 @@ const SETTINGS_CONFIGURATION = { EXTRA_CSV_LOCALES: { value = [], type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], is_advanced = true }, INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: { @@ -80,6 +86,11 @@ const SETTINGS_CONFIGURATION = { type = TYPE_BOOL, is_advanced = true }, + UPDATE_POT_FILES_AUTOMATICALLY: { + value = true, + type = TYPE_BOOL, + is_advanced = true + }, CUSTOM_TEST_SCENE_PATH: { value = preload("./test_scene.tscn").resource_path, @@ -87,6 +98,13 @@ const SETTINGS_CONFIGURATION = { hint = PROPERTY_HINT_FILE, is_advanced = true }, + EXTRA_AUTO_COMPLETE_SCRIPT_SOURCES: { + value = [], + type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d/%d:*.*" % [TYPE_STRING, PROPERTY_HINT_FILE], + is_advanced = true + }, BALLOON_PATH: { value = "", @@ -96,6 +114,8 @@ const SETTINGS_CONFIGURATION = { STATE_AUTOLOAD_SHORTCUTS: { value = [], type = TYPE_PACKED_STRING_ARRAY, + hint = PROPERTY_HINT_TYPE_STRING, + hint_string = "%d:" % [TYPE_STRING], }, WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: { value = false, @@ -203,7 +223,7 @@ static func get_user_config() -> Dictionary: recent_files = [], reopen_files = [], most_recent_reopen_file = "", - carets = {}, + file_meta = {}, run_title = "", run_resource_path = "", is_running_test_scene = false, @@ -229,10 +249,17 @@ static func set_user_value(key: String, value) -> void: save_user_config(user_config) -static func get_user_value(key: String, default = null): +static func get_user_value(key: String, default = null) -> Variant: return get_user_config().get(key, default) +static func forget_path(path: String) -> void: + remove_recent_file(path) + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta.erase(path) + set_user_value("file_meta", file_meta) + + static func add_recent_file(path: String) -> void: var recent_files: Array = get_user_value("recent_files", []) if path in recent_files: @@ -266,23 +293,34 @@ static func clear_recent_files() -> void: static func set_caret(path: String, cursor: Vector2) -> void: - var carets: Dictionary = get_user_value("carets", {}) - carets[path] = { - x = cursor.x, - y = cursor.y - } - set_user_value("carets", carets) + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta[path] = file_meta.get(path, {}).merged({ cursor = "%d,%d" % [cursor.x, cursor.y] }, true) + set_user_value("file_meta", file_meta) static func get_caret(path: String) -> Vector2: - var carets = get_user_value("carets", {}) - if carets.has(path): - var caret = carets.get(path) - return Vector2(caret.x, caret.y) + var file_meta: Dictionary = get_user_value("file_meta", {}) + if file_meta.has(path): + var cursor: PackedStringArray = file_meta.get(path).get("cursor", "0,0").split(",") + return Vector2(cursor[0].to_int(), cursor[1].to_int()) else: return Vector2.ZERO +static func set_scroll(path: String, scroll_vertical: int) -> void: + var file_meta: Dictionary = get_user_value("file_meta", {}) + file_meta[path] = file_meta.get(path, {}).merged({ scroll_vertical = scroll_vertical }, true) + set_user_value("file_meta", file_meta) + + +static func get_scroll(path: String) -> int: + var file_meta: Dictionary = get_user_value("file_meta", {}) + if file_meta.has(path): + return file_meta.get(path).get("scroll_vertical", 0) + else: + return 0 + + static func check_for_dotnet_solution() -> bool: if Engine.is_editor_hint(): var has_dotnet_solution: bool = false diff --git a/addons/dialogue_manager/test_scene.gd b/addons/dialogue_manager/test_scene.gd index 20fe115f..e6216672 100644 --- a/addons/dialogue_manager/test_scene.gd +++ b/addons/dialogue_manager/test_scene.gd @@ -10,17 +10,11 @@ const DialogueResource = preload("./dialogue_resource.gd") func _ready(): - # Is this running in Godot >=4.4? - if Engine.has_method("is_embedded_in_editor"): - 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: + if not Engine.is_embedded_in_editor: + var window: Window = get_viewport() 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) + window.position = Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - window.size) * 0.5 + window.mode = Window.MODE_WINDOWED # 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. diff --git a/addons/dialogue_manager/utilities/dialogue_cache.gd b/addons/dialogue_manager/utilities/dialogue_cache.gd index dd1da441..53b5efc4 100644 --- a/addons/dialogue_manager/utilities/dialogue_cache.gd +++ b/addons/dialogue_manager/utilities/dialogue_cache.gd @@ -37,10 +37,11 @@ func reimport_files(and_files: PackedStringArray = []) -> void: for file in and_files: if not _files_marked_for_reimport.has(file): _files_marked_for_reimport.append(file) - + if _files_marked_for_reimport.is_empty(): return EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) + _files_marked_for_reimport.clear() ## Add a dialogue file to the cache. diff --git a/addons/dialogue_manager/views/main_view.gd b/addons/dialogue_manager/views/main_view.gd index d2156de7..6dbcb549 100644 --- a/addons/dialogue_manager/views/main_view.gd +++ b/addons/dialogue_manager/views/main_view.gd @@ -98,16 +98,23 @@ var current_file_path: String = "": title_list.show() code_edit.show() + var cursor: Vector2 = DMSettings.get_caret(current_file_path) + var scroll_vertical: int = DMSettings.get_scroll(current_file_path) + code_edit.text = open_buffers[current_file_path].text code_edit.errors = [] code_edit.clear_undo_history() - code_edit.set_cursor(DMSettings.get_caret(current_file_path)) + code_edit.set_cursor(cursor) + code_edit.scroll_vertical = scroll_vertical code_edit.grab_focus() _on_code_edit_text_changed() errors_panel.errors = [] code_edit.errors = [] + + if search_and_replace.visible: + search_and_replace.search() get: return current_file_path @@ -177,6 +184,8 @@ func _ready() -> void: EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) + code_edit.get_v_scroll_bar().value_changed.connect(_on_code_edit_scroll_changed) + func _exit_tree() -> void: DMSettings.set_user_value("reopen_files", open_buffers.keys()) @@ -374,6 +383,7 @@ func apply_theme() -> void: open_button.tooltip_text = DMConstants.translate(&"open_a_file") save_all_button.icon = get_theme_icon("Save", "EditorIcons") + save_all_button.text = DMConstants.translate(&"all") save_all_button.tooltip_text = DMConstants.translate(&"start_all_files") find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons") @@ -501,6 +511,8 @@ func generate_translations_keys() -> void: var key_regex = RegEx.new() key_regex.compile("\\[ID:(?.*?)\\]") + var compiled_lines: Dictionary = DMCompiler.compile_string(code_edit.text, "").lines + # Make list of known keys var known_keys = {} for i in range(0, lines.size()): @@ -523,6 +535,7 @@ func generate_translations_keys() -> void: var l = line.strip_edges() if not [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE].has(DMCompiler.get_line_type(l)): continue + if not compiled_lines.has(str(i)): continue if "[ID:" in line: continue @@ -563,6 +576,7 @@ func generate_translations_keys() -> void: # Add a translation file to the project settings func add_path_to_project_translations(path: String) -> void: var translations: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations") + # 不更新 translation 设置 # if not path in translations: # translations.append(path) # ProjectSettings.save() @@ -830,6 +844,16 @@ func show_search_form(is_enabled: bool) -> void: search_and_replace.focus_line_edit() +func run_test_scene(from_key: String) -> void: + DMSettings.set_user_value("run_title", from_key) + DMSettings.set_user_value("is_running_test_scene", true) + DMSettings.set_user_value("run_resource_path", current_file_path) + var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") + if ResourceUID.has_id(ResourceUID.text_to_id(test_scene_path)): + test_scene_path = ResourceUID.get_id_path(ResourceUID.text_to_id(test_scene_path)) + EditorInterface.play_custom_scene(test_scene_path) + + ### Signals @@ -1014,6 +1038,10 @@ func _on_code_edit_text_changed() -> void: parse_timer.start(1) +func _on_code_edit_scroll_changed(value: int) -> void: + DMSettings.set_scroll(current_file_path, code_edit.scroll_vertical) + + func _on_code_edit_active_title_change(title: String) -> void: title_list.select_title(title) @@ -1064,12 +1092,7 @@ func _on_test_button_pressed() -> void: errors_dialog.popup_centered() return - DMSettings.set_user_value("run_title", "") - DMSettings.set_user_value("is_running_test_scene", true) - DMSettings.set_user_value("run_resource_path", current_file_path) - # var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") - # EditorInterface.play_custom_scene(test_scene_path) - EditorInterface.play_custom_scene("res://addons/dialogue_manager/test_scene.tscn") + run_test_scene("") func _on_test_line_button_pressed() -> void: @@ -1084,13 +1107,9 @@ func _on_test_line_button_pressed() -> void: for i in range(code_edit.get_cursor().y, code_edit.get_line_count()): if not code_edit.get_line(i).is_empty(): line_to_run = i - break; - DMSettings.set_user_value("run_title", str(line_to_run)) - DMSettings.set_user_value("is_running_test_scene", true) - DMSettings.set_user_value("run_resource_path", current_file_path) - # var test_scene_path: String = DMSettings.get_setting(DMSettings.CUSTOM_TEST_SCENE_PATH, "res://addons/dialogue_manager/test_scene.tscn") - # EditorInterface.play_custom_scene(test_scene_path) - EditorInterface.play_custom_scene("res://addons/dialogue_manager/test_scene.tscn") + break + + run_test_scene(str(line_to_run)) func _on_support_button_pressed() -> void: @@ -1172,3 +1191,4 @@ func _on_close_confirmation_dialog_custom_action(action: StringName) -> void: func _on_find_in_files_result_selected(path: String, cursor: Vector2, length: int) -> void: open_file(path) code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length) + code_edit.set_line_as_center_visible(cursor.y) diff --git a/scene/ground/scene/c02/s02_过道.tscn b/scene/ground/scene/c02/s02_过道.tscn index d40bd29a..038ab69f 100644 --- a/scene/ground/scene/c02/s02_过道.tscn +++ b/scene/ground/scene/c02/s02_过道.tscn @@ -66,10 +66,10 @@ oneshot_animation = "" [node name="冷飕飕Sfx" parent="Ground/AnimationPlayer" index="0" instance=ExtResource("3_fvldj")] stream = null mode = "交互与效果音" -audio_dict = Dictionary[String, AudioStream]({}) [node name="背景音效" type="AudioStreamPlayer" parent="Ground/AnimationPlayer" index="1"] stream = ExtResource("6_36l5t") +autoplay = true bus = &"game_sfx" script = ExtResource("14_jg8g0") mode = "场景背景音" diff --git a/scene/ground/scene/c02/s09_裂缝.tscn b/scene/ground/scene/c02/s09_裂缝.tscn index 9d49f991..7d14dabe 100644 --- a/scene/ground/scene/c02/s09_裂缝.tscn +++ b/scene/ground/scene/c02/s09_裂缝.tscn @@ -160,7 +160,6 @@ stream = ExtResource("5_husb8") bus = &"game_sfx" script = ExtResource("4_qjenp") mode = "场景背景音" -audio_dict = Dictionary[String, AudioStream]({}) "自动开始" = false "循环播放" = true "感应玩家操作" = false @@ -341,7 +340,6 @@ animation = &"空房间血脚印" frame = 8 [node name="PointLight2D" type="PointLight2D" parent="Ground/AmbientLayer" index="0"] -visible = false position = Vector2(3067, -61) energy = 0.7 blend_mode = 1