更新 DialogueManager

This commit is contained in:
cakipaul 2025-06-16 16:40:11 +08:00
parent 8f1a0bd8d6
commit 411940c228
35 changed files with 757 additions and 221 deletions

View File

@ -1,6 +1,7 @@
using Godot; using Godot;
using Godot.Collections; using Godot.Collections;
using System; using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -161,6 +162,18 @@ namespace DialogueManagerRuntime
} }
public static Array<string> StaticIdToLineIds(Resource dialogueResource, string staticId)
{
return (Array<string>)Instance.Call("static_id_to_line_ids", dialogueResource, staticId);
}
public static string StaticIdToLineId(Resource dialogueResource, string staticId)
{
return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId);
}
public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false) public static async void Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
{ {
Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation); Instance.Call("_bridge_mutate", mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
@ -168,12 +181,105 @@ namespace DialogueManagerRuntime
} }
public static Array<Dictionary> GetMembersForAutoload(Script script)
{
Array<Dictionary> members = new Array<Dictionary>();
string typeName = script.ResourcePath.GetFile().GetBaseName();
var matchingTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Name == typeName);
foreach (var matchingType in matchingTypes)
{
var memberInfos = matchingType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var memberInfo in memberInfos)
{
string type;
switch (memberInfo.MemberType)
{
case MemberTypes.Field:
FieldInfo fieldInfo = memberInfo as FieldInfo;
if (fieldInfo.FieldType.ToString().Contains("EventHandler"))
{
type = "signal";
}
else if (fieldInfo.IsLiteral)
{
type = "constant";
}
else
{
type = "property";
}
break;
case MemberTypes.Method:
type = "method";
break;
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<Variant> args) public bool ThingHasMethod(GodotObject thing, string method, Array<Variant> args)
{ {
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var methodInfo in methodInfos) 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; return true;
} }
@ -189,7 +295,7 @@ namespace DialogueManagerRuntime
var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); var methodInfos = thing.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var methodInfo in methodInfos) 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; info = methodInfo;
} }
@ -236,7 +342,7 @@ namespace DialogueManagerRuntime
Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult); Variant value = (Variant)taskResult.GetType().GetProperty("Result").GetValue(taskResult);
EmitSignal(SignalName.Resolved, value); EmitSignal(SignalName.Resolved, value);
} }
catch (Exception err) catch (Exception)
{ {
EmitSignal(SignalName.Resolved); EmitSignal(SignalName.Resolved);
} }
@ -344,6 +450,7 @@ namespace DialogueManagerRuntime
public DialogueLine(RefCounted data) public DialogueLine(RefCounted data)
{ {
id = (string)data.Get("id");
type = (string)data.Get("type"); type = (string)data.Get("type");
next_id = (string)data.Get("next_id"); next_id = (string)data.Get("next_id");
character = (string)data.Get("character"); character = (string)data.Get("character");
@ -411,6 +518,13 @@ namespace DialogueManagerRuntime
set => is_allowed = value; set => is_allowed = value;
} }
private string condition_as_text = "";
public string ConditionAsText
{
get => condition_as_text;
set => condition_as_text = value;
}
private string text = ""; private string text = "";
public string Text public string Text
{ {

View File

@ -168,12 +168,13 @@ func import_content(path: String, prefix: String, imported_line_map: Dictionary,
for i in range(0, content.size()): for i in range(0, content.size()):
var line = content[i] var line = content[i]
if line.strip_edges().begins_with("~ "): if line.strip_edges().begins_with("~ "):
var indent: String = "\t".repeat(get_indent(line))
var title = line.strip_edges().substr(2) var title = line.strip_edges().substr(2)
if "/" in line: if "/" in line:
var bits = title.split("/") 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: else:
content[i] = "~ %s/%s" % [str(path.hash()), title] content[i] = "%s~ %s/%s" % [indent, str(path.hash()), title]
elif "=>< " in line: elif "=>< " in line:
var jump: String = line.substr(line.find("=>< ") + "=>< ".length()).strip_edges() 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() tree_line.text = raw_line.strip_edges()
# Handle any "using" directives. # 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) var using_match: RegExMatch = regex.USING_REGEX.search(raw_line)
if "state" in using_match.names: if "state" in using_match.names:
var using_state: String = using_match.strings[using_match.names.state].strip_edges() var using_state: String = using_match.strings[using_match.names.state].strip_edges()
if not using_state in autoload_names: 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: elif not using_state in using_states:
using_states.append(using_state) using_states.append(using_state)
continue continue
@ -249,18 +250,22 @@ func build_line_tree(raw_lines: PackedStringArray) -> DMTreeLine:
tree_line.notes = "\n".join(doc_comments) tree_line.notes = "\n".join(doc_comments)
doc_comments.clear() doc_comments.clear()
# Empty lines are only kept so that we can work out groupings of things (eg. responses and # Empty lines are only kept so that we can work out groupings of things (eg. randomised
# randomised lines). Therefore we only need to keep one empty line in a row even if there # 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 # 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. # 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: if tree_line.type in [DMConstants.TYPE_UNKNOWN, DMConstants.TYPE_COMMENT] and raw_lines.size() > i + 1:
var next_line = raw_lines[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 continue
else: else:
tree_line.type = DMConstants.TYPE_UNKNOWN tree_line.type = DMConstants.TYPE_UNKNOWN
tree_line.indent = get_indent(next_line) 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 # Check for indentation changes
if tree_line.indent > parent_chain.size() - 1: if tree_line.indent > parent_chain.size() - 1:
parent_chain.append(previous_line) 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. # Check that all children are when or else.
for child in tree_line.children: for child in tree_line.children:
if child.type == DMConstants.TYPE_WHEN: continue 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 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) 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) result = add_error(tree_line.line_number, condition.index, condition.error)
else: else:
line.expression = condition 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() tree_line.text = regex.WRAPPED_CONDITION_REGEX.sub(tree_line.text, "").strip_edges()
# Find the original response in this group of responses. # Find the original response in this group of responses.
var original_response: DMTreeLine = tree_line 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: if siblings[i].type == DMConstants.TYPE_RESPONSE:
original_response = siblings[i] original_response = siblings[i]
elif siblings[i].type != DMConstants.TYPE_UNKNOWN: 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()): for i in range(0, tree_line.children.size()):
var child: DMTreeLine = tree_line.children[i] var child: DMTreeLine = tree_line.children[i]
if child.type == DMConstants.TYPE_DIALOGUE: 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 tree_line.text += "\n" + child.text
elif child.type == DMConstants.TYPE_UNKNOWN:
tree_line.text += "\n"
else: else:
result = add_error(child.line_number, child.indent, DMConstants.ERR_INVALID_INDENTATION) result = add_error(child.line_number, child.indent, DMConstants.ERR_INVALID_INDENTATION)
# Extract the static line ID # Extract the static line ID
var static_line_id: String = extract_static_line_id(tree_line.text) var static_line_id: String = extract_static_line_id(tree_line.text)
if static_line_id: 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 line.translation_key = static_line_id
# Check for simultaneous lines # Check for simultaneous lines
@ -730,7 +758,7 @@ func parse_dialogue_line(tree_line: DMTreeLine, line: DMCompiledLine, siblings:
if expression.size() == 0: if expression.size() == 0:
add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_EXPRESSION) add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_INVALID_EXPRESSION)
elif expression[0].type == DMConstants.TYPE_ERROR: 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 # If the line isn't part of a weighted random group then make it point to the next
# available sibling. # available sibling.
@ -817,9 +845,14 @@ func parse_character_and_dialogue(tree_line: DMTreeLine, line: DMCompiledLine, s
# Replace any newlines. # Replace any newlines.
text = text.replace("\\n", "\n").strip_edges() text = text.replace("\\n", "\n").strip_edges()
# If there was no manual translation key then just use the text itself # If there was no manual translation key then just use the text itself (unless this is a
if line.translation_key == "": # child dialogue below another dialogue line).
line.translation_key = text 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 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) result = add_error(tree_line.line_number, tree_line.indent, DMConstants.ERR_DUPLICATE_ID)
else: else:
_known_translation_keys[line.translation_key] = line.text _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 return result
@ -916,6 +946,9 @@ func get_line_type(raw_line: String) -> String:
if text.begins_with("import "): if text.begins_with("import "):
return DMConstants.TYPE_IMPORT return DMConstants.TYPE_IMPORT
if text.begins_with("using "):
return DMConstants.TYPE_USING
if text.begins_with("#"): if text.begins_with("#"):
return DMConstants.TYPE_COMMENT 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: elif expression[0].type == DMConstants.TYPE_ERROR:
return { return {
index = expression[0].index, index = expression[0].i,
error = expression[0].value error = expression[0].value
} }
else: else:
@ -1034,7 +1067,7 @@ func extract_mutation(text: String) -> Dictionary:
} }
elif expression[0].type == DMConstants.TYPE_ERROR: elif expression[0].type == DMConstants.TYPE_ERROR:
return { return {
index = expression[0].index, index = expression[0].i,
error = expression[0].value error = expression[0].value
} }
else: else:

