extends Node ## Emitted when a title is encountered while traversing dialogue, usually when jumping from a ## goto line signal passed_title(title) ## Emitted when a line of dialogue is encountered. signal got_dialogue(line) ## Emitted when a mutation is encountered. signal mutated(mutation) ## Emitted when some dialogue has reached the end. signal dialogue_ended(resource) ## Used internally. signal bridge_get_next_dialogue_line_completed(line) const DialogueConstants = preload("./constants.gd") const DialogueSettings = preload("./settings.gd") const DialogueResource = preload("./dialogue_resource.gd") const DialogueLine = preload("./dialogue_line.gd") const DialogueResponse = preload("./dialogue_response.gd") const DialogueManagerParser = preload("./components/parser.gd") const DialogueManagerParseResult = preload("./components/parse_result.gd") const ResolvedLineData = preload("./components/resolved_line_data.gd") enum MutationBehaviour { Wait, DoNotWait, Skip } enum TranslationSource { None, Guess, CSV, PO } ## The list of globals that dialogue can query var game_states: Array = [] ## Allow dialogue to call singletons var include_singletons: bool = true ## Allow dialogue to call static methods/properties on classes var include_classes: bool = true ## Manage translation behaviour var translation_source: TranslationSource = TranslationSource.Guess ## Used to resolve the current scene. Override if your game manages the current scene itself. var get_current_scene: Callable = func(): var current_scene: Node = get_tree().current_scene if current_scene == null: current_scene = get_tree().root.get_child(get_tree().root.get_child_count() - 1) return current_scene var _node_properties: Array = [] func _ready() -> void: # Make the dialogue manager available as a singleton Engine.register_singleton("DialogueManager", self) # Cache the known Node2D properties _node_properties = ["Script Variables"] var temp_node: Node2D = Node2D.new() for property in temp_node.get_property_list(): _node_properties.append(property.name) temp_node.free() # Add any autoloads to a generic state so we can refer to them by name var autoloads: Dictionary = {} for child in get_tree().root.get_children(): # Ignore the dialogue manager if child.name == StringName("DialogueManager"): continue # Ignore the current main scene if get_tree().current_scene and child.name == get_tree().current_scene.name: continue # Add the node to our known autoloads autoloads[child.name] = child game_states = [autoloads] # Add any other state shortcuts from settings for node_name in DialogueSettings.get_setting("states", []): var state: Node = get_node_or_null("/root/" + node_name) if state: game_states.append(state) # Connect up the C# signals if need be if _has_dotnet_solution(): _get_dotnet_dialogue_manager().Prepare() ## Step through lines and run any mutations until we either hit some dialogue or the end of the conversation func get_next_dialogue_line(resource: DialogueResource, key: String = "", extra_game_states: Array = [], mutation_behaviour: MutationBehaviour = MutationBehaviour.Wait) -> DialogueLine: # You have to provide a valid dialogue resource assert(resource != null, DialogueConstants.translate("runtime.no_resource")) assert(resource.lines.size() > 0, DialogueConstants.translate("runtime.no_content").format({ file_path = resource.resource_path })) # Inject any "using" states into the game_states for state_name in resource.using_states: var autoload = get_tree().root.get_node_or_null(state_name) if autoload == null: printerr(DialogueConstants.translate("runtime.unknown_autoload").format({ autoload = state_name })) else: extra_game_states = [autoload] + extra_game_states # Get the line data var dialogue: DialogueLine = await get_line(resource, key, extra_game_states) # If our dialogue is nothing then we hit the end if not is_valid(dialogue): dialogue_ended.emit(resource) return null # Run the mutation if it is one if dialogue.type == DialogueConstants.TYPE_MUTATION: var actual_next_id: String = dialogue.next_id.split(",")[0] match mutation_behaviour: MutationBehaviour.Wait: await mutate(dialogue.mutation, extra_game_states) MutationBehaviour.DoNotWait: mutate(dialogue.mutation, extra_game_states) MutationBehaviour.Skip: pass if actual_next_id in [DialogueConstants.ID_END_CONVERSATION, DialogueConstants.ID_NULL, null]: # End the conversation dialogue_ended.emit(resource) return null else: return await get_next_dialogue_line(resource, dialogue.next_id, extra_game_states, mutation_behaviour) else: got_dialogue.emit(dialogue) return dialogue func get_resolved_line_data(data: Dictionary, extra_game_states: Array = []) -> ResolvedLineData: var text: String = translate(data) # Resolve variables for replacement in data.text_replacements: var value = await resolve(replacement.expression.duplicate(true), extra_game_states) text = text.replace(replacement.value_in_text, str(value)) var parser: DialogueManagerParser = DialogueManagerParser.new() # Resolve random groups for found in parser.INLINE_RANDOM_REGEX.search_all(text): var options = found.get_string("options").split("|") text = text.replace("[[%s]]" % found.get_string("options"), options[randi_range(0, options.size() - 1)]) # Do a pass on the markers to find any conditionals var markers: ResolvedLineData = parser.extract_markers(text) # Resolve any conditionals and update marker positions as needed var resolved_text: String = markers.text var conditionals: Array[RegExMatch] = parser.INLINE_CONDITIONALS_REGEX.search_all(resolved_text) var replacements: Array = [] for conditional in conditionals: var condition_raw: String = conditional.strings[conditional.names.condition] var body: String = conditional.strings[conditional.names.body] var body_else: String = "" if "[else]" in body: var bits = body.split("[else]") body = bits[0] body_else = bits[1] var condition: Dictionary = parser.extract_condition("if " + condition_raw, false, 0) # If the condition fails then use the else of "" if not await check_condition({ condition = condition }, extra_game_states): body = body_else replacements.append({ start = conditional.get_start(), end = conditional.get_end(), string = conditional.get_string(), body = body }) for i in range(replacements.size() -1, -1, -1): var r: Dictionary = replacements[i] resolved_text = resolved_text.substr(0, r.start) + r.body + resolved_text.substr(r.end, 9999) # Move any other markers now that the text has changed var offset: int = r.end - r.start - r.body.length() for key in ["pauses", "speeds", "time"]: if markers.get(key) == null: continue var marker = markers.get(key) var next_marker: Dictionary = {} for index in marker: if index < r.start: next_marker[index] = marker[index] elif index > r.start: next_marker[index - offset] = marker[index] markers.set(key, next_marker) var mutations: Array[Array] = markers.mutations var next_mutations: Array[Array] = [] for mutation in mutations: var index = mutation[0] if index < r.start: next_mutations.append(mutation) elif index > r.start: next_mutations.append([index - offset, mutation[1]]) markers.mutations = next_mutations markers.text = resolved_text parser.free() return markers ## Replace any variables, etc in the character name func get_resolved_character(data: Dictionary, extra_game_states: Array = []) -> String: var character: String = data.get("character", "") # Resolve variables for replacement in data.get("character_replacements", []): var value = await resolve(replacement.expression.duplicate(true), extra_game_states) character = character.replace(replacement.value_in_text, str(value)) # Resolve random groups var random_regex: RegEx = RegEx.new() random_regex.compile("\\[\\[(?.*?)\\]\\]") for found in random_regex.search_all(character): var options = found.get_string("options").split("|") character = character.replace("[[%s]]" % found.get_string("options"), options[randi_range(0, options.size() - 1)]) return character ## Generate a dialogue resource on the fly from some text func create_resource_from_text(text: String) -> Resource: var parser: DialogueManagerParser = DialogueManagerParser.new() parser.parse(text, "") var results: DialogueManagerParseResult = parser.get_data() var errors: Array[Dictionary] = parser.get_errors() parser.free() if errors.size() > 0: printerr(DialogueConstants.translate("runtime.errors").format({ count = errors.size() })) for error in errors: printerr(DialogueConstants.translate("runtime.error_detail").format({ line = error.line_number + 1, message = DialogueConstants.get_error_message(error.error) })) assert(false, DialogueConstants.translate("runtime.errors_see_details").format({ count = errors.size() })) var resource: DialogueResource = DialogueResource.new() resource.titles = results.titles resource.character_names = results.character_names resource.lines = results.lines return resource ## Show the example balloon func show_example_dialogue_balloon(resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> CanvasLayer: var balloon: Node = load(_get_example_balloon_path()).instantiate() get_current_scene.call().add_child(balloon) balloon.start(resource, title, extra_game_states) return balloon ## Show the configured dialogue balloon func show_dialogue_balloon(resource: DialogueResource, title: String = "", extra_game_states: Array = []) -> Node: var balloon: Node = load(DialogueSettings.get_setting("balloon_path", _get_example_balloon_path())).instantiate() get_current_scene.call().add_child(balloon) balloon.start(resource, title, extra_game_states) return balloon # Get the path to the example balloon func _get_example_balloon_path() -> String: var is_small_window: bool = ProjectSettings.get_setting("display/window/size/viewport_width") < 400 var balloon_path: String = "/example_balloon/small_example_balloon.tscn" if is_small_window else "/example_balloon/example_balloon.tscn" return get_script().resource_path.get_base_dir() + balloon_path ### Dotnet bridge func _has_dotnet_solution() -> bool: if not DialogueSettings.get_setting("has_dotnet_solution", false): return false if not ResourceLoader.exists("res://addons/dialogue_manager/DialogueManager.cs"): return false if load("res://addons/dialogue_manager/DialogueManager.cs") == null: return false return true func _get_dotnet_dialogue_manager() -> Node: return load("res://addons/dialogue_manager/DialogueManager.cs").new() func _bridge_get_next_dialogue_line(resource: DialogueResource, key: String, extra_game_states: Array = []) -> void: # dotnet needs at least one await tick of the signal gets called too quickly await get_tree().process_frame var line = await get_next_dialogue_line(resource, key, extra_game_states) bridge_get_next_dialogue_line_completed.emit(line) ### Helpers # Get a line by its ID func get_line(resource: DialogueResource, key: String, extra_game_states: Array) -> DialogueLine: key = key.strip_edges() # See if we were given a stack instead of just the one key var stack: Array = key.split("|") key = stack.pop_front() var id_trail: String = "" if stack.size() == 0 else "|" + "|".join(stack) # Key is blank so just use the first title if key == null or key == "": key = resource.first_title # See if we just ended the conversation if key in [DialogueConstants.ID_END, DialogueConstants.ID_NULL, null]: if stack.size() > 0: return await get_line(resource, "|".join(stack), extra_game_states) else: return null elif key == DialogueConstants.ID_END_CONVERSATION: return null # See if it is a title if key.begins_with("~ "): key = key.substr(2) if resource.titles.has(key): key = resource.titles.get(key) if key in resource.titles.values(): passed_title.emit(resource.titles.find_key(key)) assert(resource.lines.has(key), DialogueConstants.translate("errors.key_not_found").format({ key = key })) var data: Dictionary = resource.lines.get(key) # Check for weighted random lines if data.has("siblings"): var target_weight: float = randf_range(0, data.siblings.reduce(func(total, sibling): return total + sibling.weight, 0)) var cummulative_weight: float = 0 for sibling in data.siblings: if target_weight < cummulative_weight + sibling.weight: data = resource.lines.get(sibling.id) break else: cummulative_weight += sibling.weight # Check condtiions if data.type == DialogueConstants.TYPE_CONDITION: # "else" will have no actual condition if await check_condition(data, extra_game_states): return await get_line(resource, data.next_id + id_trail, extra_game_states) else: return await get_line(resource, data.next_conditional_id + id_trail, extra_game_states) # Evaluate jumps elif data.type == DialogueConstants.TYPE_GOTO: if data.is_snippet: id_trail = "|" + data.next_id_after + id_trail return await get_line(resource, data.next_id + id_trail, extra_game_states) elif data.type == DialogueConstants.TYPE_DIALOGUE: if not data.has("id"): data.id = key # Set up a line object var line: DialogueLine = await create_dialogue_line(data, extra_game_states) # If the jump point somehow has no content then just end if not line: return null # If we are the first of a list of responses then get the other ones if data.type == DialogueConstants.TYPE_RESPONSE: line.responses = await get_responses(data.responses, resource, id_trail, extra_game_states) return line # Inject the next node's responses if they have any if resource.lines.has(line.next_id): var next_line: Dictionary = resource.lines.get(line.next_id) if next_line != null and next_line.type == DialogueConstants.TYPE_RESPONSE: line.responses = await get_responses(next_line.responses, resource, id_trail, extra_game_states) line.next_id += id_trail return line # Show a message or crash with error func show_error_for_missing_state_value(message: String, will_show: bool = true) -> void: if not will_show: return if DialogueSettings.get_setting("ignore_missing_state_values", false): push_error(message) else: # If you're here then you're missing a method or property in your game state. The error # message down in the debugger will give you some more information. assert(not will_show, message) # Translate a string func translate(data: Dictionary) -> String: if translation_source == TranslationSource.None: return data.text if data.translation_key == "" or data.translation_key == data.text: return tr(data.text) else: # Line IDs work slightly differently depending on whether the translation came from a # CSV or a PO file. CSVs use the line ID (or the line itself) as the translatable string # whereas POs use the ID as context and the line itself as the translatable string. match translation_source: TranslationSource.PO: return tr(data.text, StringName(data.translation_key)) TranslationSource.CSV: return tr(data.translation_key) TranslationSource.Guess: var translation_files: Array = ProjectSettings.get_setting("internationalization/locale/translations") if translation_files.filter(func(f: String): return f.get_extension() == "po").size() > 0: # Assume PO return tr(data.text, StringName(data.translation_key)) else: # Assume CSV return tr(data.translation_key) return tr(data.translation_key) # Create a line of dialogue func create_dialogue_line(data: Dictionary, extra_game_states: Array) -> DialogueLine: match data.type: DialogueConstants.TYPE_DIALOGUE: var resolved_data: ResolvedLineData = await get_resolved_line_data(data, extra_game_states) return DialogueLine.new({ id = data.get("id", ""), type = DialogueConstants.TYPE_DIALOGUE, next_id = data.next_id, character = await get_resolved_character(data, extra_game_states), character_replacements = data.character_replacements, text = resolved_data.text, text_replacements = data.text_replacements, translation_key = data.translation_key, pauses = resolved_data.pauses, speeds = resolved_data.speeds, inline_mutations = resolved_data.mutations, time = resolved_data.time, tags = data.get("tags", []), extra_game_states = extra_game_states }) DialogueConstants.TYPE_RESPONSE: return null DialogueConstants.TYPE_MUTATION: return DialogueLine.new({ id = data.get("id", ""), type = DialogueConstants.TYPE_MUTATION, next_id = data.next_id, mutation = data.mutation, extra_game_states = extra_game_states }) return null # Create a response func create_response(data: Dictionary, extra_game_states: Array) -> DialogueResponse: var resolved_data: ResolvedLineData = await get_resolved_line_data(data, extra_game_states) return DialogueResponse.new({ id = data.get("id", ""), type = DialogueConstants.TYPE_RESPONSE, next_id = data.next_id, is_allowed = await check_condition(data, extra_game_states), character = await get_resolved_character(data, extra_game_states), character_replacements = data.get("character_replacements", [] as Array[Dictionary]), text = resolved_data.text, text_replacements = data.text_replacements, tags = data.get("tags", []), translation_key = data.translation_key }) # Get the current game states func get_game_states(extra_game_states: Array) -> Array: var current_scene: Node = get_current_scene.call() var unique_states: Array = [] for state in extra_game_states + [current_scene] + game_states: if state != null and not unique_states.has(state): unique_states.append(state) return unique_states # Check if a condition is met func check_condition(data: Dictionary, extra_game_states: Array) -> bool: if data.get("condition", null) == null: return true if data.condition.size() == 0: return true return await resolve(data.condition.expression.duplicate(true), extra_game_states) # Make a change to game state or run a method func mutate(mutation: Dictionary, extra_game_states: Array, is_inline_mutation: bool = false) -> void: var expression: Array[Dictionary] = mutation.expression # Handle built in mutations if expression[0].type == DialogueConstants.TOKEN_FUNCTION and expression[0].function in ["wait", "debug"]: var args: Array = await resolve_each(expression[0].value, extra_game_states) match expression[0].function: "wait": mutated.emit(mutation) await get_tree().create_timer(float(args[0])).timeout return "debug": prints("Debug:", args) await get_tree().process_frame # Or pass through to the resolver else: if not mutation_contains_assignment(mutation.expression) and not is_inline_mutation: mutated.emit(mutation) await resolve(mutation.expression.duplicate(true), extra_game_states) return # Wait one frame to give the dialogue handler a chance to yield await get_tree().process_frame func mutation_contains_assignment(mutation: Array) -> bool: for token in mutation: if token.type == DialogueConstants.TOKEN_ASSIGNMENT: return true return false func resolve_each(array: Array, extra_game_states: Array) -> Array: var results: Array = [] for item in array: results.append(await resolve(item.duplicate(true), extra_game_states)) return results # Replace an array of line IDs with their response prompts func get_responses(ids: Array, resource: DialogueResource, id_trail: String, extra_game_states: Array) -> Array[DialogueResponse]: var responses: Array[DialogueResponse] = [] for id in ids: var data: Dictionary = resource.lines.get(id) if DialogueSettings.get_setting("include_all_responses", false) or await check_condition(data, extra_game_states): var response: DialogueResponse = await create_response(data, extra_game_states) response.next_id += id_trail responses.append(response) return responses # Get a value on the current scene or game state func get_state_value(property: String, extra_game_states: Array): # Special case for static primitive calls if property == "Color": return Color() var expression = Expression.new() if expression.parse(property) != OK: assert(false, DialogueConstants.translate("runtime.invalid_expression").format({ expression = property, error = expression.get_error_text() })) for state in get_game_states(extra_game_states): if typeof(state) == TYPE_DICTIONARY: if state.has(property): return state.get(property) else: var result = expression.execute([], state, false) if not expression.has_execute_failed(): return result if include_singletons and Engine.has_singleton(property): return Engine.get_singleton(property) if include_classes: for class_data in ProjectSettings.get_global_class_list(): if class_data.get("class") == property: return load(class_data.path).new() show_error_for_missing_state_value(DialogueConstants.translate("runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) })) # Set a value on the current scene or game state func set_state_value(property: String, value, extra_game_states: Array) -> void: for state in get_game_states(extra_game_states): if typeof(state) == TYPE_DICTIONARY: if state.has(property): state[property] = value return elif thing_has_property(state, property): state.set(property, value) return if property.to_snake_case() != property: show_error_for_missing_state_value(DialogueConstants.translate("runtime.property_not_found_missing_export").format({ property = property, states = str(get_game_states(extra_game_states)) })) else: show_error_for_missing_state_value(DialogueConstants.translate("runtime.property_not_found").format({ property = property, states = str(get_game_states(extra_game_states)) })) # Collapse any expressions func resolve(tokens: Array, extra_game_states: Array): # Handle groups first for token in tokens: if token.type == DialogueConstants.TOKEN_GROUP: token["type"] = "value" token["value"] = await resolve(token.value, extra_game_states) # Then variables/methods var i: int = 0 var limit: int = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_FUNCTION: var function_name: String = token.function var args = await resolve_each(token.value, extra_game_states) match function_name: "str": token["type"] = "value" token["value"] = str(args[0]) "Vector2": token["type"] = "value" token["value"] = Vector2(args[0], args[1]) "Vector2i": token["type"] = "value" token["value"] = Vector2i(args[0], args[1]) "Vector3": token["type"] = "value" token["value"] = Vector3(args[0], args[1], args[2]) "Vector3i": token["type"] = "value" token["value"] = Vector3i(args[0], args[1], args[2]) "Vector4": token["type"] = "value" token["value"] = Vector4(args[0], args[1], args[2], args[3]) "Vector4i": token["type"] = "value" token["value"] = Vector4i(args[0], args[1], args[2], args[3]) "Quaternion": token["type"] = "value" token["value"] = Quaternion(args[0], args[1], args[2], args[3]) "Color": token["type"] = "value" match args.size(): 0: token["value"] = Color() 1: token["value"] = Color(args[0]) 2: token["value"] = Color(args[0], args[1]) 3: token["value"] = Color(args[0], args[1], args[2]) 4: token["value"] = Color(args[0], args[1], args[2], args[3]) "load": token["type"] = "value" token["value"] = load(args[0]) _: if tokens[i - 1].type == DialogueConstants.TOKEN_DOT: # If we are calling a deeper function then we need to collapse the # value into the thing we are calling the function on var caller: Dictionary = tokens[i - 2] if typeof(caller.value) in DialogueConstants.SUPPORTED_PRIMITIVES: caller["type"] = "value" caller["value"] = resolve_primitive_method(caller.value, function_name, args) tokens.remove_at(i) tokens.remove_at(i-1) i -= 2 elif thing_has_method(caller.value, function_name, args): caller["type"] = "value" caller["value"] = await resolve_thing_method(caller.value, function_name, args) tokens.remove_at(i) tokens.remove_at(i-1) i -= 2 else: show_error_for_missing_state_value(DialogueConstants.translate("runtime.method_not_callable").format({ method = function_name, object = str(caller.value) })) else: var found: bool = false if function_name == "emit": token["type"] = "value" token["value"] = resolve_signal(args, extra_game_states) found = true else: for state in get_game_states(extra_game_states): if typeof(state) in DialogueConstants.SUPPORTED_PRIMITIVES and thing_has_method(state, function_name, args): token["type"] = "value" token["value"] = resolve_primitive_method(state, function_name, args) found = true elif thing_has_method(state, function_name, args): token["type"] = "value" token["value"] = await resolve_thing_method(state, function_name, args) found = true if found: break show_error_for_missing_state_value(DialogueConstants.translate("runtime.method_not_found").format({ method = args[0] if function_name in ["call", "call_deferred"] else function_name, states = str(get_game_states(extra_game_states)) }), not found) elif token.type == DialogueConstants.TOKEN_DICTIONARY_REFERENCE: var value if i > 0 and tokens[i - 1].type == DialogueConstants.TOKEN_DOT: # If we are deep referencing then we need to get the parent object. # `parent.value` is the actual object and `token.variable` is the name of # the property within it. value = tokens[i - 2].value[token.variable] # Clean up the previous tokens token.erase("variable") tokens.remove_at(i - 1) tokens.remove_at(i - 2) i -= 2 else: # Otherwise we can just get this variable as a normal state reference value = get_state_value(token.variable, extra_game_states) var index = await resolve(token.value, extra_game_states) if typeof(value) == TYPE_DICTIONARY: if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: # If the next token is an assignment then we need to leave this as a reference # so that it can be resolved once everything ahead of it has been resolved token["type"] = "dictionary" token["value"] = value token["key"] = index else: if value.has(index): token["type"] = "value" token["value"] = value[index] else: show_error_for_missing_state_value(DialogueConstants.translate("runtime.key_not_found").format({ key = str(index), dictionary = token.variable })) elif typeof(value) == TYPE_ARRAY: if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: # If the next token is an assignment then we need to leave this as a reference # so that it can be resolved once everything ahead of it has been resolved token["type"] = "array" token["value"] = value token["key"] = index else: if index >= 0 and index < value.size(): token["type"] = "value" token["value"] = value[index] else: show_error_for_missing_state_value(DialogueConstants.translate("runtime.array_index_out_of_bounds").format({ index = index, array = token.variable })) elif token.type == DialogueConstants.TOKEN_DICTIONARY_NESTED_REFERENCE: var dictionary: Dictionary = tokens[i - 1] var index = await resolve(token.value, extra_game_states) var value = dictionary.value if typeof(value) == TYPE_DICTIONARY: if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: # If the next token is an assignment then we need to leave this as a reference # so that it can be resolved once everything ahead of it has been resolved dictionary["type"] = "dictionary" dictionary["key"] = index dictionary["value"] = value tokens.remove_at(i) i -= 1 else: if dictionary.value.has(index): dictionary["value"] = value.get(index) tokens.remove_at(i) i -= 1 else: show_error_for_missing_state_value(DialogueConstants.translate("runtime.key_not_found").format({ key = str(index), dictionary = value })) elif typeof(value) == TYPE_ARRAY: if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: # If the next token is an assignment then we need to leave this as a reference # so that it can be resolved once everything ahead of it has been resolved dictionary["type"] = "array" dictionary["value"] = value dictionary["key"] = index tokens.remove_at(i) i -= 1 else: if index >= 0 and index < value.size(): dictionary["value"] = value[index] tokens.remove_at(i) i -= 1 else: show_error_for_missing_state_value(DialogueConstants.translate("runtime.array_index_out_of_bounds").format({ index = index, array = value })) elif token.type == DialogueConstants.TOKEN_ARRAY: token["type"] = "value" token["value"] = await resolve_each(token.value, extra_game_states) elif token.type == DialogueConstants.TOKEN_DICTIONARY: token["type"] = "value" var dictionary = {} for key in token.value.keys(): var resolved_key = await resolve([key], extra_game_states) var preresolved_value = token.value.get(key) if typeof(preresolved_value) != TYPE_ARRAY: preresolved_value = [preresolved_value] var resolved_value = await resolve(preresolved_value, extra_game_states) dictionary[resolved_key] = resolved_value token["value"] = dictionary elif token.type == DialogueConstants.TOKEN_VARIABLE or token.type == DialogueConstants.TOKEN_NUMBER: if str(token.value) == "null": token["type"] = "value" token["value"] = null elif tokens[i - 1].type == DialogueConstants.TOKEN_DOT: var caller: Dictionary = tokens[i - 2] var property = token.value if tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: # If the next token is an assignment then we need to leave this as a reference # so that it can be resolved once everything ahead of it has been resolved caller["type"] = "property" caller["property"] = property else: # If we are requesting a deeper property then we need to collapse the # value into the thing we are referencing from caller["type"] = "value" if typeof(caller.value) == TYPE_ARRAY: caller["value"] = caller.value[property] elif typeof(caller.value) == TYPE_COLOR: caller["value"] = caller.value[property] else: caller["value"] = caller.value.get(property) tokens.remove_at(i) tokens.remove_at(i-1) i -= 2 elif tokens.size() > i + 1 and tokens[i + 1].type == DialogueConstants.TOKEN_ASSIGNMENT: # It's a normal variable but we will be assigning to it so don't resolve # it until everything after it has been resolved token["type"] = "variable" else: token["type"] = "value" token["value"] = get_state_value(str(token.value), extra_game_states) i += 1 # Then multiply and divide i = 0 limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["*", "/", "%"]: token["type"] = "value" token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) tokens.remove_at(i+1) tokens.remove_at(i-1) i -= 1 i += 1 assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong")) # Then addition and subtraction i = 0 limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_OPERATOR and token.value in ["+", "-"]: token["type"] = "value" token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) tokens.remove_at(i+1) tokens.remove_at(i-1) i -= 1 i += 1 assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong")) # Then negations i = 0 limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_NOT: token["type"] = "value" token["value"] = not tokens[i+1].value tokens.remove_at(i+1) i -= 1 i += 1 assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong")) # Then comparisons i = 0 limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_COMPARISON: token["type"] = "value" token["value"] = compare(token.value, tokens[i-1].value, tokens[i+1].value) tokens.remove_at(i+1) tokens.remove_at(i-1) i -= 1 i += 1 assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong")) # Then and/or i = 0 limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_AND_OR: token["type"] = "value" token["value"] = apply_operation(token.value, tokens[i-1].value, tokens[i+1].value) tokens.remove_at(i+1) tokens.remove_at(i-1) i -= 1 i += 1 assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong")) # Lastly, resolve any assignments i = 0 limit = 0 while i < tokens.size() and limit < 1000: limit += 1 var token: Dictionary = tokens[i] if token.type == DialogueConstants.TOKEN_ASSIGNMENT: var lhs: Dictionary = tokens[i - 1] var value match lhs.type: "variable": value = apply_operation(token.value, get_state_value(lhs.value, extra_game_states), tokens[i+1].value) set_state_value(lhs.value, value, extra_game_states) "property": value = apply_operation(token.value, lhs.value.get(lhs.property), tokens[i+1].value) if typeof(lhs.value) == TYPE_DICTIONARY: lhs.value[lhs.property] = value else: lhs.value.set(lhs.property, value) "dictionary": value = apply_operation(token.value, lhs.value.get(lhs.key, null), tokens[i+1].value) lhs.value[lhs.key] = value "array": show_error_for_missing_state_value( DialogueConstants.translate("runtime.array_index_out_of_bounds").format({ index = lhs.key, array = lhs.value }), lhs.key >= lhs.value.size() ) value = apply_operation(token.value, lhs.value[lhs.key], tokens[i+1].value) lhs.value[lhs.key] = value _: show_error_for_missing_state_value(DialogueConstants.translate("runtime.left_hand_size_cannot_be_assigned_to")) token["type"] = "value" token["value"] = value tokens.remove_at(i+1) tokens.remove_at(i-1) i -= 1 i += 1 assert(limit < 1000, DialogueConstants.translate("runtime.something_went_wrong")) # Account for Signal literals in emit calls if tokens[0].value is Signal: return tokens[0].value.get_name() return tokens[0].value func compare(operator: String, first_value, second_value) -> bool: match operator: "in": if first_value == null or second_value == null: return false else: return first_value in second_value "<": if first_value == null: return true elif second_value == null: return false else: return first_value < second_value ">": if first_value == null: return false elif second_value == null: return true else: return first_value > second_value "<=": if first_value == null: return true elif second_value == null: return false else: return first_value <= second_value ">=": if first_value == null: return false elif second_value == null: return true else: return first_value >= second_value "==": if first_value == null: if typeof(second_value) == TYPE_BOOL: return second_value == false else: return false else: return first_value == second_value "!=": if first_value == null: if typeof(second_value) == TYPE_BOOL: return second_value == true else: return false else: return first_value != second_value return false func apply_operation(operator: String, first_value, second_value): match operator: "=": return second_value "+", "+=": return first_value + second_value "-", "-=": return first_value - second_value "/", "/=": return first_value / second_value "*", "*=": return first_value * second_value "%": return first_value % second_value "and": return first_value and second_value "or": return first_value or second_value assert(false, DialogueConstants.translate("runtime.unknown_operator")) # Check if a dialogue line contains meaningful information func is_valid(line: DialogueLine) -> bool: if line == null: return false if line.type == DialogueConstants.TYPE_MUTATION and line.mutation == null: return false if line.type == DialogueConstants.TYPE_RESPONSE and line.responses.size() == 0: return false return true func thing_has_method(thing, method: String, args: Array) -> bool: match typeof(thing): TYPE_DICTIONARY: return method in DialogueConstants.SUPPORTED_DICTIONARY_METHODS TYPE_ARRAY: return method in DialogueConstants.SUPPORTED_ARRAY_METHODS TYPE_QUATERNION: return method in DialogueConstants.SUPPORTED_QUATERNION_METHODS TYPE_COLOR: return method in DialogueConstants.SUPPORTED_COLOR_METHODS TYPE_SIGNAL: return method == "emit" if method in ["call", "call_deferred"]: return thing.has_method(args[0]) if thing.has_method(method): return true if method.to_snake_case() != method and _has_dotnet_solution(): # 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) return false # Check if a given property exists func thing_has_property(thing: Object, property: String) -> bool: if thing == null: return false for p in thing.get_property_list(): if _node_properties.has(p.name): # Ignore any properties on the base Node continue if p.name == property: return true return false func resolve_signal(args: Array, extra_game_states: Array): for state in get_game_states(extra_game_states): if typeof(state) == TYPE_DICTIONARY: continue elif state.has_signal(args[0]): match args.size(): 1: state.emit_signal(args[0]) 2: state.emit_signal(args[0], args[1]) 3: state.emit_signal(args[0], args[1], args[2]) 4: state.emit_signal(args[0], args[1], args[2], args[3]) 5: state.emit_signal(args[0], args[1], args[2], args[3], args[4]) 6: state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5]) 7: state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) 8: state.emit_signal(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) return # The signal hasn't been found anywhere show_error_for_missing_state_value(DialogueConstants.translate("runtime.signal_not_found").format({ signal_name = args[0], states = str(get_game_states(extra_game_states)) })) func resolve_thing_method(thing, method: String, args: Array): if thing.has_method(method): return await thing.callv(method, args) # If we get here then it's probably a C# method with a Task return type var dotnet_dialogue_manager = _get_dotnet_dialogue_manager() dotnet_dialogue_manager.ResolveThingMethod(thing, method, args) return await dotnet_dialogue_manager.Resolved func resolve_primitive_method(primitive, method_name: String, args: Array): match typeof(primitive): TYPE_ARRAY: return resolve_array_method(primitive, method_name, args) TYPE_DICTIONARY: return resolve_dictionary_method(primitive, method_name, args) TYPE_QUATERNION: return resolve_quaternion_method(primitive, method_name, args) TYPE_COLOR: return resolve_color_method(primitive, method_name, args) TYPE_SIGNAL: match args.size(): 0: primitive.emit() 1: primitive.emit(args[0]) 2: primitive.emit(args[0], args[1]) 3: primitive.emit(args[0], args[1], args[2]) 4: primitive.emit(args[0], args[1], args[2], args[3]) 5: primitive.emit(args[0], args[1], args[2], args[3], args[4]) 6: primitive.emit(args[0], args[1], args[2], args[3], args[4], args[5]) 7: primitive.emit(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) 8: primitive.emit(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) return null func resolve_array_method(array: Array, method_name: String, args: Array): match method_name: "assign": array.assign(args[0]) return null "append": array.append(args[0]) return null "append_array": array.append_array(args[0]) return null "back": return array.back() "count": return array.count(args[0]) "clear": array.clear() return null "erase": array.erase(args[0]) return null "has": return array.has(args[0]) "insert": return array.insert(args[0], args[1]) "is_empty": return array.is_empty() "max": return array.max() "min": return array.min() "pick_random": return array.pick_random() "pop_at": return array.pop_at(args[0]) "pop_back": return array.pop_back() "pop_front": return array.pop_front() "push_back": array.push_back(args[0]) return null "push_front": array.push_front(args[0]) return null "remove_at": array.remove_at(args[0]) return null "reverse": array.reverse() return null "shuffle": array.shuffle() return null "size": return array.size() "sort": array.sort() return null show_error_for_missing_state_value(DialogueConstants.translate("runtime.unsupported_array_method").format({ method_name = method_name })) func resolve_dictionary_method(dictionary: Dictionary, method_name: String, args: Array): match method_name: "has": return dictionary.has(args[0]) "has_all": return dictionary.has_all(args[0]) "get": return dictionary.get(args[0]) "keys": return dictionary.keys() "values": return dictionary.values() "size": return dictionary.size() show_error_for_missing_state_value(DialogueConstants.translate("runtime.unsupported_dictionary_method").format({ method_name = method_name })) func resolve_quaternion_method(quaternion: Quaternion, method_name: String, args: Array): match method_name: "angle_to": return quaternion.angle_to(args[0]) "dot": return quaternion.dot(args[0]) "exp": return quaternion.exp() "from_euler": return Quaternion.from_euler(args[0]) "get_angle": return quaternion.get_angle() "get_axis": return quaternion.get_axis() "get_euler": return quaternion.get_euler() if args.size() == 0 else quaternion.get_euler(args[0]) "inverse": return quaternion.inverse() "is_equal_approx": return quaternion.is_equal_approx(args[0]) "is_finite": return quaternion.is_finite() "is_normalized": return quaternion.is_normalized() "length": return quaternion.length() "length_squared": return quaternion.length_squared() "log": return quaternion.log() "normalized": return quaternion.normalized() "slerp": return quaternion.slerp(args[0], args[1]) "slerpni": return quaternion.slerpni(args[0], args[1]) "spherical_cubic_interpolate": return quaternion.spherical_cubic_interpolate(args[0], args[1], args[2], args[3]) "spherical_cubic_interpolate_in_time": return quaternion.spherical_cubic_interpolate_in_time(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) show_error_for_missing_state_value(DialogueConstants.translate("runtime.unsupported_quaternion_method").format({ method_name = method_name })) func resolve_color_method(color: Color, method_name: String, args: Array): match method_name: "blend": return color.blend(args[0]) "clamp": match args.size(): 0: return color.clamp() 1: return color.clamp(args[0]) 2: return color.clamp(args[0], args[1]) "darkened": return color.darkened(args[0]) "from_hsv": match args.size(): 3: return Color.from_hsv(args[0], args[1], args[2]) 4: return Color.from_hsv(args[0], args[1], args[2], args[3]) "from_ok_hsl": match args.size(): 3: return Color.from_ok_hsl(args[0], args[1], args[2]) 4: return Color.from_ok_hsl(args[0], args[1], args[2], args[3]) "from_rgbe9995": return Color.from_rgbe9995(args[0]) "from_string": return Color.from_string(args[0], args[1]) "get_luminance": return color.get_luminance() "hex": return Color.hex(args[0]) "hex64": return Color.hex64(args[0]) "html": return Color.html(args[0]) "html_is_valid": return Color.html_is_valid(args[0]) "inverted": return color.inverted() "is_equal_approx": return color.is_equal_approx(args[0]) "lerp": return color.lerp(args[0], args[1]) "lightened": return color.lightened(args[0]) "linear_to_srgb": return color.linear_to_srgb() "srgb_to_linear": return color.srgb_to_linear() "to_abgr32": return color.to_abgr32() "to_abgr64": return color.to_abgr64() "to_argb32": return color.to_argb32() "to_argb64": return color.to_argb64() "to_html": match args.size(): 0: return color.to_html() 1: return color.to_html(args[0]) "to_rgba32": return color.to_rgba32() "to_rgba64": return color.to_rgba64()