View File

@ -26,6 +26,8 @@ var concurrent_lines: PackedStringArray = []
var tags: PackedStringArray = [] var tags: PackedStringArray = []
## The condition or mutation expression for this line. ## The condition or mutation expression for this line.
var expression: Dictionary = {} 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. ## The next sequential line to go to after this line.
var next_id: String = "" var next_id: String = ""
## The next line to go to after this line if it is unknown and compile time. ## 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 d.siblings = siblings
DMConstants.TYPE_RESPONSE: DMConstants.TYPE_RESPONSE:
# d.text = text.replace("<br>", "\n")
d.text = text d.text = text
if not responses.is_empty(): if not responses.is_empty():
@ -130,9 +131,10 @@ func to_data() -> Dictionary:
d.tags = tags d.tags = tags
if not notes.is_empty(): if not notes.is_empty():
d.notes = notes d.notes = notes
if not expression_text.is_empty():
d.condition_as_text = expression_text
DMConstants.TYPE_DIALOGUE: DMConstants.TYPE_DIALOGUE:
# d.text = text.replace("<br>", "\n")
d.text = text d.text = text
if translation_key != text: if translation_key != text:

View File

@ -14,7 +14,6 @@ static func compile_string(text: String, path: String) -> DMCompilerResult:
result.titles = compilation.titles result.titles = compilation.titles
result.first_title = compilation.first_title result.first_title = compilation.first_title
result.errors = compilation.errors result.errors = compilation.errors
# result.lines = compilation.lines
result.lines = compilation.data result.lines = compilation.data
result.raw_text = text result.raw_text = text

View File

@ -38,6 +38,7 @@ var TOKEN_DEFINITIONS: Dictionary = {
DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"),
DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"),
DMConstants.TOKEN_COMMA: 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_DOT: RegEx.create_from_string("^\\."),
DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"), DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"),
DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"),

View File

@ -2,6 +2,9 @@
class_name DMExpressionParser extends RefCounted class_name DMExpressionParser extends RefCounted
var include_comments: bool = false
# Reference to the common [RegEx] that the parser needs. # Reference to the common [RegEx] that the parser needs.
var regex: DMCompilerRegEx = DMCompilerRegEx.new() var regex: DMCompilerRegEx = DMCompilerRegEx.new()
@ -25,7 +28,7 @@ func tokenise(text: String, line_type: String, index: int) -> Array:
index += 1 index += 1
text = text.substr(1) text = text.substr(1)
else: 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] 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: elif expression[0].type == DMConstants.TYPE_ERROR:
replacement = { replacement = {
index = expression[0].index, index = expression[0].i,
error = expression[0].value error = expression[0].value
} }
else: else:
@ -76,8 +79,13 @@ func extract_replacements(text: String, index: int) -> Array[Dictionary]:
# Create a token that represents an error. # Create a token that represents an error.
func _build_token_tree_error(error: int, index: int) -> Array: func _build_token_tree_error(tree: Array, error: int, index: int) -> Array:
return [{ type = DMConstants.TOKEN_ERROR, value = error, index = index }] tree.insert(0, {
type = DMConstants.TOKEN_ERROR,
value = error,
i = index
})
return tree
# Convert a list of tokens into an abstract syntax 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) var error = _check_next_token(token, tokens, line_type, expected_close_token)
if error != OK: if error != OK:
var error_token: Dictionary = tokens[1] if tokens.size() > 1 else token 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: match token.type:
DMConstants.TOKEN_COMMENT:
if include_comments:
tree.append({
type = DMConstants.TOKEN_COMMENT,
value = token.value,
i = token.index
})
DMConstants.TOKEN_FUNCTION: DMConstants.TOKEN_FUNCTION:
var sub_tree = _build_token_tree(tokens, line_type, DMConstants.TOKEN_PARENS_CLOSE) 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: 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({ tree.append({
type = DMConstants.TOKEN_FUNCTION, 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) 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: 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]) var args = _tokens_to_list(sub_tree[0])
if args.size() != 1: 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({ tree.append({
type = DMConstants.TOKEN_DICTIONARY_REFERENCE, 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) 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: 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] var t = sub_tree[0]
for i in range(0, t.size() - 2): 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) 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: 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 type = DMConstants.TOKEN_ARRAY
var value = _tokens_to_list(sub_tree[0]) 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) 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: 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({ tree.append({
type = DMConstants.TOKEN_GROUP, 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_BRACE_CLOSE, \
DMConstants.TOKEN_BRACKET_CLOSE: DMConstants.TOKEN_BRACKET_CLOSE:
if token.type != expected_close_token: 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({ tree.append({
type = token.type, type = token.type,
@ -211,10 +227,11 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl
DMConstants.TOKEN_COMMA, \ DMConstants.TOKEN_COMMA, \
DMConstants.TOKEN_COLON, \ DMConstants.TOKEN_COLON, \
DMConstants.TOKEN_DOT: DMConstants.TOKEN_DOT, \
DMConstants.TOKEN_NULL_COALESCE:
tree.append({ tree.append({
type = token.type, type = token.type,
i = token.index i = token.index
}) })
DMConstants.TOKEN_COMPARISON, \ DMConstants.TOKEN_COMPARISON, \
@ -230,7 +247,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl
tree.append({ tree.append({
type = token.type, type = token.type,
value = value, value = value,
i = token.index i = token.index
}) })
DMConstants.TOKEN_STRING: DMConstants.TOKEN_STRING:
@ -248,7 +265,7 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl
}) })
DMConstants.TOKEN_CONDITION: 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: DMConstants.TOKEN_BOOL:
tree.append({ tree.append({
@ -280,8 +297,8 @@ func _build_token_tree(tokens: Array[Dictionary], line_type: String, expected_cl
}) })
if expected_close_token != "": if expected_close_token != "":
var index: int = tokens[0].index if tokens.size() > 0 else 0 var index: int = tokens[0].i if tokens.size() > 0 else 0
return [_build_token_tree_error(DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens] return [_build_token_tree_error(tree, DMConstants.ERR_MISSING_CLOSING_BRACKET, index), tokens]
return [tree, 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_COMPARISON, \
DMConstants.TOKEN_OPERATOR, \ DMConstants.TOKEN_OPERATOR, \
DMConstants.TOKEN_COMMA, \
DMConstants.TOKEN_DOT, \ DMConstants.TOKEN_DOT, \
DMConstants.TOKEN_NULL_COALESCE, \
DMConstants.TOKEN_NOT, \ DMConstants.TOKEN_NOT, \
DMConstants.TOKEN_AND_OR, \ DMConstants.TOKEN_AND_OR, \
DMConstants.TOKEN_DICTIONARY_REFERENCE: 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_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: DMConstants.TOKEN_COLON:
unexpected_token_types = [ unexpected_token_types = [
DMConstants.TOKEN_COMMA, DMConstants.TOKEN_COMMA,
@ -409,7 +440,8 @@ func _check_next_token(token: Dictionary, next_tokens: Array[Dictionary], line_t
DMConstants.TOKEN_BRACKET_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): 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: match next_token.type:
null: null:
return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION return DMConstants.ERR_UNEXPECTED_END_OF_EXPRESSION

View File

@ -22,6 +22,8 @@ var text: String = ""
var children: Array[DMTreeLine] = [] var children: Array[DMTreeLine] = []
## Any doc comments attached to this line. ## Any doc comments attached to this line.
var notes: String = "" 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: func _init(initial_id: String) -> void:

View File

@ -53,6 +53,10 @@ var font_size: int:
var WEIGHTED_RANDOM_PREFIX: RegEx = RegEx.create_from_string("^\\%[\\d.]+\\s") 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: func _ready() -> void:
# Add error gutter # Add error gutter
@ -65,6 +69,10 @@ func _ready() -> void:
syntax_highlighter = DMSyntaxHighlighter.new() syntax_highlighter = DMSyntaxHighlighter.new()
# Keep track of any autoloads
ProjectSettings.settings_changed.connect(_on_project_settings_changed)
_on_project_settings_changed()
func _gui_input(event: InputEvent) -> void: func _gui_input(event: InputEvent) -> void:
# Handle shortcuts that come from the editor # 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: if cursor.x > -1 and cursor.y > -1:
set_cursor(cursor) set_cursor(cursor)
remove_secondary_carets() remove_secondary_carets()
if has_method("insert_text"): insert_text("\"%s\"" % file, cursor.y, cursor.x)
call("insert_text", "\"%s\"" % file, cursor.y, cursor.x)
else:
call("insert_text_at_cursor", "\"%s\"" % file)
grab_focus() grab_focus()
@ -144,6 +149,7 @@ func _request_code_completion(force: bool) -> void:
var cursor: Vector2 = get_cursor() var cursor: Vector2 = get_cursor()
var current_line: String = get_line(cursor.y) var current_line: String = get_line(cursor.y)
# Match jumps
if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")): if ("=> " in current_line or "=>< " in current_line) and (cursor.x > current_line.find("=>")):
var prompt: String = current_line.split("=>")[1] var prompt: String = current_line.split("=>")[1]
if prompt.begins_with("< "): 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")) 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)
return
# Match character names
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(), "")
if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]: if name_so_far != "" and name_so_far[0].to_upper() == name_so_far[0]:
# Only show names starting with that character # Only show names starting with that character
@ -179,9 +184,72 @@ func _request_code_completion(force: bool) -> void:
if names.size() > 0: if names.size() > 0:
for name in names: 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")) 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: # Match autoloads on mutation lines
cancel_code_completion() 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: 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()) var completion = get_code_completion_option(get_code_completion_selected_index())
begin_complex_operation() begin_complex_operation()
# Delete any part of the text that we've already typed # Delete any part of the text that we've already typed
for i in range(0, completion.display_text.length() - completion.insert_text.length()): if completion.insert_text.length() > 0:
backspace() for i in range(0, completion.display_text.length() - completion.insert_text.length()):
backspace()
# Insert the whole match # Insert the whole match
insert_text_at_caret(completion.display_text) insert_text_at_caret(completion.display_text)
end_complex_operation() end_complex_operation()
if completion.display_text.ends_with("()"):
set_cursor(get_cursor() - Vector2.RIGHT)
# Close the autocomplete menu on the next tick # Close the autocomplete menu on the next tick
call_deferred("cancel_code_completion") call_deferred("cancel_code_completion")
### Helpers #region Helpers
# Get the current caret as a Vector2 # 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()) 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 ## Get a list of titles from the current text
func get_titles() -> PackedStringArray: func get_titles() -> PackedStringArray:
var titles = PackedStringArray([]) var titles = PackedStringArray([])
@ -420,7 +555,18 @@ func move_line(offset: int) -> void:
scroll_vertical = starting_scroll + offset 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: 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) var line_errors = errors.filter(func(error): return error.line_number == line)
if line_errors.size() > 0: if line_errors.size() > 0:
error_clicked.emit(line) error_clicked.emit(line)
#endregion

View File

@ -14,6 +14,8 @@ func _clear_highlighting_cache() -> void:
func _get_line_syntax_highlighting(line: int) -> Dictionary: func _get_line_syntax_highlighting(line: int) -> Dictionary:
expression_parser.include_comments = true
var colors: Dictionary = {} var colors: Dictionary = {}
var text_edit: TextEdit = get_text_edit() var text_edit: TextEdit = get_text_edit()
var text: String = text_edit.get_line(line) var text: String = text_edit.get_line(line)
@ -31,6 +33,18 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
var index: int = 0 var index: int = 0
match DMCompiler.get_line_type(text): 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: DMConstants.TYPE_COMMENT:
colors[index] = { color = theme.comments_color } colors[index] = { color = theme.comments_color }
@ -42,7 +56,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
index = text.find(" ") index = text.find(" ")
if index > -1: if index > -1:
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0) 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 } colors[index] = { color = theme.critical_color }
else: else:
_highlight_expression(expression, colors, index) _highlight_expression(expression, colors, index)
@ -51,7 +65,7 @@ func _get_line_syntax_highlighting(line: int) -> Dictionary:
colors[0] = { color = theme.mutations_color } colors[0] = { color = theme.mutations_color }
index = text.find(" ") index = text.find(" ")
var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0) 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 } colors[index] = { color = theme.critical_color }
else: else:
_highlight_expression(expression, colors, index) _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("=>")) 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(":") 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 # Interpolation
var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text) 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: for token: Dictionary in tokens:
last_index = token.i last_index = token.i
match token.type: match token.type:
DMConstants.TOKEN_COMMENT:
colors[index + token.i] = { color = theme.comments_color }
DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR:
colors[index + token.i] = { color = theme.conditions_color } colors[index + token.i] = { color = theme.conditions_color }
@ -153,7 +174,9 @@ func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int
else: else:
colors[index + token.i] = { color = theme.members_color } 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 } colors[index + token.i] = { color = theme.symbols_color }
DMConstants.TOKEN_STRING: DMConstants.TOKEN_STRING:

View File

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

View File

@ -9,6 +9,7 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
size_flags_vertical = 3
script = ExtResource("1_cytii") script = ExtResource("1_cytii")
icon = ExtResource("2_3ijx1") icon = ExtResource("2_3ijx1")

View File

@ -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]" 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]" highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]"
else: 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()) 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.text = "%s: %s" % [str(path_result.line).lpad(4), text]
result_label.gui_input.connect(func(event): result_label.gui_input.connect(func(event):

View File

@ -114,7 +114,7 @@ func find_in_line(line: String, text: String, from_index: int = 0) -> int:
return line.findn(text, from_index) return line.findn(text, from_index)
### Signals #region Signals
func _on_text_edit_gui_input(event: InputEvent) -> void: 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 # Replace the selection at result index
var r: Array = results[result_index] var r: Array = results[result_index]
code_edit.begin_complex_operation()
var lines: PackedStringArray = code_edit.text.split("\n") var lines: PackedStringArray = code_edit.text.split("\n")
var line: String = lines[r[0]] var line: String = lines[r[0]]
line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2]) line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2])
lines[r[0]] = line lines[r[0]] = line
code_edit.text = "\n".join(lines) code_edit.text = "\n".join(lines)
search(input.text, result_index) code_edit.end_complex_operation()
code_edit.text_changed.emit() code_edit.text_changed.emit()
search(input.text, result_index)
func _on_replace_all_button_pressed() -> void: func _on_replace_all_button_pressed() -> void:
if match_case_button.button_pressed: 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: func _on_match_case_check_box_toggled(button_pressed: bool) -> void:
search() search()
#endregion

View File

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

View File

@ -37,6 +37,7 @@ const TOKEN_COMPARISON = &"comparison"
const TOKEN_ASSIGNMENT = &"assignment" const TOKEN_ASSIGNMENT = &"assignment"
const TOKEN_OPERATOR = &"operator" const TOKEN_OPERATOR = &"operator"
const TOKEN_COMMA = &"comma" const TOKEN_COMMA = &"comma"
const TOKEN_NULL_COALESCE = &"null_coalesce"
const TOKEN_DOT = &"dot" const TOKEN_DOT = &"dot"
const TOKEN_CONDITION = &"condition" const TOKEN_CONDITION = &"condition"
const TOKEN_BOOL = &"bool" const TOKEN_BOOL = &"bool"
@ -54,6 +55,7 @@ const TOKEN_ERROR = &"error"
const TYPE_UNKNOWN = &"" const TYPE_UNKNOWN = &""
const TYPE_IMPORT = &"import" const TYPE_IMPORT = &"import"
const TYPE_USING = &"using"
const TYPE_COMMENT = &"comment" const TYPE_COMMENT = &"comment"
const TYPE_RESPONSE = &"response" const TYPE_RESPONSE = &"response"
const TYPE_TITLE = &"title" 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_WHEN_MUST_BELONG_TO_MATCH = 138
const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139 const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139
const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140 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 ## Get the error message
@ -203,6 +207,10 @@ static func get_error_message(error: int) -> String:
return translate(&"errors.concurrent_line_without_origin") return translate(&"errors.concurrent_line_without_origin")
ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES:
return translate(&"errors.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") return translate(&"errors.unknown")

View File

@ -185,6 +185,7 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array)
continue continue
elif await _check_case_value(value, case, extra_game_states): elif await _check_case_value(value, case, extra_game_states):
next_id = case.next_id next_id = case.next_id
break
# Nothing matched so check for else case # Nothing matched so check for else case
if next_id == "": if next_id == "":
if not else_case.is_empty(): if not else_case.is_empty():
@ -206,16 +207,6 @@ func get_line(resource: DialogueResource, key: String, extra_game_states: Array)
else: else:
cummulative_weight += sibling.weight 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 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.type in [DMConstants.TYPE_COMMENT, DMConstants.TYPE_UNKNOWN]:
if data.next_id in [DMConstants.ID_END, DMConstants.ID_NULL, null]: 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. # Set up a line object.
var line: DialogueLine = await create_dialogue_line(data, extra_game_states) 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 the jump point somehow has no content then just end.
if not line: return null 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 we are the first of a list of responses then get the other ones.
if data.type == DMConstants.TYPE_RESPONSE: if data.type == DMConstants.TYPE_RESPONSE:
# Note: For some reason C# has occasional issues with using the responses property directly # 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 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. # Call "start" on the given balloon.
func _start_balloon(balloon: Node, resource: DialogueResource, title: String, extra_game_states: Array) -> void: 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"): if balloon.has_method(&"start"):
balloon.start(resource, title, extra_game_states) 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 # Translate a string
func translate(data: Dictionary) -> 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 return data.text
var translation_key: String = data.get(&"translation_key", 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, type = DMConstants.TYPE_RESPONSE,
next_id = data.next_id, next_id = data.next_id,
is_allowed = data.is_allowed, is_allowed = data.is_allowed,
condition_as_text = data.get(&"condition_as_text", ""),
character = await get_resolved_character(data, extra_game_states), character = await get_resolved_character(data, extra_game_states),
character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]), character_replacements = data.get(&"character_replacements", [] as Array[Dictionary]),
text = resolved_data.text, text = resolved_data.text,
text_replacements = data.get(&"text_replacements", [] as Array[Dictionary]), text_replacements = data.get(&"text_replacements", [] as Array[Dictionary]),
tags = data.get(&"tags", []), 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] game_states = [_autoloads]
# Add any other state shortcuts from settings # Add any other state shortcuts from settings
for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): 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: if state:
game_states.append(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) 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 # Check for multiple values
var already_compared: bool = false var expressions_to_check: Array = []
if expression[0].type == DMConstants.TOKEN_COMPARISON: var previous_comma_index: int = 0
expression.insert(0, { for i in range(0, expression.size()):
type = DMConstants.TOKEN_VALUE, if expression[i].type == DMConstants.TOKEN_COMMA:
value = match_value expressions_to_check.append(expression.slice(previous_comma_index, i))
}) previous_comma_index = i + 1
already_compared = true 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: var resolved_value = await _resolve(expression_to_check, extra_game_states)
return resolved_value if (already_compared and resolved_value) or match_value == resolved_value:
else: return true
return match_value == resolved_value
return false
# Make a change to game state or run a method # 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): if state.has(property):
return state.get(property) return state.get(property)
else: 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) var result = expression.execute([], state, false)
if not expression.has_execute_failed(): if not expression.has_execute_failed():
return result 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. # Get the list of state shortcuts.
var state_shortcuts: Array = [] var state_shortcuts: Array = []
for node_name in DMSettings.get_setting(DMSettings.STATE_AUTOLOAD_SHORTCUTS, ""): 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: if state:
state_shortcuts.append(state) state_shortcuts.append(state)
@ -868,7 +895,18 @@ func _resolve(tokens: Array, extra_game_states: Array):
limit += 1 limit += 1
var token: Dictionary = tokens[i] 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 function_name: String = token.function
var args = await _resolve_each(token.value, extra_game_states) var args = await _resolve_each(token.value, extra_game_states)
if tokens[i - 1].type == DMConstants.TOKEN_DOT: 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. # Check that a thing has a given method.
func _thing_has_method(thing, method: String, args: Array) -> bool: func _thing_has_method(thing, method: String, args: Array) -> bool:
if not is_instance_valid(thing):
return false
if Builtins.is_supported(thing, method): if Builtins.is_supported(thing, method):
return thing != _autoloads return thing != _autoloads
elif thing is Dictionary: elif thing is Dictionary:
@ -1345,7 +1386,7 @@ func _thing_has_method(thing, method: String, args: Array) -> bool:
if thing.has_method(method): if thing.has_method(method):
return true 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 # 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) 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: if p.name == property:
return true 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 return false

View File

@ -14,6 +14,9 @@ var next_id: String = ""
## [code]true[/code] if the condition of this line was met. ## [code]true[/code] if the condition of this line was met.
var is_allowed: bool = true var is_allowed: bool = true
## The original condition text.
var condition_as_text: String = ""
## A character (depending on the "characters in responses" behaviour setting). ## A character (depending on the "characters in responses" behaviour setting).
var character: String = "" var character: String = ""
@ -45,6 +48,7 @@ func _init(data: Dictionary = {}) -> void:
text_replacements = data.text_replacements text_replacements = data.text_replacements
tags = data.tags tags = data.tags
translation_key = data.translation_key translation_key = data.translation_key
condition_as_text = data.condition_as_text
func _to_string() -> String: func _to_string() -> String:

View File

@ -100,16 +100,20 @@ func _configure_focus() -> void:
if i == 0: if i == 0:
item.focus_neighbor_top = item.get_path() item.focus_neighbor_top = item.get_path()
item.focus_neighbor_left = item.get_path()
item.focus_previous = item.get_path() item.focus_previous = item.get_path()
else: else:
item.focus_neighbor_top = items[i - 1].get_path() 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() item.focus_previous = items[i - 1].get_path()
if i == items.size() - 1: if i == items.size() - 1:
item.focus_neighbor_bottom = item.get_path() item.focus_neighbor_bottom = item.get_path()
item.focus_neighbor_right = item.get_path()
item.focus_next = item.get_path() item.focus_next = item.get_path()
else: else:
item.focus_neighbor_bottom = items[i + 1].get_path() 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.focus_next = items[i + 1].get_path()
item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) item.mouse_entered.connect(_on_response_mouse_entered.bind(item))

View File

@ -40,7 +40,7 @@ corner_radius_top_right = 5
corner_radius_bottom_right = 5 corner_radius_bottom_right = 5
corner_radius_bottom_left = 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) bg_color = Color(0, 0, 0, 1)
border_width_left = 3 border_width_left = 3
border_width_top = 3 border_width_top = 3
@ -61,7 +61,7 @@ MarginContainer/constants/margin_bottom = 15
MarginContainer/constants/margin_left = 30 MarginContainer/constants/margin_left = 30
MarginContainer/constants/margin_right = 30 MarginContainer/constants/margin_right = 30
MarginContainer/constants/margin_top = 15 MarginContainer/constants/margin_top = 15
Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt")
[node name="ExampleBalloon" type="CanvasLayer"] [node name="ExampleBalloon" type="CanvasLayer"]
layer = 100 layer = 100
@ -77,33 +77,28 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
theme = SubResource("Theme_qq3yp") theme = SubResource("Theme_qq3yp")
[node name="Panel" type="Panel" parent="Balloon"] [node name="MarginContainer" type="MarginContainer" 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
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = 21.0 offset_top = -219.0
offset_top = -183.0
offset_right = -19.0
offset_bottom = -19.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"]
clip_children = 2
layout_mode = 2
mouse_filter = 1 mouse_filter = 1
[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] [node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"]
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"]
layout_mode = 2 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 unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.501961) modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2 layout_mode = 2
@ -113,37 +108,35 @@ text = "Character"
fit_content = true fit_content = true
scroll_active = false 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
text = "Dialogue..." text = "Dialogue..."
[node name="Responses" type="MarginContainer" parent="Balloon"] [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")]
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")]
unique_name_in_owner = true 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 size_flags_vertical = 8
theme_override_constants/separation = 2 theme_override_constants/separation = 2
alignment = 1
script = ExtResource("3_72ixx") script = ExtResource("3_72ixx")
response_template = NodePath("ResponseExample") response_template = NodePath("ResponseExample")
[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] [node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"]
layout_mode = 2 layout_mode = 2
text = "Response example" text = "Response example"
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] [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"]

View File

@ -66,7 +66,7 @@ corner_radius_top_right = 3
corner_radius_bottom_right = 3 corner_radius_bottom_right = 3
corner_radius_bottom_left = 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) bg_color = Color(0, 0, 0, 1)
border_width_left = 1 border_width_left = 1
border_width_top = 1 border_width_top = 1
@ -87,7 +87,7 @@ MarginContainer/constants/margin_bottom = 4
MarginContainer/constants/margin_left = 8 MarginContainer/constants/margin_left = 8
MarginContainer/constants/margin_right = 8 MarginContainer/constants/margin_right = 8
MarginContainer/constants/margin_top = 4 MarginContainer/constants/margin_top = 4
Panel/styles/panel = SubResource("StyleBoxFlat_uy0d5") PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm")
[node name="ExampleBalloon" type="CanvasLayer"] [node name="ExampleBalloon" type="CanvasLayer"]
layer = 100 layer = 100
@ -103,33 +103,28 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
theme = SubResource("Theme_qq3yp") theme = SubResource("Theme_qq3yp")
[node name="Panel" type="Panel" parent="Balloon"] [node name="MarginContainer" type="MarginContainer" 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
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = 3.0 offset_top = -71.0
offset_top = -62.0
offset_right = -4.0
offset_bottom = -4.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
[node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"]
clip_children = 2
layout_mode = 2
mouse_filter = 1 mouse_filter = 1
[node name="Dialogue" type="MarginContainer" parent="Balloon/Panel"] [node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"]
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"]
layout_mode = 2 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 unique_name_in_owner = true
modulate = Color(1, 1, 1, 0.501961) modulate = Color(1, 1, 1, 0.501961)
layout_mode = 2 layout_mode = 2
@ -139,36 +134,33 @@ text = "Character"
fit_content = true fit_content = true
scroll_active = false 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
text = "Dialogue..." text = "Dialogue..."
[node name="Responses" type="MarginContainer" parent="Balloon"] [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -124.0
offset_top = -218.0
offset_right = 125.0
offset_bottom = -50.0
grow_horizontal = 2
grow_vertical = 0
[node name="ResponsesMenu" type="VBoxContainer" parent="Balloon/Responses"]
unique_name_in_owner = true 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 size_flags_vertical = 8
theme_override_constants/separation = 2 theme_override_constants/separation = 2
script = ExtResource("3_1j1j0") script = ExtResource("3_1j1j0")
[node name="ResponseExample" type="Button" parent="Balloon/Responses/ResponsesMenu"] [node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"]
layout_mode = 2 layout_mode = 2
text = "Response Example" text = "Response Example"
[connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] [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"]

View File

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

View File

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

View File

@ -5,12 +5,15 @@ class_name DMImportPlugin extends EditorImportPlugin
signal compiled_resource(resource: Resource) signal compiled_resource(resource: Resource)
const COMPILER_VERSION = 14 const COMPILER_VERSION = 15
func _get_importer_name() -> String: func _get_importer_name() -> String:
# NOTE: A change to this forces a re-import of all dialogue return "dialogue_manager"
return "dialogue_manager_compiler_%s" % COMPILER_VERSION
func _get_format_version() -> int:
return COMPILER_VERSION
func _get_visible_name() -> String: 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: if result.errors.size() > 0:
printerr("%d errors found in %s" % [result.errors.size(), source_file]) printerr("%d errors found in %s" % [result.errors.size(), source_file])
cache.add_errors_to_file(source_file, result.errors) cache.add_errors_to_file(source_file, result.errors)
return ERR_PARSE_ERROR return OK
# Get the current addon version # Get the current addon version
var config: ConfigFile = ConfigFile.new() var config: ConfigFile = ConfigFile.new()

Binary file not shown.

View File

@ -5,7 +5,7 @@ msgstr ""
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: de\n" "Language: en\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"
@ -33,6 +33,9 @@ msgstr "Clear recent files"
msgid "save_all_files" msgid "save_all_files"
msgstr "Save all files" msgstr "Save all files"
msgid "all"
msgstr "All"
msgid "find_in_files" msgid "find_in_files"
msgstr "Find in files..." msgstr "Find in files..."
@ -321,6 +324,12 @@ msgstr "Concurrent lines need an origin line that doesn't start with \"| \"."
msgid "errors.goto_not_allowed_on_concurrect_lines" msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr "Goto references are not allowed on concurrent dialogue 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" msgid "errors.unknown"
msgstr "Unknown syntax." msgstr "Unknown syntax."

View File

@ -26,6 +26,9 @@ msgstr ""
msgid "save_all_files" msgid "save_all_files"
msgstr "" msgstr ""
msgid "all"
msgstr ""
msgid "find_in_files" msgid "find_in_files"
msgstr "" msgstr ""
@ -311,6 +314,12 @@ msgstr ""
msgid "errors.goto_not_allowed_on_concurrect_lines" msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr "" msgstr ""
msgid "errors.unexpected_syntax_on_nested_dialogue_line"
msgstr ""
msgid "errors.err_nested_dialogue_invalid_jump"
msgstr ""
msgid "errors.unknown" msgid "errors.unknown"
msgstr "" msgstr ""

View File

@ -320,6 +320,12 @@ msgstr "Паралельні рядки потребують початково
msgid "errors.goto_not_allowed_on_concurrect_lines" msgid "errors.goto_not_allowed_on_concurrect_lines"
msgstr "У паралельних діалогових рядках не допускаються Goto посилання." msgstr "У паралельних діалогових рядках не допускаються Goto посилання."
msgid "errors.unexpected_syntax_on_nested_dialogue_line"
msgstr "Вкладені рядки діалогу можуть містити лише діалог."
msgid "errors.err_nested_dialogue_invalid_jump"
msgstr "Лише останній рядок вкладеного діалогу може містити перехід."
msgid "errors.unknown" msgid "errors.unknown"
msgstr "Невідомий синтаксис." msgstr "Невідомий синтаксис."

View File

@ -3,5 +3,5 @@
name="Dialogue Manager" name="Dialogue Manager"
description="A powerful nonlinear dialogue system" description="A powerful nonlinear dialogue system"
author="Nathan Hoad" author="Nathan Hoad"
version="3.4.0" version="3.6.3"
script="plugin.gd" script="plugin.gd"

View File

@ -6,15 +6,22 @@ const MainView = preload("./views/main_view.tscn")
var import_plugin: DMImportPlugin var import_plugin: DMImportPlugin
var export_plugin: DMExportPlugin
var inspector_plugin: DMInspectorPlugin var inspector_plugin: DMInspectorPlugin
var translation_parser_plugin: DMTranslationParserPlugin var translation_parser_plugin: DMTranslationParserPlugin
var main_view var main_view
var dialogue_cache: DMCache var dialogue_cache: DMCache
func _enter_tree() -> void: func _enable_plugin() -> void:
add_autoload_singleton("DialogueManager", get_plugin_path() + "/dialogue_manager.gd") 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(): if Engine.is_editor_hint():
Engine.set_meta("DialogueManagerPlugin", self) Engine.set_meta("DialogueManagerPlugin", self)
@ -26,6 +33,9 @@ func _enter_tree() -> void:
import_plugin = DMImportPlugin.new() import_plugin = DMImportPlugin.new()
add_import_plugin(import_plugin) add_import_plugin(import_plugin)
export_plugin = DMExportPlugin.new()
add_export_plugin(export_plugin)
inspector_plugin = DMInspectorPlugin.new() inspector_plugin = DMInspectorPlugin.new()
add_inspector_plugin(inspector_plugin) add_inspector_plugin(inspector_plugin)
@ -101,11 +111,12 @@ func _enter_tree() -> void:
func _exit_tree() -> void: func _exit_tree() -> void:
remove_autoload_singleton("DialogueManager")
remove_import_plugin(import_plugin) remove_import_plugin(import_plugin)
import_plugin = null import_plugin = null
remove_export_plugin(export_plugin)
export_plugin = null
remove_inspector_plugin(inspector_plugin) remove_inspector_plugin(inspector_plugin)
inspector_plugin = null inspector_plugin = null
@ -165,6 +176,11 @@ func _apply_changes() -> void:
_update_localization() _update_localization()
func _save_external_data() -> void:
if dialogue_cache != null:
dialogue_cache.reimport_files()
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
DMSettings.check_for_dotnet_solution() DMSettings.check_for_dotnet_solution()
@ -313,6 +329,9 @@ func update_import_paths(from_path: String, to_path: String) -> void:
func _update_localization() -> void: func _update_localization() -> void:
if not DMSettings.get_setting(DMSettings.UPDATE_POT_FILES_AUTOMATICALLY, true):
return
var dialogue_files = dialogue_cache.get_files() var dialogue_files = dialogue_cache.get_files()
# Add any new files to POT generation # Add any new files to POT generation

View File

@ -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" const INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS = "editor/translations/include_character_in_translation_exports"
## Includes a "_notes" column in CSV exports ## Includes a "_notes" column in CSV exports
const INCLUDE_NOTES_IN_TRANSLATION_EXPORTS = "editor/translations/include_notes_in_translation_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. ## A custom test scene to use when testing dialogue.
const CUSTOM_TEST_SCENE_PATH = "editor/advanced/custom_test_scene_path" 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. ## The custom balloon for this game.
const BALLOON_PATH = "runtime/balloon_path" 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 USES_DOTNET = "runtime/advanced/uses_dotnet"
const SETTINGS_CONFIGURATION = { static var SETTINGS_CONFIGURATION = {
WRAP_LONG_LINES: { WRAP_LONG_LINES: {
value = false, value = false,
type = TYPE_BOOL, type = TYPE_BOOL,
@ -68,6 +72,8 @@ const SETTINGS_CONFIGURATION = {
EXTRA_CSV_LOCALES: { EXTRA_CSV_LOCALES: {
value = [], value = [],
type = TYPE_PACKED_STRING_ARRAY, type = TYPE_PACKED_STRING_ARRAY,
hint = PROPERTY_HINT_TYPE_STRING,
hint_string = "%d:" % [TYPE_STRING],
is_advanced = true is_advanced = true
}, },
INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: { INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS: {
@ -80,6 +86,11 @@ const SETTINGS_CONFIGURATION = {
type = TYPE_BOOL, type = TYPE_BOOL,
is_advanced = true is_advanced = true
}, },
UPDATE_POT_FILES_AUTOMATICALLY: {
value = true,
type = TYPE_BOOL,
is_advanced = true
},
CUSTOM_TEST_SCENE_PATH: { CUSTOM_TEST_SCENE_PATH: {
value = preload("./test_scene.tscn").resource_path, value = preload("./test_scene.tscn").resource_path,
@ -87,6 +98,13 @@ const SETTINGS_CONFIGURATION = {
hint = PROPERTY_HINT_FILE, hint = PROPERTY_HINT_FILE,
is_advanced = true 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: { BALLOON_PATH: {
value = "", value = "",
@ -96,6 +114,8 @@ const SETTINGS_CONFIGURATION = {
STATE_AUTOLOAD_SHORTCUTS: { STATE_AUTOLOAD_SHORTCUTS: {
value = [], value = [],
type = TYPE_PACKED_STRING_ARRAY, type = TYPE_PACKED_STRING_ARRAY,
hint = PROPERTY_HINT_TYPE_STRING,
hint_string = "%d:" % [TYPE_STRING],
}, },
WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: { WARN_ABOUT_METHOD_PROPERTY_OR_SIGNAL_NAME_CONFLICTS: {
value = false, value = false,
@ -203,7 +223,7 @@ static func get_user_config() -> Dictionary:
recent_files = [], recent_files = [],
reopen_files = [], reopen_files = [],
most_recent_reopen_file = "", most_recent_reopen_file = "",
carets = {}, file_meta = {},
run_title = "", run_title = "",
run_resource_path = "", run_resource_path = "",
is_running_test_scene = false, is_running_test_scene = false,
@ -229,10 +249,17 @@ static func set_user_value(key: String, value) -> void:
save_user_config(user_config) 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) 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: static func add_recent_file(path: String) -> void:
var recent_files: Array = get_user_value("recent_files", []) var recent_files: Array = get_user_value("recent_files", [])
if path in 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: static func set_caret(path: String, cursor: Vector2) -> void:
var carets: Dictionary = get_user_value("carets", {}) var file_meta: Dictionary = get_user_value("file_meta", {})
carets[path] = { file_meta[path] = file_meta.get(path, {}).merged({ cursor = "%d,%d" % [cursor.x, cursor.y] }, true)
x = cursor.x, set_user_value("file_meta", file_meta)
y = cursor.y
}
set_user_value("carets", carets)
static func get_caret(path: String) -> Vector2: static func get_caret(path: String) -> Vector2:
var carets = get_user_value("carets", {}) var file_meta: Dictionary = get_user_value("file_meta", {})
if carets.has(path): if file_meta.has(path):
var caret = carets.get(path) var cursor: PackedStringArray = file_meta.get(path).get("cursor", "0,0").split(",")
return Vector2(caret.x, caret.y) return Vector2(cursor[0].to_int(), cursor[1].to_int())
else: else:
return Vector2.ZERO 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: static func check_for_dotnet_solution() -> bool:
if Engine.is_editor_hint(): if Engine.is_editor_hint():
var has_dotnet_solution: bool = false var has_dotnet_solution: bool = false

View File

@ -10,17 +10,11 @@ const DialogueResource = preload("./dialogue_resource.gd")
func _ready(): func _ready():
# Is this running in Godot >=4.4? if not Engine.is_embedded_in_editor:
if Engine.has_method("is_embedded_in_editor"): var window: Window = get_viewport()
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() 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) window.position = Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - window.size) * 0.5
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) window.mode = 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.

View File

@ -37,10 +37,11 @@ func reimport_files(and_files: PackedStringArray = []) -> void:
for file in and_files: for file in and_files:
if not _files_marked_for_reimport.has(file): if not _files_marked_for_reimport.has(file):
_files_marked_for_reimport.append(file) _files_marked_for_reimport.append(file)
if _files_marked_for_reimport.is_empty(): return if _files_marked_for_reimport.is_empty(): return
EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport)
_files_marked_for_reimport.clear()
## Add a dialogue file to the cache. ## Add a dialogue file to the cache.

View File

@ -98,16 +98,23 @@ var current_file_path: String = "":
title_list.show() title_list.show()
code_edit.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.text = open_buffers[current_file_path].text
code_edit.errors = [] code_edit.errors = []
code_edit.clear_undo_history() 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() code_edit.grab_focus()
_on_code_edit_text_changed() _on_code_edit_text_changed()
errors_panel.errors = [] errors_panel.errors = []
code_edit.errors = [] code_edit.errors = []
if search_and_replace.visible:
search_and_replace.search()
get: get:
return current_file_path return current_file_path
@ -177,6 +184,8 @@ func _ready() -> void:
EditorInterface.get_file_system_dock().files_moved.connect(_on_files_moved) 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: func _exit_tree() -> void:
DMSettings.set_user_value("reopen_files", open_buffers.keys()) 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") open_button.tooltip_text = DMConstants.translate(&"open_a_file")
save_all_button.icon = get_theme_icon("Save", "EditorIcons") 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") save_all_button.tooltip_text = DMConstants.translate(&"start_all_files")
find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons") find_in_files_button.icon = get_theme_icon("ViewportZoom", "EditorIcons")
@ -501,6 +511,8 @@ func generate_translations_keys() -> void:
var key_regex = RegEx.new() var key_regex = RegEx.new()
key_regex.compile("\\[ID:(?<key>.*?)\\]") key_regex.compile("\\[ID:(?<key>.*?)\\]")
var compiled_lines: Dictionary = DMCompiler.compile_string(code_edit.text, "").lines
# Make list of known keys # Make list of known keys
var known_keys = {} var known_keys = {}
for i in range(0, lines.size()): for i in range(0, lines.size()):
@ -523,6 +535,7 @@ func generate_translations_keys() -> void:
var l = line.strip_edges() var l = line.strip_edges()
if not [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE].has(DMCompiler.get_line_type(l)): continue 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 if "[ID:" in line: continue
@ -563,6 +576,7 @@ func generate_translations_keys() -> void:
# Add a translation file to the project settings # Add a translation file to the project settings
func add_path_to_project_translations(path: String) -> void: func add_path_to_project_translations(path: String) -> void:
var translations: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations") var translations: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations")
# 不更新 translation 设置
# if not path in translations: # if not path in translations:
# translations.append(path) # translations.append(path)
# ProjectSettings.save() # ProjectSettings.save()
@ -830,6 +844,16 @@ func show_search_form(is_enabled: bool) -> void:
search_and_replace.focus_line_edit() 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 ### Signals
@ -1014,6 +1038,10 @@ func _on_code_edit_text_changed() -> void:
parse_timer.start(1) 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: func _on_code_edit_active_title_change(title: String) -> void:
title_list.select_title(title) title_list.select_title(title)
@ -1064,12 +1092,7 @@ func _on_test_button_pressed() -> void:
errors_dialog.popup_centered() errors_dialog.popup_centered()
return return
DMSettings.set_user_value("run_title", "") run_test_scene("")
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")
func _on_test_line_button_pressed() -> void: 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()): for i in range(code_edit.get_cursor().y, code_edit.get_line_count()):
if not code_edit.get_line(i).is_empty(): if not code_edit.get_line(i).is_empty():
line_to_run = i line_to_run = i
break; break
DMSettings.set_user_value("run_title", str(line_to_run))
DMSettings.set_user_value("is_running_test_scene", true) run_test_scene(str(line_to_run))
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")
func _on_support_button_pressed() -> void: 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: func _on_find_in_files_result_selected(path: String, cursor: Vector2, length: int) -> void:
open_file(path) open_file(path)
code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length) code_edit.select(cursor.y, cursor.x, cursor.y, cursor.x + length)
code_edit.set_line_as_center_visible(cursor.y)

View File

@ -66,10 +66,10 @@ oneshot_animation = ""
[node name="冷飕飕Sfx" parent="Ground/AnimationPlayer" index="0" instance=ExtResource("3_fvldj")] [node name="冷飕飕Sfx" parent="Ground/AnimationPlayer" index="0" instance=ExtResource("3_fvldj")]
stream = null stream = null
mode = "交互与效果音" mode = "交互与效果音"
audio_dict = Dictionary[String, AudioStream]({})
[node name="背景音效" type="AudioStreamPlayer" parent="Ground/AnimationPlayer" index="1"] [node name="背景音效" type="AudioStreamPlayer" parent="Ground/AnimationPlayer" index="1"]
stream = ExtResource("6_36l5t") stream = ExtResource("6_36l5t")
autoplay = true
bus = &"game_sfx" bus = &"game_sfx"
script = ExtResource("14_jg8g0") script = ExtResource("14_jg8g0")
mode = "场景背景音" mode = "场景背景音"

View File

@ -160,7 +160,6 @@ stream = ExtResource("5_husb8")
bus = &"game_sfx" bus = &"game_sfx"
script = ExtResource("4_qjenp") script = ExtResource("4_qjenp")
mode = "场景背景音" mode = "场景背景音"
audio_dict = Dictionary[String, AudioStream]({})
"自动开始" = false "自动开始" = false
"循环播放" = true "循环播放" = true
"感应玩家操作" = false "感应玩家操作" = false
@ -341,7 +340,6 @@ animation = &"空房间血脚印"
frame = 8 frame = 8
[node name="PointLight2D" type="PointLight2D" parent="Ground/AmbientLayer" index="0"] [node name="PointLight2D" type="PointLight2D" parent="Ground/AmbientLayer" index="0"]
visible = false
position = Vector2(3067, -61) position = Vector2(3067, -61)
energy = 0.7 energy = 0.7
blend_mode = 1 blend_mode = 1