└── addons └── dialogue_manager ├── DialogueManager.cs ├── DialogueManager.cs.uid ├── LICENSE ├── assets ├── icon.svg ├── icon.svg.import ├── responses_menu.svg ├── responses_menu.svg.import ├── update.svg └── update.svg.import ├── compiler ├── compilation.gd ├── compilation.gd.uid ├── compiled_line.gd ├── compiled_line.gd.uid ├── compiler.gd ├── compiler.gd.uid ├── compiler_regex.gd ├── compiler_regex.gd.uid ├── compiler_result.gd ├── compiler_result.gd.uid ├── expression_parser.gd ├── expression_parser.gd.uid ├── resolved_goto_data.gd ├── resolved_goto_data.gd.uid ├── resolved_line_data.gd ├── resolved_line_data.gd.uid ├── resolved_tag_data.gd ├── resolved_tag_data.gd.uid ├── tree_line.gd └── tree_line.gd.uid ├── components ├── code_edit.gd ├── code_edit.gd.uid ├── code_edit.tscn ├── code_edit_syntax_highlighter.gd ├── code_edit_syntax_highlighter.gd.uid ├── download_update_panel.gd ├── download_update_panel.gd.uid ├── download_update_panel.tscn ├── editor_property │ ├── editor_property.gd │ ├── editor_property.gd.uid │ ├── editor_property_control.gd │ ├── editor_property_control.gd.uid │ ├── editor_property_control.tscn │ ├── resource_button.gd │ ├── resource_button.gd.uid │ └── resource_button.tscn ├── errors_panel.gd ├── errors_panel.gd.uid ├── errors_panel.tscn ├── files_list.gd ├── files_list.gd.uid ├── files_list.tscn ├── find_in_files.gd ├── find_in_files.gd.uid ├── find_in_files.tscn ├── search_and_replace.gd ├── search_and_replace.gd.uid ├── search_and_replace.tscn ├── title_list.gd ├── title_list.gd.uid ├── title_list.tscn ├── update_button.gd ├── update_button.gd.uid └── update_button.tscn ├── constants.gd ├── constants.gd.uid ├── dialogue_label.gd ├── dialogue_label.gd.uid ├── dialogue_label.tscn ├── dialogue_line.gd ├── dialogue_line.gd.uid ├── dialogue_manager.gd ├── dialogue_manager.gd.uid ├── dialogue_resource.gd ├── dialogue_resource.gd.uid ├── dialogue_response.gd ├── dialogue_response.gd.uid ├── dialogue_responses_menu.gd ├── dialogue_responses_menu.gd.uid ├── editor_translation_parser_plugin.gd ├── editor_translation_parser_plugin.gd.uid ├── example_balloon ├── ExampleBalloon.cs ├── ExampleBalloon.cs.uid ├── example_balloon.gd ├── example_balloon.gd.uid ├── example_balloon.tscn └── small_example_balloon.tscn ├── export_plugin.gd ├── export_plugin.gd.uid ├── import_plugin.gd ├── import_plugin.gd.uid ├── inspector_plugin.gd ├── inspector_plugin.gd.uid ├── l10n ├── en.po ├── es.po ├── translations.pot ├── uk.po ├── zh.po └── zh_TW.po ├── plugin.cfg ├── plugin.cfg.uid ├── plugin.gd ├── plugin.gd.uid ├── settings.gd ├── settings.gd.uid ├── test_scene.gd ├── test_scene.gd.uid ├── test_scene.tscn ├── utilities ├── builtins.gd ├── builtins.gd.uid ├── dialogue_cache.gd └── dialogue_cache.gd.uid └── views ├── main_view.gd ├── main_view.gd.uid └── main_view.tscn /addons/dialogue_manager/DialogueManager.cs.uid: -------------------------------------------------------------------------------- 1 | uid://c4c5lsrwy3opj 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present Nathan Hoad and Dialogue Manager contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/dialogue_manager/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 40 | 42 | 46 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /addons/dialogue_manager/assets/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d3lr2uas6ax8v" 6 | path="res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex" 7 | metadata={ 8 | "has_editor_variant": true, 9 | "vram_texture": false 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/dialogue_manager/assets/icon.svg" 15 | dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=0 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=1 36 | svg/scale=1.0 37 | editor/scale_with_editor_scale=true 38 | editor/convert_colors_with_editor_theme=true 39 | -------------------------------------------------------------------------------- /addons/dialogue_manager/assets/responses_menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 40 | 42 | 46 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /addons/dialogue_manager/assets/responses_menu.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://drjfciwitjm83" 6 | path="res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex" 7 | metadata={ 8 | "has_editor_variant": true, 9 | "vram_texture": false 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/dialogue_manager/assets/responses_menu.svg" 15 | dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572f4eb8f.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=0 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=1 36 | svg/scale=1.0 37 | editor/scale_with_editor_scale=true 38 | editor/convert_colors_with_editor_theme=true 39 | -------------------------------------------------------------------------------- /addons/dialogue_manager/assets/update.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d3baj6rygkb3f" 6 | path="res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/dialogue_manager/assets/update.svg" 14 | dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compilation.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dsgpnyqg6cprg 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiled_line.gd: -------------------------------------------------------------------------------- 1 | ## A compiled line of dialogue. 2 | class_name DMCompiledLine extends RefCounted 3 | 4 | 5 | ## The ID of the line 6 | var id: String 7 | ## The translation key (or static line ID). 8 | var translation_key: String = "" 9 | ## The type of line. 10 | var type: String = "" 11 | ## The character name. 12 | var character: String = "" 13 | ## Any interpolation expressions for the character name. 14 | var character_replacements: Array[Dictionary] = [] 15 | ## The text of the line. 16 | var text: String = "" 17 | ## Any interpolation expressions for the text. 18 | var text_replacements: Array[Dictionary] = [] 19 | ## Any response siblings associated with this line. 20 | var responses: PackedStringArray = [] 21 | ## Any randomise or case siblings for this line. 22 | var siblings: Array[Dictionary] = [] 23 | ## Any lines said simultaneously. 24 | var concurrent_lines: PackedStringArray = [] 25 | ## Any tags on this line. 26 | var tags: PackedStringArray = [] 27 | ## The condition or mutation expression for this line. 28 | var expression: Dictionary = {} 29 | ## The express as the raw text that was given. 30 | var expression_text: String = "" 31 | ## The next sequential line to go to after this line. 32 | var next_id: String = "" 33 | ## The next line to go to after this line if it is unknown and compile time. 34 | var next_id_expression: Array[Dictionary] = [] 35 | ## Whether this jump line should return after the jump target sequence has ended. 36 | var is_snippet: bool = false 37 | ## The ID of the next sibling line. 38 | var next_sibling_id: String = "" 39 | ## The ID after this line if it belongs to a block (eg. conditions). 40 | var next_id_after: String = "" 41 | ## Any doc comments attached to this line. 42 | var notes: String = "" 43 | 44 | 45 | #region Hooks 46 | 47 | 48 | func _init(initial_id: String, initial_type: String) -> void: 49 | id = initial_id 50 | type = initial_type 51 | 52 | 53 | func _to_string() -> String: 54 | var s: Array = [ 55 | "[%s]" % [type], 56 | "%s:" % [character] if character != "" else null, 57 | text if text != "" else null, 58 | expression if expression.size() > 0 else null, 59 | "[%s]" % [",".join(tags)] if tags.size() > 0 else null, 60 | str(siblings) if siblings.size() > 0 else null, 61 | str(responses) if responses.size() > 0 else null, 62 | "=> END" if "end" in next_id else "=> %s" % [next_id], 63 | "(~> %s)" % [next_sibling_id] if next_sibling_id != "" else null, 64 | "(==> %s)" % [next_id_after] if next_id_after != "" else null, 65 | ].filter(func(item): return item != null) 66 | 67 | return " ".join(s) 68 | 69 | 70 | #endregion 71 | 72 | #region Helpers 73 | 74 | 75 | ## Express this line as a [Dictionary] that can be stored in a resource. 76 | func to_data() -> Dictionary: 77 | var d: Dictionary = { 78 | id = id, 79 | type = type, 80 | next_id = next_id 81 | } 82 | 83 | if next_id_expression.size() > 0: 84 | d.next_id_expression = next_id_expression 85 | 86 | match type: 87 | DMConstants.TYPE_CONDITION: 88 | d.condition = expression 89 | if not next_sibling_id.is_empty(): 90 | d.next_sibling_id = next_sibling_id 91 | d.next_id_after = next_id_after 92 | 93 | DMConstants.TYPE_WHILE: 94 | d.condition = expression 95 | d.next_id_after = next_id_after 96 | 97 | DMConstants.TYPE_MATCH: 98 | d.condition = expression 99 | d.next_id_after = next_id_after 100 | d.cases = siblings 101 | 102 | DMConstants.TYPE_MUTATION: 103 | d.mutation = expression 104 | 105 | DMConstants.TYPE_GOTO: 106 | d.is_snippet = is_snippet 107 | d.next_id_after = next_id_after 108 | if not siblings.is_empty(): 109 | d.siblings = siblings 110 | 111 | DMConstants.TYPE_RANDOM: 112 | d.siblings = siblings 113 | 114 | DMConstants.TYPE_RESPONSE: 115 | d.text = text 116 | 117 | if not responses.is_empty(): 118 | d.responses = responses 119 | 120 | if translation_key != text: 121 | d.translation_key = translation_key 122 | if not expression.is_empty(): 123 | d.condition = expression 124 | if not character.is_empty(): 125 | d.character = character 126 | if not character_replacements.is_empty(): 127 | d.character_replacements = character_replacements 128 | if not text_replacements.is_empty(): 129 | d.text_replacements = text_replacements 130 | if not tags.is_empty(): 131 | d.tags = tags 132 | if not notes.is_empty(): 133 | d.notes = notes 134 | if not expression_text.is_empty(): 135 | d.condition_as_text = expression_text 136 | 137 | DMConstants.TYPE_DIALOGUE: 138 | d.text = text 139 | 140 | if translation_key != text: 141 | d.translation_key = translation_key 142 | 143 | if not character.is_empty(): 144 | d.character = character 145 | if not character_replacements.is_empty(): 146 | d.character_replacements = character_replacements 147 | if not text_replacements.is_empty(): 148 | d.text_replacements = text_replacements 149 | if not tags.is_empty(): 150 | d.tags = tags 151 | if not notes.is_empty(): 152 | d.notes = notes 153 | if not siblings.is_empty(): 154 | d.siblings = siblings 155 | if not concurrent_lines.is_empty(): 156 | d.concurrent_lines = concurrent_lines 157 | 158 | return d 159 | 160 | 161 | #endregion 162 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiled_line.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dg8j5hudp4210 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiler.gd: -------------------------------------------------------------------------------- 1 | ## A compiler of Dialogue Manager dialogue. 2 | class_name DMCompiler extends RefCounted 3 | 4 | 5 | ## Compile a dialogue script. 6 | static func compile_string(text: String, path: String) -> DMCompilerResult: 7 | var compilation: DMCompilation = DMCompilation.new() 8 | compilation.compile(text, path) 9 | 10 | var result: DMCompilerResult = DMCompilerResult.new() 11 | result.imported_paths = compilation.imported_paths 12 | result.using_states = compilation.using_states 13 | result.character_names = compilation.character_names 14 | result.titles = compilation.titles 15 | result.first_title = compilation.first_title 16 | result.errors = compilation.errors 17 | result.lines = compilation.data 18 | result.raw_text = text 19 | 20 | return result 21 | 22 | 23 | ## Get the line type of a string. The returned string will match one of the [code]TYPE_[/code] constants of [DMConstants]. 24 | static func get_line_type(text: String) -> String: 25 | var compilation: DMCompilation = DMCompilation.new() 26 | return compilation.get_line_type(text) 27 | 28 | 29 | ## Get the static line ID (eg. [code][ID:SOMETHING][/code]) of some text. 30 | static func get_static_line_id(text: String) -> String: 31 | var compilation: DMCompilation = DMCompilation.new() 32 | return compilation.extract_static_line_id(text) 33 | 34 | 35 | ## Get the translatable part of a line. 36 | static func extract_translatable_string(text: String) -> String: 37 | var compilation: DMCompilation = DMCompilation.new() 38 | 39 | var tree_line = DMTreeLine.new("") 40 | tree_line.text = text 41 | var line: DMCompiledLine = DMCompiledLine.new("", compilation.get_line_type(text)) 42 | compilation.parse_character_and_dialogue(tree_line, line, [tree_line], 0, null) 43 | 44 | return line.text 45 | 46 | 47 | ## Get the known titles in a dialogue script. 48 | static func get_titles_in_text(text: String, path: String) -> Dictionary: 49 | var compilation: DMCompilation = DMCompilation.new() 50 | compilation.build_line_tree(compilation.inject_imported_files(text, path)) 51 | return compilation.titles 52 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiler.gd.uid: -------------------------------------------------------------------------------- 1 | uid://chtfdmr0cqtp4 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiler_regex.gd: -------------------------------------------------------------------------------- 1 | ## A collection of [RegEx] for use by the [DMCompiler]. 2 | class_name DMCompilerRegEx extends RefCounted 3 | 4 | 5 | var IMPORT_REGEX: RegEx = RegEx.create_from_string("import \"(?[^\"]+)\" as (?[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+)") 6 | var USING_REGEX: RegEx = RegEx.create_from_string("^using (?.*)$") 7 | var INDENT_REGEX: RegEx = RegEx.create_from_string("^\\t+") 8 | var VALID_TITLE_REGEX: RegEx = RegEx.create_from_string("^[a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*$") 9 | var BEGINS_WITH_NUMBER_REGEX: RegEx = RegEx.create_from_string("^\\d") 10 | var CONDITION_REGEX: RegEx = RegEx.create_from_string("(if|elif|while|else if|match|when) (?.*)\\:?") 11 | var WRAPPED_CONDITION_REGEX: RegEx = RegEx.create_from_string("\\[if (?.*)\\]") 12 | var MUTATION_REGEX: RegEx = RegEx.create_from_string("(?do|do!|set) (?.*)") 13 | var STATIC_LINE_ID_REGEX: RegEx = RegEx.create_from_string("\\[ID:(?.*?)\\]") 14 | var WEIGHTED_RANDOM_SIBLINGS_REGEX: RegEx = RegEx.create_from_string("^\\%(?[\\d.]+)?( \\[if (?.+?)\\])? ") 15 | var GOTO_REGEX: RegEx = RegEx.create_from_string("=>.*)") 16 | 17 | var INLINE_RANDOM_REGEX: RegEx = RegEx.create_from_string("\\[\\[(?.*?)\\]\\]") 18 | var INLINE_CONDITIONALS_REGEX: RegEx = RegEx.create_from_string("\\[if (?.+?)\\](?.*?)\\[\\/if\\]") 19 | 20 | var TAGS_REGEX: RegEx = RegEx.create_from_string("\\[#(?.*?)\\]") 21 | 22 | var REPLACEMENTS_REGEX: RegEx = RegEx.create_from_string("{{(.*?)}}") 23 | 24 | var ALPHA_NUMERIC: RegEx = RegEx.create_from_string("[^a-zA-Z0-9\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]+") 25 | 26 | var TOKEN_DEFINITIONS: Dictionary = { 27 | DMConstants.TOKEN_FUNCTION: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\("), 28 | DMConstants.TOKEN_DICTIONARY_REFERENCE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*\\["), 29 | DMConstants.TOKEN_PARENS_OPEN: RegEx.create_from_string("^\\("), 30 | DMConstants.TOKEN_PARENS_CLOSE: RegEx.create_from_string("^\\)"), 31 | DMConstants.TOKEN_BRACKET_OPEN: RegEx.create_from_string("^\\["), 32 | DMConstants.TOKEN_BRACKET_CLOSE: RegEx.create_from_string("^\\]"), 33 | DMConstants.TOKEN_BRACE_OPEN: RegEx.create_from_string("^\\{"), 34 | DMConstants.TOKEN_BRACE_CLOSE: RegEx.create_from_string("^\\}"), 35 | DMConstants.TOKEN_COLON: RegEx.create_from_string("^:"), 36 | DMConstants.TOKEN_COMPARISON: RegEx.create_from_string("^(==|<=|>=|<|>|!=|in )"), 37 | DMConstants.TOKEN_ASSIGNMENT: RegEx.create_from_string("^(\\+=|\\-=|\\*=|/=|=)"), 38 | DMConstants.TOKEN_NUMBER: RegEx.create_from_string("^\\-?\\d+(\\.\\d+)?"), 39 | DMConstants.TOKEN_OPERATOR: RegEx.create_from_string("^(\\+|\\-|\\*|/|%)"), 40 | DMConstants.TOKEN_COMMA: RegEx.create_from_string("^,"), 41 | DMConstants.TOKEN_NULL_COALESCE: RegEx.create_from_string("^\\?\\."), 42 | DMConstants.TOKEN_DOT: RegEx.create_from_string("^\\."), 43 | DMConstants.TOKEN_STRING: RegEx.create_from_string("^&?(\".*?\"|\'.*?\')"), 44 | DMConstants.TOKEN_NOT: RegEx.create_from_string("^(not( |$)|!)"), 45 | DMConstants.TOKEN_AND_OR: RegEx.create_from_string("^(and|or|&&|\\|\\|)( |$)"), 46 | DMConstants.TOKEN_VARIABLE: RegEx.create_from_string("^[a-zA-Z_\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}][a-zA-Z_0-9\\p{Emoji_Presentation}\\p{Han}\\p{Katakana}\\p{Hiragana}\\p{Cyrillic}]*"), 47 | DMConstants.TOKEN_COMMENT: RegEx.create_from_string("^#.*"), 48 | DMConstants.TOKEN_CONDITION: RegEx.create_from_string("^(if|elif|else)"), 49 | DMConstants.TOKEN_BOOL: RegEx.create_from_string("^(true|false)") 50 | } 51 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiler_regex.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d3tvcrnicjibp 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiler_result.gd: -------------------------------------------------------------------------------- 1 | ## The result of using the [DMCompiler] to compile some dialogue. 2 | class_name DMCompilerResult extends RefCounted 3 | 4 | 5 | ## Any paths that were imported into the compiled dialogue file. 6 | var imported_paths: PackedStringArray = [] 7 | 8 | ## Any "using" directives. 9 | var using_states: PackedStringArray = [] 10 | 11 | ## All titles in the file and the line they point to. 12 | var titles: Dictionary = {} 13 | 14 | ## The first title in the file. 15 | var first_title: String = "" 16 | 17 | ## All character names. 18 | var character_names: PackedStringArray = [] 19 | 20 | ## Any compilation errors. 21 | var errors: Array[Dictionary] = [] 22 | 23 | ## A map of all compiled lines. 24 | var lines: Dictionary = {} 25 | 26 | ## The raw dialogue text. 27 | var raw_text: String = "" 28 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/compiler_result.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dmk74tknimqvg 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/expression_parser.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dbi4hbar8ubwu 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/resolved_goto_data.gd: -------------------------------------------------------------------------------- 1 | ## Data associated with a dialogue jump/goto line. 2 | class_name DMResolvedGotoData extends RefCounted 3 | 4 | 5 | ## The title that was specified 6 | var title: String = "" 7 | ## The target line's ID 8 | var next_id: String = "" 9 | ## An expression to determine the target line at runtime. 10 | var expression: Array[Dictionary] = [] 11 | ## The given line text with the jump syntax removed. 12 | var text_without_goto: String = "" 13 | ## Whether this is a jump-and-return style jump. 14 | var is_snippet: bool = false 15 | ## A parse error if there was one. 16 | var error: int 17 | ## The index in the string where 18 | var index: int = 0 19 | 20 | # An instance of the compiler [RegEx] list. 21 | var regex: DMCompilerRegEx = DMCompilerRegEx.new() 22 | 23 | 24 | func _init(text: String, titles: Dictionary) -> void: 25 | if not "=> " in text and not "=>< " in text: return 26 | 27 | if "=> " in text: 28 | text_without_goto = text.substr(0, text.find("=> ")).strip_edges() 29 | elif "=>< " in text: 30 | is_snippet = true 31 | text_without_goto = text.substr(0, text.find("=>< ")).strip_edges() 32 | 33 | var found: RegExMatch = regex.GOTO_REGEX.search(text) 34 | if found == null: 35 | return 36 | 37 | title = found.strings[found.names.goto].strip_edges() 38 | index = found.get_start(0) 39 | 40 | if title == "": 41 | error = DMConstants.ERR_UNKNOWN_TITLE 42 | return 43 | 44 | # "=> END!" means end the conversation, ignoring any "=><" chains. 45 | if title == "END!": 46 | next_id = DMConstants.ID_END_CONVERSATION 47 | 48 | # "=> END" means end the current title (and go back to the previous one if there is one 49 | # in the stack) 50 | elif title == "END": 51 | next_id = DMConstants.ID_END 52 | 53 | elif titles.has(title): 54 | next_id = titles.get(title) 55 | elif title.begins_with("{{"): 56 | var expression_parser: DMExpressionParser = DMExpressionParser.new() 57 | var title_expression: Array[Dictionary] = expression_parser.extract_replacements(title, 0) 58 | if title_expression[0].has("error"): 59 | error = title_expression[0].error 60 | else: 61 | expression = title_expression[0].expression 62 | else: 63 | next_id = title 64 | error = DMConstants.ERR_UNKNOWN_TITLE 65 | 66 | 67 | func _to_string() -> String: 68 | return "%s =>%s %s (%s)" % [text_without_goto, "<" if is_snippet else "", title, next_id] 69 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/resolved_goto_data.gd.uid: -------------------------------------------------------------------------------- 1 | uid://llhl5pt47eoq 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/resolved_line_data.gd: -------------------------------------------------------------------------------- 1 | ## Any data associated with inline dialogue BBCodes. 2 | class_name DMResolvedLineData extends RefCounted 3 | 4 | ## The line's text 5 | var text: String = "" 6 | ## A map of pauses against where they are found in the text. 7 | var pauses: Dictionary = {} 8 | ## A map of speed changes against where they are found in the text. 9 | var speeds: Dictionary = {} 10 | ## A list of any mutations to run and where they are found in the text. 11 | var mutations: Array[Array] = [] 12 | ## A duration reference for the line. Represented as "auto" or a stringified number. 13 | var time: String = "" 14 | 15 | 16 | func _init(line: String) -> void: 17 | text = line 18 | pauses = {} 19 | speeds = {} 20 | mutations = [] 21 | time = "" 22 | 23 | var bbcodes: Array = [] 24 | 25 | # Remove any escaped brackets (ie. "\[") 26 | var escaped_open_brackets: PackedInt32Array = [] 27 | var escaped_close_brackets: PackedInt32Array = [] 28 | for i in range(0, text.length() - 1): 29 | if text.substr(i, 2) == "\\[": 30 | text = text.substr(0, i) + "!" + text.substr(i + 2) 31 | escaped_open_brackets.append(i) 32 | elif text.substr(i, 2) == "\\]": 33 | text = text.substr(0, i) + "!" + text.substr(i + 2) 34 | escaped_close_brackets.append(i) 35 | 36 | # Extract all of the BB codes so that we know the actual text (we could do this easier with 37 | # a RichTextLabel but then we'd need to await idle_frame which is annoying) 38 | var bbcode_positions = find_bbcode_positions_in_string(text) 39 | var accumulaive_length_offset = 0 40 | for position in bbcode_positions: 41 | # Ignore our own markers 42 | if position.code in ["wait", "speed", "/speed", "do", "do!", "set", "next", "if", "else", "/if"]: 43 | continue 44 | 45 | bbcodes.append({ 46 | bbcode = position.bbcode, 47 | start = position.start, 48 | offset_start = position.start - accumulaive_length_offset 49 | }) 50 | accumulaive_length_offset += position.bbcode.length() 51 | 52 | for bb in bbcodes: 53 | text = text.substr(0, bb.offset_start) + text.substr(bb.offset_start + bb.bbcode.length()) 54 | 55 | # Now find any dialogue markers 56 | var next_bbcode_position = find_bbcode_positions_in_string(text, false) 57 | var limit = 0 58 | while next_bbcode_position.size() > 0 and limit < 1000: 59 | limit += 1 60 | 61 | var bbcode = next_bbcode_position[0] 62 | 63 | var index = bbcode.start 64 | var code = bbcode.code 65 | var raw_args = bbcode.raw_args 66 | var args = {} 67 | if code in ["do", "do!", "set"]: 68 | var compilation: DMCompilation = DMCompilation.new() 69 | args["value"] = compilation.extract_mutation("%s %s" % [code, raw_args]) 70 | else: 71 | # Could be something like: 72 | # "=1.0" 73 | # " rate=20 level=10" 74 | if raw_args and raw_args[0] == "=": 75 | raw_args = "value" + raw_args 76 | for pair in raw_args.strip_edges().split(" "): 77 | if "=" in pair: 78 | var bits = pair.split("=") 79 | args[bits[0]] = bits[1] 80 | 81 | match code: 82 | "wait": 83 | if pauses.has(index): 84 | pauses[index] += args.get("value").to_float() 85 | else: 86 | pauses[index] = args.get("value").to_float() 87 | "speed": 88 | speeds[index] = args.get("value").to_float() 89 | "/speed": 90 | speeds[index] = 1.0 91 | "do", "do!", "set": 92 | mutations.append([index, args.get("value")]) 93 | "next": 94 | time = args.get("value") if args.has("value") else "0" 95 | 96 | # Find any BB codes that are after this index and remove the length from their start 97 | var length = bbcode.bbcode.length() 98 | for bb in bbcodes: 99 | if bb.offset_start > bbcode.start: 100 | bb.offset_start -= length 101 | bb.start -= length 102 | 103 | # Find any escaped brackets after this that need moving 104 | for i in range(0, escaped_open_brackets.size()): 105 | if escaped_open_brackets[i] > bbcode.start: 106 | escaped_open_brackets[i] -= length 107 | for i in range(0, escaped_close_brackets.size()): 108 | if escaped_close_brackets[i] > bbcode.start: 109 | escaped_close_brackets[i] -= length 110 | 111 | text = text.substr(0, index) + text.substr(index + length) 112 | next_bbcode_position = find_bbcode_positions_in_string(text, false) 113 | 114 | # Put the BB Codes back in 115 | for bb in bbcodes: 116 | text = text.insert(bb.start, bb.bbcode) 117 | 118 | # Put the escaped brackets back in 119 | for index in escaped_open_brackets: 120 | text = text.left(index) + "[" + text.right(text.length() - index - 1) 121 | for index in escaped_close_brackets: 122 | text = text.left(index) + "]" + text.right(text.length() - index - 1) 123 | 124 | 125 | func find_bbcode_positions_in_string(string: String, find_all: bool = true, include_conditions: bool = false) -> Array[Dictionary]: 126 | if not "[" in string: return [] 127 | 128 | var positions: Array[Dictionary] = [] 129 | 130 | var open_brace_count: int = 0 131 | var start: int = 0 132 | var bbcode: String = "" 133 | var code: String = "" 134 | var is_finished_code: bool = false 135 | for i in range(0, string.length()): 136 | if string[i] == "[": 137 | if open_brace_count == 0: 138 | start = i 139 | bbcode = "" 140 | code = "" 141 | is_finished_code = false 142 | open_brace_count += 1 143 | 144 | else: 145 | if not is_finished_code and (string[i].to_upper() != string[i] or string[i] == "/" or string[i] == "!"): 146 | code += string[i] 147 | else: 148 | is_finished_code = true 149 | 150 | if open_brace_count > 0: 151 | bbcode += string[i] 152 | 153 | if string[i] == "]": 154 | open_brace_count -= 1 155 | if open_brace_count == 0 and (include_conditions or not code in ["if", "else", "/if"]): 156 | positions.append({ 157 | bbcode = bbcode, 158 | code = code, 159 | start = start, 160 | end = i, 161 | raw_args = bbcode.substr(code.length() + 1, bbcode.length() - code.length() - 2).strip_edges() 162 | }) 163 | 164 | if not find_all: 165 | return positions 166 | 167 | return positions 168 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/resolved_line_data.gd.uid: -------------------------------------------------------------------------------- 1 | uid://0k6q8kukq0qa 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/resolved_tag_data.gd: -------------------------------------------------------------------------------- 1 | ## Tag data associated with a line of dialogue. 2 | class_name DMResolvedTagData extends RefCounted 3 | 4 | 5 | ## The list of tags. 6 | var tags: PackedStringArray = [] 7 | ## The line with any tag syntax removed. 8 | var text_without_tags: String = "" 9 | 10 | # An instance of the compiler [RegEx]. 11 | var regex: DMCompilerRegEx = DMCompilerRegEx.new() 12 | 13 | 14 | func _init(text: String) -> void: 15 | var resolved_tags: PackedStringArray = [] 16 | var tag_matches: Array[RegExMatch] = regex.TAGS_REGEX.search_all(text) 17 | for tag_match in tag_matches: 18 | text = text.replace(tag_match.get_string(), "") 19 | var tags = tag_match.get_string().replace("[#", "").replace("]", "").replace(", ", ",").split(",") 20 | for tag in tags: 21 | tag = tag.replace("#", "") 22 | if not tag in resolved_tags: 23 | resolved_tags.append(tag) 24 | 25 | tags = resolved_tags 26 | text_without_tags = text 27 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/resolved_tag_data.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cqai3ikuilqfq 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/tree_line.gd: -------------------------------------------------------------------------------- 1 | ## An intermediate representation of a dialogue line before it gets compiled. 2 | class_name DMTreeLine extends RefCounted 3 | 4 | 5 | ## The line number where this dialogue was found (after imported files have had their content imported). 6 | var line_number: int = 0 7 | ## The parent [DMTreeLine] of this line. 8 | ## This is stored as a Weak Reference so that this RefCounted can elegantly free itself. 9 | ## Without it being a Weak Reference, this can easily cause a cyclical reference that keeps this resource alive. 10 | var parent: WeakRef 11 | ## The ID of this line. 12 | var id: String 13 | ## The type of this line (as a [String] defined in [DMConstants]. 14 | var type: String = "" 15 | ## Is this line part of a randomised group? 16 | var is_random: bool = false 17 | ## The indent count for this line. 18 | var indent: int = 0 19 | ## The text of this line. 20 | var text: String = "" 21 | ## The child [DMTreeLine]s of this line. 22 | var children: Array[DMTreeLine] = [] 23 | ## Any doc comments attached to this line. 24 | var notes: String = "" 25 | ## Is this a dialogue line that is the child of another dialogue line? 26 | var is_nested_dialogue: bool = false 27 | 28 | 29 | func _init(initial_id: String) -> void: 30 | id = initial_id 31 | 32 | 33 | func _to_string() -> String: 34 | var tabs = [] 35 | tabs.resize(indent) 36 | tabs.fill("\t") 37 | tabs = "".join(tabs) 38 | 39 | return tabs.join([tabs + "{\n", 40 | "\tid: %s\n" % [id], 41 | "\ttype: %s\n" % [type], 42 | "\tis_random: %s\n" % ["true" if is_random else "false"], 43 | "\ttext: %s\n" % [text], 44 | "\tnotes: %s\n" % [notes], 45 | "\tchildren: []\n" if children.size() == 0 else "\tchildren: [\n" + ",\n".join(children.map(func(child): return str(child))) + "]\n", 46 | "}"]) 47 | -------------------------------------------------------------------------------- /addons/dialogue_manager/compiler/tree_line.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dsu4i84dpif14 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/code_edit.gd.uid: -------------------------------------------------------------------------------- 1 | uid://djeybvlb332mp 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/code_edit.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://civ6shmka5e8u"] 2 | 3 | [ext_resource type="Script" uid="uid://klpiq4tk3t7a" path="res://addons/dialogue_manager/components/code_edit_syntax_highlighter.gd" id="1_58cfo"] 4 | [ext_resource type="Script" uid="uid://djeybvlb332mp" path="res://addons/dialogue_manager/components/code_edit.gd" id="1_g324i"] 5 | 6 | [sub_resource type="SyntaxHighlighter" id="SyntaxHighlighter_cobxx"] 7 | script = ExtResource("1_58cfo") 8 | 9 | [node name="CodeEdit" type="CodeEdit"] 10 | anchors_preset = 15 11 | anchor_right = 1.0 12 | anchor_bottom = 1.0 13 | grow_horizontal = 2 14 | grow_vertical = 2 15 | text = "~ title_thing 16 | 17 | if this = \"that\" or 'this' 18 | Nathan: Something 19 | - Then [if test.thing() == 2.0] => somewhere 20 | - Other => END! 21 | 22 | ~ somewhere 23 | 24 | set has_something = true 25 | => END" 26 | highlight_all_occurrences = true 27 | highlight_current_line = true 28 | draw_tabs = true 29 | syntax_highlighter = SubResource("SyntaxHighlighter_cobxx") 30 | scroll_past_end_of_file = true 31 | minimap_draw = true 32 | symbol_lookup_on_click = true 33 | line_folding = true 34 | gutters_draw_line_numbers = true 35 | gutters_draw_fold_gutter = true 36 | delimiter_strings = Array[String](["\" \""]) 37 | delimiter_comments = Array[String](["#"]) 38 | code_completion_enabled = true 39 | code_completion_prefixes = Array[String]([">", "<"]) 40 | indent_automatic = true 41 | auto_brace_completion_enabled = true 42 | auto_brace_completion_highlight_matching = true 43 | auto_brace_completion_pairs = { 44 | "\"": "\"", 45 | "(": ")", 46 | "[": "]", 47 | "{": "}" 48 | } 49 | script = ExtResource("1_g324i") 50 | 51 | [connection signal="caret_changed" from="." to="." method="_on_code_edit_caret_changed"] 52 | [connection signal="gutter_clicked" from="." to="." method="_on_code_edit_gutter_clicked"] 53 | [connection signal="symbol_lookup" from="." to="." method="_on_code_edit_symbol_lookup"] 54 | [connection signal="symbol_validate" from="." to="." method="_on_code_edit_symbol_validate"] 55 | [connection signal="text_changed" from="." to="." method="_on_code_edit_text_changed"] 56 | [connection signal="text_set" from="." to="." method="_on_code_edit_text_set"] 57 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/code_edit_syntax_highlighter.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name DMSyntaxHighlighter extends SyntaxHighlighter 3 | 4 | 5 | var regex: DMCompilerRegEx = DMCompilerRegEx.new() 6 | var compilation: DMCompilation = DMCompilation.new() 7 | var expression_parser = DMExpressionParser.new() 8 | 9 | var cache: Dictionary = {} 10 | 11 | 12 | func _clear_highlighting_cache() -> void: 13 | cache.clear() 14 | 15 | 16 | func _get_line_syntax_highlighting(line: int) -> Dictionary: 17 | expression_parser.include_comments = true 18 | 19 | var colors: Dictionary = {} 20 | var text_edit: TextEdit = get_text_edit() 21 | var text: String = text_edit.get_line(line) 22 | 23 | # Prevent an error from popping up while developing 24 | if not is_instance_valid(text_edit) or text_edit.theme_overrides.is_empty(): 25 | return colors 26 | 27 | # Disable this, as well as the line at the bottom of this function to remove the cache. 28 | if text in cache: 29 | return cache[text] 30 | 31 | var theme: Dictionary = text_edit.theme_overrides 32 | 33 | var index: int = 0 34 | 35 | match DMCompiler.get_line_type(text): 36 | DMConstants.TYPE_USING: 37 | colors[index] = { color = theme.conditions_color } 38 | colors[index + "using ".length()] = { color = theme.text_color } 39 | 40 | DMConstants.TYPE_IMPORT: 41 | colors[index] = { color = theme.conditions_color } 42 | var import: RegExMatch = regex.IMPORT_REGEX.search(text) 43 | if import: 44 | colors[index + import.get_start("path") - 1] = { color = theme.strings_color } 45 | colors[index + import.get_end("path") + 1] = { color = theme.conditions_color } 46 | colors[index + import.get_start("prefix")] = { color = theme.text_color } 47 | 48 | DMConstants.TYPE_COMMENT: 49 | colors[index] = { color = theme.comments_color } 50 | 51 | DMConstants.TYPE_TITLE: 52 | colors[index] = { color = theme.titles_color } 53 | 54 | DMConstants.TYPE_CONDITION, DMConstants.TYPE_WHILE, DMConstants.TYPE_MATCH, DMConstants.TYPE_WHEN: 55 | colors[0] = { color = theme.conditions_color } 56 | index = text.find(" ") 57 | if index > -1: 58 | var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_CONDITION, 0) 59 | if expression.size() == 0: 60 | colors[index] = { color = theme.critical_color } 61 | else: 62 | _highlight_expression(expression, colors, index) 63 | 64 | DMConstants.TYPE_MUTATION: 65 | colors[0] = { color = theme.mutations_color } 66 | index = text.find(" ") 67 | var expression: Array = expression_parser.tokenise(text.substr(index), DMConstants.TYPE_MUTATION, 0) 68 | if expression.size() == 0: 69 | colors[index] = { color = theme.critical_color } 70 | else: 71 | _highlight_expression(expression, colors, index) 72 | 73 | DMConstants.TYPE_GOTO: 74 | if text.strip_edges().begins_with("%"): 75 | colors[index] = { color = theme.symbols_color } 76 | index = text.find(" ") 77 | _highlight_goto(text, colors, index) 78 | 79 | DMConstants.TYPE_RANDOM: 80 | colors[index] = { color = theme.symbols_color } 81 | 82 | DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE: 83 | if text.strip_edges().begins_with("%"): 84 | colors[index] = { color = theme.symbols_color } 85 | index = text.find(" ", text.find("%")) 86 | colors[index] = { color = theme.text_color.lerp(theme.symbols_color, 0.5) } 87 | 88 | var dialogue_text: String = text.substr(index, text.find("=>")) 89 | 90 | # Highlight character name (but ignore ":" within line ID reference) 91 | var split_index: int = dialogue_text.replace("\\:", "??").find(":") 92 | if text.substr(split_index - 3, 3) != "[ID": 93 | colors[index + split_index + 1] = { color = theme.text_color } 94 | else: 95 | # If there's no character name then just highlight the text as dialogue. 96 | colors[index] = { color = theme.text_color } 97 | 98 | # Interpolation 99 | var replacements: Array[RegExMatch] = regex.REPLACEMENTS_REGEX.search_all(dialogue_text) 100 | for replacement: RegExMatch in replacements: 101 | var expression_text: String = replacement.get_string().substr(0, replacement.get_string().length() - 2).substr(2) 102 | var expression: Array = expression_parser.tokenise(expression_text, DMConstants.TYPE_MUTATION, replacement.get_start()) 103 | var expression_index: int = index + replacement.get_start() 104 | colors[expression_index] = { color = theme.symbols_color } 105 | if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: 106 | colors[expression_index] = { color = theme.critical_color } 107 | else: 108 | _highlight_expression(expression, colors, index + 2) 109 | colors[expression_index + expression_text.length() + 2] = { color = theme.symbols_color } 110 | colors[expression_index + expression_text.length() + 4] = { color = theme.text_color } 111 | # Tags (and inline mutations) 112 | var resolved_line_data: DMResolvedLineData = DMResolvedLineData.new("") 113 | var bbcodes: Array[Dictionary] = resolved_line_data.find_bbcode_positions_in_string(dialogue_text, true, true) 114 | for bbcode: Dictionary in bbcodes: 115 | var tag: String = bbcode.code 116 | var code: String = bbcode.raw_args 117 | if code.begins_with("["): 118 | colors[index + bbcode.start] = { color = theme.symbols_color } 119 | colors[index + bbcode.start + 2] = { color = theme.text_color } 120 | var pipe_cursor: int = code.find("|") 121 | while pipe_cursor > -1: 122 | colors[index + bbcode.start + pipe_cursor + 1] = { color = theme.symbols_color } 123 | colors[index + bbcode.start + pipe_cursor + 2] = { color = theme.text_color } 124 | pipe_cursor = code.find("|", pipe_cursor + 1) 125 | colors[index + bbcode.end - 1] = { color = theme.symbols_color } 126 | colors[index + bbcode.end + 1] = { color = theme.text_color } 127 | else: 128 | colors[index + bbcode.start] = { color = theme.symbols_color } 129 | if tag.begins_with("do") or tag.begins_with("set") or tag.begins_with("if"): 130 | if tag.begins_with("if"): 131 | colors[index + bbcode.start + 1] = { color = theme.conditions_color } 132 | else: 133 | colors[index + bbcode.start + 1] = { color = theme.mutations_color } 134 | var expression: Array = expression_parser.tokenise(code, DMConstants.TYPE_MUTATION, bbcode.start + bbcode.code.length()) 135 | if expression.size() == 0 or expression[0].type == DMConstants.TYPE_ERROR: 136 | colors[index + bbcode.start + tag.length() + 1] = { color = theme.critical_color } 137 | else: 138 | _highlight_expression(expression, colors, index + 2) 139 | # else and closing if have no expression 140 | elif tag.begins_with("else") or tag.begins_with("/if"): 141 | colors[index + bbcode.start + 1] = { color = theme.conditions_color } 142 | colors[index + bbcode.end] = { color = theme.symbols_color } 143 | colors[index + bbcode.end + 1] = { color = theme.text_color } 144 | # Jumps 145 | if "=> " in text or "=>< " in text: 146 | _highlight_goto(text, colors, index) 147 | 148 | # Order the dictionary keys to prevent CodeEdit from having issues 149 | var ordered_colors: Dictionary = {} 150 | var ordered_keys: Array = colors.keys() 151 | ordered_keys.sort() 152 | for key_index: int in ordered_keys: 153 | ordered_colors[key_index] = colors[key_index] 154 | 155 | cache[text] = ordered_colors 156 | return ordered_colors 157 | 158 | 159 | func _highlight_expression(tokens: Array, colors: Dictionary, index: int) -> int: 160 | var theme: Dictionary = get_text_edit().theme_overrides 161 | var last_index: int = index 162 | for token: Dictionary in tokens: 163 | last_index = token.i 164 | match token.type: 165 | DMConstants.TOKEN_COMMENT: 166 | colors[index + token.i] = { color = theme.comments_color } 167 | 168 | DMConstants.TOKEN_CONDITION, DMConstants.TOKEN_AND_OR: 169 | colors[index + token.i] = { color = theme.conditions_color } 170 | 171 | DMConstants.TOKEN_VARIABLE: 172 | if token.value in ["true", "false"]: 173 | colors[index + token.i] = { color = theme.conditions_color } 174 | else: 175 | colors[index + token.i] = { color = theme.members_color } 176 | 177 | DMConstants.TOKEN_OPERATOR, DMConstants.TOKEN_COLON, \ 178 | DMConstants.TOKEN_COMMA, DMConstants.TOKEN_DOT, DMConstants.TOKEN_NULL_COALESCE, \ 179 | DMConstants.TOKEN_NUMBER, DMConstants.TOKEN_ASSIGNMENT: 180 | colors[index + token.i] = { color = theme.symbols_color } 181 | 182 | DMConstants.TOKEN_STRING: 183 | colors[index + token.i] = { color = theme.strings_color } 184 | 185 | DMConstants.TOKEN_FUNCTION: 186 | colors[index + token.i] = { color = theme.mutations_color } 187 | colors[index + token.i + token.function.length()] = { color = theme.symbols_color } 188 | for parameter: Array in token.value: 189 | last_index = _highlight_expression(parameter, colors, index) 190 | DMConstants.TOKEN_PARENS_CLOSE: 191 | colors[index + token.i] = { color = theme.symbols_color } 192 | 193 | DMConstants.TOKEN_DICTIONARY_REFERENCE: 194 | colors[index + token.i] = { color = theme.members_color } 195 | colors[index + token.i + token.variable.length()] = { color = theme.symbols_color } 196 | last_index = _highlight_expression(token.value, colors, index) 197 | DMConstants.TOKEN_ARRAY: 198 | colors[index + token.i] = { color = theme.symbols_color } 199 | for item: Array in token.value: 200 | last_index = _highlight_expression(item, colors, index) 201 | DMConstants.TOKEN_BRACKET_CLOSE: 202 | colors[index + token.i] = { color = theme.symbols_color } 203 | 204 | DMConstants.TOKEN_DICTIONARY: 205 | colors[index + token.i] = { color = theme.symbols_color } 206 | last_index = _highlight_expression(token.value.keys() + token.value.values(), colors, index) 207 | DMConstants.TOKEN_BRACE_CLOSE: 208 | colors[index + token.i] = { color = theme.symbols_color } 209 | last_index += 1 210 | 211 | DMConstants.TOKEN_GROUP: 212 | last_index = _highlight_expression(token.value, colors, index) 213 | 214 | return last_index 215 | 216 | 217 | func _highlight_goto(text: String, colors: Dictionary, index: int) -> int: 218 | var theme: Dictionary = get_text_edit().theme_overrides 219 | var goto_data: DMResolvedGotoData = DMResolvedGotoData.new(text, {}) 220 | colors[goto_data.index] = { color = theme.jumps_color } 221 | if "{{" in text: 222 | index = text.find("{{", goto_data.index) 223 | var last_index: int = 0 224 | if goto_data.error: 225 | colors[index + 2] = { color = theme.critical_color } 226 | else: 227 | last_index = _highlight_expression(goto_data.expression, colors, index) 228 | index = text.find("}}", index + last_index) 229 | colors[index] = { color = theme.jumps_color } 230 | 231 | return index 232 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/code_edit_syntax_highlighter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://klpiq4tk3t7a 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/download_update_panel.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | 5 | signal failed() 6 | signal updated(updated_to_version: String) 7 | 8 | 9 | const DialogueConstants = preload("../constants.gd") 10 | 11 | const TEMP_FILE_NAME = "user://temp.zip" 12 | 13 | 14 | @onready var logo: TextureRect = %Logo 15 | @onready var label: Label = $VBox/Label 16 | @onready var http_request: HTTPRequest = $HTTPRequest 17 | @onready var download_button: Button = %DownloadButton 18 | 19 | var next_version_release: Dictionary: 20 | set(value): 21 | next_version_release = value 22 | label.text = DialogueConstants.translate(&"update.is_available_for_download") % value.tag_name.substr(1) 23 | get: 24 | return next_version_release 25 | 26 | 27 | func _ready() -> void: 28 | $VBox/Center/DownloadButton.text = DialogueConstants.translate(&"update.download_update") 29 | $VBox/Center2/NotesButton.text = DialogueConstants.translate(&"update.release_notes") 30 | 31 | 32 | ### Signals 33 | 34 | 35 | func _on_download_button_pressed() -> void: 36 | # Safeguard the actual dialogue manager repo from accidentally updating itself 37 | if FileAccess.file_exists("res://tests/test_basic_dialogue.gd"): 38 | prints("You can't update the addon from within itself.") 39 | failed.emit() 40 | return 41 | 42 | http_request.request(next_version_release.zipball_url) 43 | download_button.disabled = true 44 | download_button.text = DialogueConstants.translate(&"update.downloading") 45 | 46 | 47 | func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: 48 | if result != HTTPRequest.RESULT_SUCCESS: 49 | failed.emit() 50 | return 51 | 52 | # Save the downloaded zip 53 | var zip_file: FileAccess = FileAccess.open(TEMP_FILE_NAME, FileAccess.WRITE) 54 | zip_file.store_buffer(body) 55 | zip_file.close() 56 | 57 | OS.move_to_trash(ProjectSettings.globalize_path("res://addons/dialogue_manager")) 58 | 59 | var zip_reader: ZIPReader = ZIPReader.new() 60 | zip_reader.open(TEMP_FILE_NAME) 61 | var files: PackedStringArray = zip_reader.get_files() 62 | 63 | var base_path = files[1] 64 | # Remove archive folder 65 | files.remove_at(0) 66 | # Remove assets folder 67 | files.remove_at(0) 68 | 69 | for path in files: 70 | var new_file_path: String = path.replace(base_path, "") 71 | if path.ends_with("/"): 72 | DirAccess.make_dir_recursive_absolute("res://addons/%s" % new_file_path) 73 | else: 74 | var file: FileAccess = FileAccess.open("res://addons/%s" % new_file_path, FileAccess.WRITE) 75 | file.store_buffer(zip_reader.read_file(path)) 76 | 77 | zip_reader.close() 78 | DirAccess.remove_absolute(TEMP_FILE_NAME) 79 | 80 | updated.emit(next_version_release.tag_name.substr(1)) 81 | 82 | 83 | func _on_notes_button_pressed() -> void: 84 | OS.shell_open(next_version_release.html_url) 85 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/download_update_panel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://kpwo418lb2t2 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/download_update_panel.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://qdxrxv3c3hxk"] 2 | 3 | [ext_resource type="Script" uid="uid://kpwo418lb2t2" path="res://addons/dialogue_manager/components/download_update_panel.gd" id="1_4tm1k"] 4 | [ext_resource type="Texture2D" uid="uid://d3baj6rygkb3f" path="res://addons/dialogue_manager/assets/update.svg" id="2_4o2m6"] 5 | 6 | [node name="DownloadUpdatePanel" type="Control"] 7 | layout_mode = 3 8 | anchors_preset = 15 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | grow_horizontal = 2 12 | grow_vertical = 2 13 | script = ExtResource("1_4tm1k") 14 | 15 | [node name="HTTPRequest" type="HTTPRequest" parent="."] 16 | 17 | [node name="VBox" type="VBoxContainer" parent="."] 18 | layout_mode = 1 19 | anchors_preset = 15 20 | anchor_right = 1.0 21 | anchor_bottom = 1.0 22 | offset_left = -1.0 23 | offset_top = 9.0 24 | offset_right = -1.0 25 | offset_bottom = 9.0 26 | grow_horizontal = 2 27 | grow_vertical = 2 28 | theme_override_constants/separation = 10 29 | 30 | [node name="Logo" type="TextureRect" parent="VBox"] 31 | unique_name_in_owner = true 32 | clip_contents = true 33 | custom_minimum_size = Vector2(300, 80) 34 | layout_mode = 2 35 | texture = ExtResource("2_4o2m6") 36 | stretch_mode = 5 37 | 38 | [node name="Label" type="Label" parent="VBox"] 39 | layout_mode = 2 40 | text = "v1.2.3 is available for download." 41 | horizontal_alignment = 1 42 | 43 | [node name="Center" type="CenterContainer" parent="VBox"] 44 | layout_mode = 2 45 | 46 | [node name="DownloadButton" type="Button" parent="VBox/Center"] 47 | unique_name_in_owner = true 48 | layout_mode = 2 49 | text = "Download update" 50 | 51 | [node name="Center2" type="CenterContainer" parent="VBox"] 52 | layout_mode = 2 53 | 54 | [node name="NotesButton" type="LinkButton" parent="VBox/Center2"] 55 | layout_mode = 2 56 | text = "Read release notes" 57 | 58 | [connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"] 59 | [connection signal="pressed" from="VBox/Center/DownloadButton" to="." method="_on_download_button_pressed"] 60 | [connection signal="pressed" from="VBox/Center2/NotesButton" to="." method="_on_notes_button_pressed"] 61 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/editor_property.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorProperty 3 | 4 | 5 | const DialoguePropertyEditorControl = preload("./editor_property_control.tscn") 6 | 7 | 8 | var editor_plugin: EditorPlugin 9 | 10 | var control = DialoguePropertyEditorControl.instantiate() 11 | var current_value: Resource 12 | var is_updating: bool = false 13 | 14 | 15 | func _init() -> void: 16 | add_child(control) 17 | 18 | control.resource = current_value 19 | 20 | control.pressed.connect(_on_button_pressed) 21 | control.resource_changed.connect(_on_resource_changed) 22 | 23 | 24 | func _update_property() -> void: 25 | var next_value = get_edited_object()[get_edited_property()] 26 | 27 | # The resource might have been deleted elsewhere so check that it's not in a weird state 28 | if is_instance_valid(next_value) and not next_value.resource_path.ends_with(".dialogue"): 29 | emit_changed(get_edited_property(), null) 30 | return 31 | 32 | if next_value == current_value: return 33 | 34 | is_updating = true 35 | current_value = next_value 36 | control.resource = current_value 37 | is_updating = false 38 | 39 | 40 | ### Signals 41 | 42 | 43 | func _on_button_pressed() -> void: 44 | editor_plugin.edit(current_value) 45 | 46 | 47 | func _on_resource_changed(next_resource: Resource) -> void: 48 | emit_changed(get_edited_property(), next_resource) 49 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/editor_property.gd.uid: -------------------------------------------------------------------------------- 1 | uid://nyypeje1a036 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/editor_property_control.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | 4 | 5 | signal pressed() 6 | signal resource_changed(next_resource: Resource) 7 | 8 | 9 | const ITEM_NEW = 100 10 | const ITEM_QUICK_LOAD = 200 11 | const ITEM_LOAD = 201 12 | const ITEM_EDIT = 300 13 | const ITEM_CLEAR = 301 14 | const ITEM_FILESYSTEM = 400 15 | 16 | 17 | @onready var button: Button = $ResourceButton 18 | @onready var menu_button: Button = $MenuButton 19 | @onready var menu: PopupMenu = $Menu 20 | @onready var quick_open_dialog: ConfirmationDialog = $QuickOpenDialog 21 | @onready var files_list = $QuickOpenDialog/FilesList 22 | @onready var new_dialog: FileDialog = $NewDialog 23 | @onready var open_dialog: FileDialog = $OpenDialog 24 | 25 | var editor_plugin: EditorPlugin 26 | 27 | var resource: Resource: 28 | set(next_resource): 29 | resource = next_resource 30 | if button: 31 | button.resource = resource 32 | get: 33 | return resource 34 | 35 | var is_waiting_for_file: bool = false 36 | var quick_selected_file: String = "" 37 | 38 | 39 | func _ready() -> void: 40 | menu_button.icon = get_theme_icon("GuiDropdown", "EditorIcons") 41 | editor_plugin = Engine.get_meta("DialogueManagerPlugin") 42 | 43 | 44 | func build_menu() -> void: 45 | menu.clear() 46 | 47 | menu.add_icon_item(editor_plugin._get_plugin_icon(), "New Dialogue", ITEM_NEW) 48 | menu.add_separator() 49 | menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Quick Load", ITEM_QUICK_LOAD) 50 | menu.add_icon_item(get_theme_icon("Load", "EditorIcons"), "Load", ITEM_LOAD) 51 | if resource: 52 | menu.add_icon_item(get_theme_icon("Edit", "EditorIcons"), "Edit", ITEM_EDIT) 53 | menu.add_icon_item(get_theme_icon("Clear", "EditorIcons"), "Clear", ITEM_CLEAR) 54 | menu.add_separator() 55 | menu.add_item("Show in FileSystem", ITEM_FILESYSTEM) 56 | 57 | menu.size = Vector2.ZERO 58 | 59 | 60 | ### Signals 61 | 62 | 63 | func _on_new_dialog_file_selected(path: String) -> void: 64 | editor_plugin.main_view.new_file(path) 65 | is_waiting_for_file = false 66 | if Engine.get_meta("DMCache").has_file(path): 67 | resource_changed.emit(load(path)) 68 | else: 69 | var next_resource: Resource = await editor_plugin.import_plugin.compiled_resource 70 | next_resource.resource_path = path 71 | resource_changed.emit(next_resource) 72 | 73 | 74 | func _on_open_dialog_file_selected(file: String) -> void: 75 | resource_changed.emit(load(file)) 76 | 77 | 78 | func _on_file_dialog_canceled() -> void: 79 | is_waiting_for_file = false 80 | 81 | 82 | func _on_resource_button_pressed() -> void: 83 | if is_instance_valid(resource): 84 | EditorInterface.call_deferred("edit_resource", resource) 85 | else: 86 | build_menu() 87 | menu.position = get_viewport().position + Vector2i( 88 | button.global_position.x + button.size.x - menu.size.x, 89 | 2 + menu_button.global_position.y + button.size.y 90 | ) 91 | menu.popup() 92 | 93 | 94 | func _on_resource_button_resource_dropped(next_resource: Resource) -> void: 95 | resource_changed.emit(next_resource) 96 | 97 | 98 | func _on_menu_button_pressed() -> void: 99 | build_menu() 100 | menu.position = get_viewport().position + Vector2i( 101 | menu_button.global_position.x + menu_button.size.x - menu.size.x, 102 | 2 + menu_button.global_position.y + menu_button.size.y 103 | ) 104 | menu.popup() 105 | 106 | 107 | func _on_menu_id_pressed(id: int) -> void: 108 | match id: 109 | ITEM_NEW: 110 | is_waiting_for_file = true 111 | new_dialog.popup_centered() 112 | 113 | ITEM_QUICK_LOAD: 114 | quick_selected_file = "" 115 | files_list.files = Engine.get_meta("DMCache").get_files() 116 | if resource: 117 | files_list.select_file(resource.resource_path) 118 | quick_open_dialog.popup_centered() 119 | files_list.focus_filter() 120 | 121 | ITEM_LOAD: 122 | is_waiting_for_file = true 123 | open_dialog.popup_centered() 124 | 125 | ITEM_EDIT: 126 | EditorInterface.call_deferred("edit_resource", resource) 127 | 128 | ITEM_CLEAR: 129 | resource_changed.emit(null) 130 | 131 | ITEM_FILESYSTEM: 132 | var file_system = EditorInterface.get_file_system_dock() 133 | file_system.navigate_to_path(resource.resource_path) 134 | 135 | 136 | func _on_files_list_file_double_clicked(file_path: String) -> void: 137 | resource_changed.emit(load(file_path)) 138 | quick_open_dialog.hide() 139 | 140 | 141 | func _on_files_list_file_selected(file_path: String) -> void: 142 | quick_selected_file = file_path 143 | 144 | 145 | func _on_quick_open_dialog_confirmed() -> void: 146 | if quick_selected_file != "": 147 | resource_changed.emit(load(quick_selected_file)) 148 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/editor_property_control.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dooe2pflnqtve 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/editor_property_control.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://ycn6uaj7dsrh"] 2 | 3 | [ext_resource type="Script" uid="uid://dooe2pflnqtve" path="res://addons/dialogue_manager/components/editor_property/editor_property_control.gd" id="1_het12"] 4 | [ext_resource type="PackedScene" uid="uid://b16uuqjuof3n5" path="res://addons/dialogue_manager/components/editor_property/resource_button.tscn" id="2_hh3d4"] 5 | [ext_resource type="PackedScene" uid="uid://dnufpcdrreva3" path="res://addons/dialogue_manager/components/files_list.tscn" id="3_l8fp6"] 6 | 7 | [node name="PropertyEditorButton" type="HBoxContainer"] 8 | offset_right = 40.0 9 | offset_bottom = 40.0 10 | size_flags_horizontal = 3 11 | theme_override_constants/separation = 0 12 | script = ExtResource("1_het12") 13 | 14 | [node name="ResourceButton" parent="." instance=ExtResource("2_hh3d4")] 15 | layout_mode = 2 16 | text = "" 17 | text_overrun_behavior = 3 18 | clip_text = true 19 | 20 | [node name="MenuButton" type="Button" parent="."] 21 | layout_mode = 2 22 | 23 | [node name="Menu" type="PopupMenu" parent="."] 24 | 25 | [node name="QuickOpenDialog" type="ConfirmationDialog" parent="."] 26 | title = "Find Dialogue Resource" 27 | size = Vector2i(400, 600) 28 | min_size = Vector2i(400, 600) 29 | ok_button_text = "Open" 30 | 31 | [node name="FilesList" parent="QuickOpenDialog" instance=ExtResource("3_l8fp6")] 32 | 33 | [node name="NewDialog" type="FileDialog" parent="."] 34 | size = Vector2i(900, 750) 35 | min_size = Vector2i(900, 750) 36 | dialog_hide_on_ok = true 37 | filters = PackedStringArray("*.dialogue ; Dialogue") 38 | 39 | [node name="OpenDialog" type="FileDialog" parent="."] 40 | title = "Open a File" 41 | size = Vector2i(900, 750) 42 | min_size = Vector2i(900, 750) 43 | ok_button_text = "Open" 44 | dialog_hide_on_ok = true 45 | file_mode = 0 46 | filters = PackedStringArray("*.dialogue ; Dialogue") 47 | 48 | [connection signal="pressed" from="ResourceButton" to="." method="_on_resource_button_pressed"] 49 | [connection signal="resource_dropped" from="ResourceButton" to="." method="_on_resource_button_resource_dropped"] 50 | [connection signal="pressed" from="MenuButton" to="." method="_on_menu_button_pressed"] 51 | [connection signal="id_pressed" from="Menu" to="." method="_on_menu_id_pressed"] 52 | [connection signal="confirmed" from="QuickOpenDialog" to="." method="_on_quick_open_dialog_confirmed"] 53 | [connection signal="file_double_clicked" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_double_clicked"] 54 | [connection signal="file_selected" from="QuickOpenDialog/FilesList" to="." method="_on_files_list_file_selected"] 55 | [connection signal="canceled" from="NewDialog" to="." method="_on_file_dialog_canceled"] 56 | [connection signal="file_selected" from="NewDialog" to="." method="_on_new_dialog_file_selected"] 57 | [connection signal="canceled" from="OpenDialog" to="." method="_on_file_dialog_canceled"] 58 | [connection signal="file_selected" from="OpenDialog" to="." method="_on_open_dialog_file_selected"] 59 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/resource_button.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | 4 | 5 | signal resource_dropped(next_resource: Resource) 6 | 7 | 8 | var resource: Resource: 9 | set(next_resource): 10 | resource = next_resource 11 | if resource: 12 | icon = Engine.get_meta("DialogueManagerPlugin")._get_plugin_icon() 13 | text = resource.resource_path.get_file().replace(".dialogue", "") 14 | else: 15 | icon = null 16 | text = "" 17 | get: 18 | return resource 19 | 20 | 21 | func _notification(what: int) -> void: 22 | match what: 23 | NOTIFICATION_DRAG_BEGIN: 24 | var data = get_viewport().gui_get_drag_data() 25 | if typeof(data) == TYPE_DICTIONARY and data.type == "files" and data.files.size() > 0 and data.files[0].ends_with(".dialogue"): 26 | add_theme_stylebox_override("normal", get_theme_stylebox("focus", "LineEdit")) 27 | add_theme_stylebox_override("hover", get_theme_stylebox("focus", "LineEdit")) 28 | 29 | NOTIFICATION_DRAG_END: 30 | self.resource = resource 31 | remove_theme_stylebox_override("normal") 32 | remove_theme_stylebox_override("hover") 33 | 34 | 35 | func _can_drop_data(at_position: Vector2, data) -> bool: 36 | if typeof(data) != TYPE_DICTIONARY: return false 37 | if data.type != "files": return false 38 | 39 | var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") 40 | return files.size() > 0 41 | 42 | 43 | func _drop_data(at_position: Vector2, data) -> void: 44 | var files: PackedStringArray = Array(data.files).filter(func(f): return f.get_extension() == "dialogue") 45 | 46 | if files.size() == 0: return 47 | 48 | resource_dropped.emit(load(files[0])) 49 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/resource_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://damhqta55t67c 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/editor_property/resource_button.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://b16uuqjuof3n5"] 2 | 3 | [ext_resource type="Script" uid="uid://damhqta55t67c" path="res://addons/dialogue_manager/components/editor_property/resource_button.gd" id="1_7u2i7"] 4 | 5 | [node name="ResourceButton" type="Button"] 6 | offset_right = 8.0 7 | offset_bottom = 8.0 8 | size_flags_horizontal = 3 9 | script = ExtResource("1_7u2i7") 10 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/errors_panel.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | 4 | 5 | signal error_pressed(line_number) 6 | 7 | 8 | const DialogueConstants = preload("../constants.gd") 9 | 10 | 11 | @onready var error_button: Button = $ErrorButton 12 | @onready var next_button: Button = $NextButton 13 | @onready var count_label: Label = $CountLabel 14 | @onready var previous_button: Button = $PreviousButton 15 | 16 | ## The index of the current error being shown 17 | var error_index: int = 0: 18 | set(next_error_index): 19 | error_index = wrap(next_error_index, 0, errors.size()) 20 | show_error() 21 | get: 22 | return error_index 23 | 24 | ## The list of all errors 25 | var errors: Array = []: 26 | set(next_errors): 27 | errors = next_errors 28 | self.error_index = 0 29 | get: 30 | return errors 31 | 32 | 33 | func _ready() -> void: 34 | apply_theme() 35 | hide() 36 | 37 | 38 | ## Set up colors and icons 39 | func apply_theme() -> void: 40 | error_button.add_theme_color_override("font_color", get_theme_color("error_color", "Editor")) 41 | error_button.add_theme_color_override("font_hover_color", get_theme_color("error_color", "Editor")) 42 | error_button.icon = get_theme_icon("StatusError", "EditorIcons") 43 | previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons") 44 | next_button.icon = get_theme_icon("ArrowRight", "EditorIcons") 45 | 46 | 47 | ## Move the error index to match a given line 48 | func show_error_for_line_number(line_number: int) -> void: 49 | for i in range(0, errors.size()): 50 | if errors[i].line_number == line_number: 51 | self.error_index = i 52 | 53 | 54 | ## Show the current error 55 | func show_error() -> void: 56 | if errors.size() == 0: 57 | hide() 58 | else: 59 | show() 60 | count_label.text = DialogueConstants.translate(&"n_of_n").format({ index = error_index + 1, total = errors.size() }) 61 | var error = errors[error_index] 62 | error_button.text = DialogueConstants.translate(&"errors.line_and_message").format({ line = error.line_number, column = error.column_number, message = DialogueConstants.get_error_message(error.error) }) 63 | if error.has("external_error"): 64 | error_button.text += " " + DialogueConstants.get_error_message(error.external_error) 65 | 66 | 67 | ### Signals 68 | 69 | 70 | func _on_errors_panel_theme_changed() -> void: 71 | apply_theme() 72 | 73 | 74 | func _on_error_button_pressed() -> void: 75 | error_pressed.emit(errors[error_index].line_number, errors[error_index].column_number) 76 | 77 | 78 | func _on_previous_button_pressed() -> void: 79 | self.error_index -= 1 80 | _on_error_button_pressed() 81 | 82 | 83 | func _on_next_button_pressed() -> void: 84 | self.error_index += 1 85 | _on_error_button_pressed() 86 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/errors_panel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d2l8nlb6hhrfp 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/errors_panel.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://cs8pwrxr5vxix"] 2 | 3 | [ext_resource type="Script" uid="uid://d2l8nlb6hhrfp" path="res://addons/dialogue_manager/components/errors_panel.gd" id="1_nfm3c"] 4 | 5 | [sub_resource type="Image" id="Image_w0gko"] 6 | data = { 7 | "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), 8 | "format": "RGBA8", 9 | "height": 16, 10 | "mipmaps": false, 11 | "width": 16 12 | } 13 | 14 | [sub_resource type="ImageTexture" id="ImageTexture_s6fxl"] 15 | image = SubResource("Image_w0gko") 16 | 17 | [node name="ErrorsPanel" type="HBoxContainer"] 18 | visible = false 19 | offset_right = 1024.0 20 | offset_bottom = 600.0 21 | grow_horizontal = 2 22 | grow_vertical = 2 23 | script = ExtResource("1_nfm3c") 24 | metadata/_edit_layout_mode = 1 25 | 26 | [node name="ErrorButton" type="Button" parent="."] 27 | layout_mode = 2 28 | size_flags_horizontal = 3 29 | theme_override_colors/font_color = Color(0, 0, 0, 1) 30 | theme_override_colors/font_hover_color = Color(0, 0, 0, 1) 31 | theme_override_constants/h_separation = 3 32 | icon = SubResource("ImageTexture_s6fxl") 33 | flat = true 34 | alignment = 0 35 | text_overrun_behavior = 4 36 | 37 | [node name="Spacer" type="Control" parent="."] 38 | custom_minimum_size = Vector2(40, 0) 39 | layout_mode = 2 40 | 41 | [node name="PreviousButton" type="Button" parent="."] 42 | layout_mode = 2 43 | icon = SubResource("ImageTexture_s6fxl") 44 | flat = true 45 | 46 | [node name="CountLabel" type="Label" parent="."] 47 | layout_mode = 2 48 | 49 | [node name="NextButton" type="Button" parent="."] 50 | layout_mode = 2 51 | icon = SubResource("ImageTexture_s6fxl") 52 | flat = true 53 | 54 | [connection signal="pressed" from="ErrorButton" to="." method="_on_error_button_pressed"] 55 | [connection signal="pressed" from="PreviousButton" to="." method="_on_previous_button_pressed"] 56 | [connection signal="pressed" from="NextButton" to="." method="_on_next_button_pressed"] 57 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/files_list.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends VBoxContainer 3 | 4 | 5 | signal file_selected(file_path: String) 6 | signal file_popup_menu_requested(at_position: Vector2) 7 | signal file_double_clicked(file_path: String) 8 | signal file_middle_clicked(file_path: String) 9 | 10 | 11 | const DialogueConstants = preload("../constants.gd") 12 | 13 | const MODIFIED_SUFFIX = "(*)" 14 | 15 | 16 | @export var icon: Texture2D 17 | 18 | @onready var filter_edit: LineEdit = $FilterEdit 19 | @onready var list: ItemList = $List 20 | 21 | var file_map: Dictionary = {} 22 | 23 | var current_file_path: String = "" 24 | var last_selected_file_path: String = "" 25 | 26 | var files: PackedStringArray = []: 27 | set(next_files): 28 | files = next_files 29 | files.sort() 30 | update_file_map() 31 | apply_filter() 32 | get: 33 | return files 34 | 35 | var unsaved_files: Array[String] = [] 36 | 37 | var filter: String = "": 38 | set(next_filter): 39 | filter = next_filter 40 | apply_filter() 41 | get: 42 | return filter 43 | 44 | 45 | func _ready() -> void: 46 | apply_theme() 47 | 48 | filter_edit.placeholder_text = DialogueConstants.translate(&"files_list.filter") 49 | 50 | 51 | func focus_filter() -> void: 52 | filter_edit.grab_focus() 53 | 54 | 55 | func select_file(file: String) -> void: 56 | list.deselect_all() 57 | for i in range(0, list.get_item_count()): 58 | var item_text = list.get_item_text(i).replace(MODIFIED_SUFFIX, "") 59 | if item_text == get_nice_file(file, item_text.count("/") + 1): 60 | list.select(i) 61 | last_selected_file_path = file 62 | 63 | 64 | func mark_file_as_unsaved(file: String, is_unsaved: bool) -> void: 65 | if not file in unsaved_files and is_unsaved: 66 | unsaved_files.append(file) 67 | elif file in unsaved_files and not is_unsaved: 68 | unsaved_files.erase(file) 69 | apply_filter() 70 | 71 | 72 | func update_file_map() -> void: 73 | file_map = {} 74 | for file in files: 75 | var nice_file: String = get_nice_file(file) 76 | 77 | # See if a value with just the file name is already in the map 78 | for key in file_map.keys(): 79 | if file_map[key] == nice_file: 80 | var bit_count = nice_file.count("/") + 2 81 | 82 | var existing_nice_file = get_nice_file(key, bit_count) 83 | nice_file = get_nice_file(file, bit_count) 84 | 85 | while nice_file == existing_nice_file: 86 | bit_count += 1 87 | existing_nice_file = get_nice_file(key, bit_count) 88 | nice_file = get_nice_file(file, bit_count) 89 | 90 | file_map[key] = existing_nice_file 91 | 92 | file_map[file] = nice_file 93 | 94 | 95 | func get_nice_file(file_path: String, path_bit_count: int = 1) -> String: 96 | var bits = file_path.replace("res://", "").replace(".dialogue", "").split("/") 97 | bits = bits.slice(-path_bit_count) 98 | return "/".join(bits) 99 | 100 | 101 | func apply_filter() -> void: 102 | list.clear() 103 | for file in file_map.keys(): 104 | if filter == "" or filter.to_lower() in file.to_lower(): 105 | var nice_file = file_map[file] 106 | if file in unsaved_files: 107 | nice_file += MODIFIED_SUFFIX 108 | var new_id := list.add_item(nice_file) 109 | list.set_item_icon(new_id, icon) 110 | 111 | select_file(current_file_path) 112 | 113 | 114 | func apply_theme() -> void: 115 | if is_instance_valid(filter_edit): 116 | filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") 117 | if is_instance_valid(list): 118 | list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) 119 | 120 | 121 | ### Signals 122 | 123 | 124 | func _on_theme_changed() -> void: 125 | apply_theme() 126 | 127 | 128 | func _on_filter_edit_text_changed(new_text: String) -> void: 129 | self.filter = new_text 130 | 131 | 132 | func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void: 133 | var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "") 134 | var file = file_map.find_key(item_text) 135 | 136 | if mouse_button_index == MOUSE_BUTTON_LEFT or mouse_button_index == MOUSE_BUTTON_RIGHT: 137 | select_file(file) 138 | file_selected.emit(file) 139 | if mouse_button_index == MOUSE_BUTTON_RIGHT: 140 | file_popup_menu_requested.emit(at_position) 141 | 142 | if mouse_button_index == MOUSE_BUTTON_MIDDLE: 143 | file_middle_clicked.emit(file) 144 | 145 | 146 | func _on_list_item_activated(index: int) -> void: 147 | var item_text = list.get_item_text(index).replace(MODIFIED_SUFFIX, "") 148 | var file = file_map.find_key(item_text) 149 | select_file(file) 150 | file_double_clicked.emit(file) 151 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/files_list.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dqa4a4wwoo0aa 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/files_list.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://dnufpcdrreva3"] 2 | 3 | [ext_resource type="Script" uid="uid://dqa4a4wwoo0aa" path="res://addons/dialogue_manager/components/files_list.gd" id="1_cytii"] 4 | [ext_resource type="Texture2D" uid="uid://d3lr2uas6ax8v" path="res://addons/dialogue_manager/assets/icon.svg" id="2_3ijx1"] 5 | 6 | [node name="FilesList" type="VBoxContainer"] 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | size_flags_vertical = 3 13 | script = ExtResource("1_cytii") 14 | icon = ExtResource("2_3ijx1") 15 | 16 | [node name="FilterEdit" type="LineEdit" parent="."] 17 | layout_mode = 2 18 | placeholder_text = "Filter files" 19 | clear_button_enabled = true 20 | 21 | [node name="List" type="ItemList" parent="."] 22 | layout_mode = 2 23 | size_flags_vertical = 3 24 | allow_rmb_select = true 25 | 26 | [connection signal="theme_changed" from="." to="." method="_on_theme_changed"] 27 | [connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"] 28 | [connection signal="item_activated" from="List" to="." method="_on_list_item_activated"] 29 | [connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"] 30 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/find_in_files.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | signal result_selected(path: String, cursor: Vector2, length: int) 5 | 6 | 7 | const DialogueConstants = preload("../constants.gd") 8 | 9 | 10 | @export var main_view: Control 11 | @export var code_edit: CodeEdit 12 | 13 | @onready var input: LineEdit = %Input 14 | @onready var search_button: Button = %SearchButton 15 | @onready var match_case_button: CheckBox = %MatchCaseButton 16 | @onready var replace_toggle: CheckButton = %ReplaceToggle 17 | @onready var replace_container: VBoxContainer = %ReplaceContainer 18 | @onready var replace_input: LineEdit = %ReplaceInput 19 | @onready var replace_selected_button: Button = %ReplaceSelectedButton 20 | @onready var replace_all_button: Button = %ReplaceAllButton 21 | @onready var results_container: VBoxContainer = %ResultsContainer 22 | @onready var result_template: HBoxContainer = %ResultTemplate 23 | 24 | var current_results: Dictionary = {}: 25 | set(value): 26 | current_results = value 27 | update_results_view() 28 | if current_results.size() == 0: 29 | replace_selected_button.disabled = true 30 | replace_all_button.disabled = true 31 | else: 32 | replace_selected_button.disabled = false 33 | replace_all_button.disabled = false 34 | get: 35 | return current_results 36 | 37 | var selections: PackedStringArray = [] 38 | 39 | 40 | func prepare() -> void: 41 | input.grab_focus() 42 | 43 | var template_label = result_template.get_node("Label") 44 | template_label.get_theme_stylebox(&"focus").bg_color = code_edit.theme_overrides.current_line_color 45 | template_label.add_theme_font_override(&"normal_font", code_edit.get_theme_font(&"font")) 46 | 47 | replace_toggle.set_pressed_no_signal(false) 48 | replace_container.hide() 49 | 50 | $VBoxContainer/HBoxContainer/FindContainer/Label.text = DialogueConstants.translate(&"search.find") 51 | input.placeholder_text = DialogueConstants.translate(&"search.placeholder") 52 | input.text = "" 53 | search_button.text = DialogueConstants.translate(&"search.find_all") 54 | match_case_button.text = DialogueConstants.translate(&"search.match_case") 55 | replace_toggle.text = DialogueConstants.translate(&"search.toggle_replace") 56 | $VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with") 57 | replace_input.placeholder_text = DialogueConstants.translate(&"search.replace_placeholder") 58 | replace_input.text = "" 59 | replace_all_button.text = DialogueConstants.translate(&"search.replace_all") 60 | replace_selected_button.text = DialogueConstants.translate(&"search.replace_selected") 61 | 62 | selections.clear() 63 | self.current_results = {} 64 | 65 | #region helpers 66 | 67 | 68 | func update_results_view() -> void: 69 | for child in results_container.get_children(): 70 | child.queue_free() 71 | 72 | for path in current_results.keys(): 73 | var path_label: Label = Label.new() 74 | path_label.text = path 75 | # Show open files 76 | if main_view.open_buffers.has(path): 77 | path_label.text += "(*)" 78 | results_container.add_child(path_label) 79 | for path_result in current_results.get(path): 80 | var result_item: HBoxContainer = result_template.duplicate() 81 | 82 | var checkbox: CheckBox = result_item.get_node("CheckBox") as CheckBox 83 | var key: String = get_selection_key(path, path_result) 84 | checkbox.toggled.connect(func(is_pressed): 85 | if is_pressed: 86 | if not selections.has(key): 87 | selections.append(key) 88 | else: 89 | if selections.has(key): 90 | selections.remove_at(selections.find(key)) 91 | ) 92 | checkbox.set_pressed_no_signal(selections.has(key)) 93 | checkbox.visible = replace_toggle.button_pressed 94 | 95 | var result_label: RichTextLabel = result_item.get_node("Label") as RichTextLabel 96 | var colors: Dictionary = code_edit.theme_overrides 97 | var highlight: String = "" 98 | if replace_toggle.button_pressed: 99 | var matched_word: String = "[bgcolor=" + colors.critical_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" 100 | highlight = "[s]" + matched_word + "[/s][bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + replace_input.text + "[/color][/bgcolor]" 101 | else: 102 | highlight = "[bgcolor=" + colors.notice_color.to_html() + "][color=" + colors.text_color.to_html() + "]" + path_result.matched_text + "[/color][/bgcolor]" 103 | var text: String = path_result.text.substr(0, path_result.index) + highlight + path_result.text.substr(path_result.index + path_result.query.length()) 104 | result_label.text = "%s: %s" % [str(path_result.line).lpad(4), text] 105 | result_label.gui_input.connect(func(event): 106 | if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT and (event as InputEventMouseButton).double_click: 107 | result_selected.emit(path, Vector2(path_result.index, path_result.line), path_result.query.length()) 108 | ) 109 | 110 | results_container.add_child(result_item) 111 | 112 | 113 | func find_in_files() -> Dictionary: 114 | var results: Dictionary = {} 115 | 116 | var q: String = input.text 117 | var cache = Engine.get_meta("DMCache") 118 | var file: FileAccess 119 | for path in cache.get_files(): 120 | var path_results: Array = [] 121 | var lines: PackedStringArray = [] 122 | 123 | if main_view.open_buffers.has(path): 124 | lines = main_view.open_buffers.get(path).text.split("\n") 125 | else: 126 | file = FileAccess.open(path, FileAccess.READ) 127 | lines = file.get_as_text().split("\n") 128 | 129 | for i in range(0, lines.size()): 130 | var index: int = find_in_line(lines[i], q) 131 | while index > -1: 132 | path_results.append({ 133 | line = i, 134 | index = index, 135 | text = lines[i], 136 | matched_text = lines[i].substr(index, q.length()), 137 | query = q 138 | }) 139 | index = find_in_line(lines[i], q, index + q.length()) 140 | 141 | if file != null and file.is_open(): 142 | file.close() 143 | 144 | if path_results.size() > 0: 145 | results[path] = path_results 146 | 147 | return results 148 | 149 | 150 | func get_selection_key(path: String, path_result: Dictionary) -> String: 151 | return "%s-%d-%d" % [path, path_result.line, path_result.index] 152 | 153 | 154 | func find_in_line(line: String, query: String, from_index: int = 0) -> int: 155 | if match_case_button.button_pressed: 156 | return line.find(query, from_index) 157 | else: 158 | return line.findn(query, from_index) 159 | 160 | 161 | func replace_results(only_selected: bool) -> void: 162 | var file: FileAccess 163 | var lines: PackedStringArray = [] 164 | for path in current_results: 165 | if main_view.open_buffers.has(path): 166 | lines = main_view.open_buffers.get(path).text.split("\n") 167 | else: 168 | file = FileAccess.open(path, FileAccess.READ_WRITE) 169 | lines = file.get_as_text().split("\n") 170 | 171 | # Read the results in reverse because we're going to be modifying them as we go 172 | var path_results: Array = current_results.get(path).duplicate() 173 | path_results.reverse() 174 | for path_result in path_results: 175 | var key: String = get_selection_key(path, path_result) 176 | if not only_selected or (only_selected and selections.has(key)): 177 | lines[path_result.line] = lines[path_result.line].substr(0, path_result.index) + replace_input.text + lines[path_result.line].substr(path_result.index + path_result.matched_text.length()) 178 | 179 | var replaced_text: String = "\n".join(lines) 180 | if file != null and file.is_open(): 181 | file.seek(0) 182 | file.store_string(replaced_text) 183 | file.close() 184 | else: 185 | main_view.open_buffers.get(path).text = replaced_text 186 | if main_view.current_file_path == path: 187 | code_edit.text = replaced_text 188 | 189 | current_results = find_in_files() 190 | 191 | 192 | #endregion 193 | 194 | #region signals 195 | 196 | 197 | func _on_search_button_pressed() -> void: 198 | selections.clear() 199 | self.current_results = find_in_files() 200 | 201 | 202 | func _on_input_text_submitted(new_text: String) -> void: 203 | _on_search_button_pressed() 204 | 205 | 206 | func _on_replace_toggle_toggled(toggled_on: bool) -> void: 207 | replace_container.visible = toggled_on 208 | if toggled_on: 209 | replace_input.grab_focus() 210 | update_results_view() 211 | 212 | 213 | func _on_replace_input_text_changed(new_text: String) -> void: 214 | update_results_view() 215 | 216 | 217 | func _on_replace_selected_button_pressed() -> void: 218 | replace_results(true) 219 | 220 | 221 | func _on_replace_all_button_pressed() -> void: 222 | replace_results(false) 223 | 224 | 225 | func _on_match_case_button_toggled(toggled_on: bool) -> void: 226 | _on_search_button_pressed() 227 | 228 | 229 | #endregion 230 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/find_in_files.gd.uid: -------------------------------------------------------------------------------- 1 | uid://q368fmxxa8sd 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/find_in_files.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://0n7hwviyyly4"] 2 | 3 | [ext_resource type="Script" uid="uid://q368fmxxa8sd" path="res://addons/dialogue_manager/components/find_in_files.gd" id="1_3xicy"] 4 | 5 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owohg"] 6 | bg_color = Color(0.266667, 0.278431, 0.352941, 0.243137) 7 | corner_detail = 1 8 | 9 | [node name="FindInFiles" type="Control"] 10 | layout_mode = 3 11 | anchors_preset = 15 12 | anchor_right = 1.0 13 | anchor_bottom = 1.0 14 | grow_horizontal = 2 15 | grow_vertical = 2 16 | size_flags_horizontal = 3 17 | size_flags_vertical = 3 18 | script = ExtResource("1_3xicy") 19 | 20 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 21 | layout_mode = 1 22 | anchors_preset = 15 23 | anchor_right = 1.0 24 | anchor_bottom = 1.0 25 | grow_horizontal = 2 26 | grow_vertical = 2 27 | 28 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] 29 | layout_mode = 2 30 | 31 | [node name="FindContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"] 32 | layout_mode = 2 33 | size_flags_horizontal = 3 34 | 35 | [node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/FindContainer"] 36 | layout_mode = 2 37 | text = "Find:" 38 | 39 | [node name="Input" type="LineEdit" parent="VBoxContainer/HBoxContainer/FindContainer"] 40 | unique_name_in_owner = true 41 | layout_mode = 2 42 | clear_button_enabled = true 43 | 44 | [node name="FindToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/FindContainer"] 45 | layout_mode = 2 46 | 47 | [node name="SearchButton" type="Button" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] 48 | unique_name_in_owner = true 49 | layout_mode = 2 50 | text = "Find all..." 51 | 52 | [node name="MatchCaseButton" type="CheckBox" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] 53 | unique_name_in_owner = true 54 | layout_mode = 2 55 | text = "Match case" 56 | 57 | [node name="Control" type="Control" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] 58 | layout_mode = 2 59 | size_flags_horizontal = 3 60 | 61 | [node name="ReplaceToggle" type="CheckButton" parent="VBoxContainer/HBoxContainer/FindContainer/FindToolbar"] 62 | unique_name_in_owner = true 63 | layout_mode = 2 64 | text = "Replace" 65 | 66 | [node name="ReplaceContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer"] 67 | unique_name_in_owner = true 68 | layout_mode = 2 69 | size_flags_horizontal = 3 70 | 71 | [node name="ReplaceLabel" type="Label" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] 72 | layout_mode = 2 73 | text = "Replace with:" 74 | 75 | [node name="ReplaceInput" type="LineEdit" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] 76 | unique_name_in_owner = true 77 | layout_mode = 2 78 | size_flags_horizontal = 3 79 | clear_button_enabled = true 80 | 81 | [node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/ReplaceContainer"] 82 | layout_mode = 2 83 | 84 | [node name="ReplaceSelectedButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"] 85 | unique_name_in_owner = true 86 | layout_mode = 2 87 | text = "Replace selected" 88 | 89 | [node name="ReplaceAllButton" type="Button" parent="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar"] 90 | unique_name_in_owner = true 91 | layout_mode = 2 92 | text = "Replace all" 93 | 94 | [node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"] 95 | layout_mode = 2 96 | 97 | [node name="ReplaceToolbar" type="HBoxContainer" parent="VBoxContainer/VBoxContainer"] 98 | layout_mode = 2 99 | 100 | [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] 101 | layout_mode = 2 102 | size_flags_vertical = 3 103 | follow_focus = true 104 | 105 | [node name="ResultsContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] 106 | unique_name_in_owner = true 107 | layout_mode = 2 108 | size_flags_horizontal = 3 109 | size_flags_vertical = 3 110 | theme_override_constants/separation = 0 111 | 112 | [node name="ResultTemplate" type="HBoxContainer" parent="."] 113 | unique_name_in_owner = true 114 | layout_mode = 0 115 | offset_left = 155.0 116 | offset_top = -74.0 117 | offset_right = 838.0 118 | offset_bottom = -51.0 119 | 120 | [node name="CheckBox" type="CheckBox" parent="ResultTemplate"] 121 | layout_mode = 2 122 | 123 | [node name="Label" type="RichTextLabel" parent="ResultTemplate"] 124 | layout_mode = 2 125 | size_flags_horizontal = 3 126 | focus_mode = 2 127 | theme_override_styles/focus = SubResource("StyleBoxFlat_owohg") 128 | bbcode_enabled = true 129 | text = "Result" 130 | fit_content = true 131 | scroll_active = false 132 | 133 | [connection signal="text_submitted" from="VBoxContainer/HBoxContainer/FindContainer/Input" to="." method="_on_input_text_submitted"] 134 | [connection signal="pressed" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/SearchButton" to="." method="_on_search_button_pressed"] 135 | [connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/MatchCaseButton" to="." method="_on_match_case_button_toggled"] 136 | [connection signal="toggled" from="VBoxContainer/HBoxContainer/FindContainer/FindToolbar/ReplaceToggle" to="." method="_on_replace_toggle_toggled"] 137 | [connection signal="text_changed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceInput" to="." method="_on_replace_input_text_changed"] 138 | [connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceSelectedButton" to="." method="_on_replace_selected_button_pressed"] 139 | [connection signal="pressed" from="VBoxContainer/HBoxContainer/ReplaceContainer/ReplaceToolbar/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"] 140 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/search_and_replace.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends VBoxContainer 3 | 4 | 5 | signal open_requested() 6 | signal close_requested() 7 | 8 | 9 | const DialogueConstants = preload("../constants.gd") 10 | 11 | 12 | @onready var input: LineEdit = $Search/Input 13 | @onready var result_label: Label = $Search/ResultLabel 14 | @onready var previous_button: Button = $Search/PreviousButton 15 | @onready var next_button: Button = $Search/NextButton 16 | @onready var match_case_button: CheckBox = $Search/MatchCaseCheckBox 17 | @onready var replace_check_button: CheckButton = $Search/ReplaceCheckButton 18 | @onready var replace_panel: HBoxContainer = $Replace 19 | @onready var replace_input: LineEdit = $Replace/Input 20 | @onready var replace_button: Button = $Replace/ReplaceButton 21 | @onready var replace_all_button: Button = $Replace/ReplaceAllButton 22 | 23 | # The code edit we will be affecting (for some reason exporting this didn't work) 24 | var code_edit: CodeEdit: 25 | set(next_code_edit): 26 | code_edit = next_code_edit 27 | code_edit.gui_input.connect(_on_text_edit_gui_input) 28 | code_edit.text_changed.connect(_on_text_edit_text_changed) 29 | get: 30 | return code_edit 31 | 32 | var results: Array = [] 33 | var result_index: int = -1: 34 | set(next_result_index): 35 | result_index = next_result_index 36 | if results.size() > 0: 37 | var r = results[result_index] 38 | code_edit.set_caret_line(r[0]) 39 | code_edit.select(r[0], r[1], r[0], r[1] + r[2]) 40 | else: 41 | result_index = -1 42 | if is_instance_valid(code_edit): 43 | code_edit.deselect() 44 | 45 | result_label.text = DialogueConstants.translate(&"n_of_n").format({ index = result_index + 1, total = results.size() }) 46 | get: 47 | return result_index 48 | 49 | 50 | func _ready() -> void: 51 | apply_theme() 52 | 53 | input.placeholder_text = DialogueConstants.translate(&"search.placeholder") 54 | previous_button.tooltip_text = DialogueConstants.translate(&"search.previous") 55 | next_button.tooltip_text = DialogueConstants.translate(&"search.next") 56 | match_case_button.text = DialogueConstants.translate(&"search.match_case") 57 | $Search/ReplaceCheckButton.text = DialogueConstants.translate(&"search.toggle_replace") 58 | replace_button.text = DialogueConstants.translate(&"search.replace") 59 | replace_all_button.text = DialogueConstants.translate(&"search.replace_all") 60 | $Replace/ReplaceLabel.text = DialogueConstants.translate(&"search.replace_with") 61 | 62 | self.result_index = -1 63 | 64 | replace_panel.hide() 65 | replace_button.disabled = true 66 | replace_all_button.disabled = true 67 | 68 | hide() 69 | 70 | 71 | func focus_line_edit() -> void: 72 | input.grab_focus() 73 | input.select_all() 74 | 75 | 76 | func apply_theme() -> void: 77 | if is_instance_valid(previous_button): 78 | previous_button.icon = get_theme_icon("ArrowLeft", "EditorIcons") 79 | if is_instance_valid(next_button): 80 | next_button.icon = get_theme_icon("ArrowRight", "EditorIcons") 81 | 82 | 83 | # Find text in the code 84 | func search(text: String = "", default_result_index: int = 0) -> void: 85 | results.clear() 86 | 87 | if text == "": 88 | text = input.text 89 | 90 | var lines = code_edit.text.split("\n") 91 | for line_number in range(0, lines.size()): 92 | var line = lines[line_number] 93 | 94 | var column = find_in_line(line, text, 0) 95 | while column > -1: 96 | results.append([line_number, column, text.length()]) 97 | column = find_in_line(line, text, column + 1) 98 | 99 | if results.size() > 0: 100 | replace_button.disabled = false 101 | replace_all_button.disabled = false 102 | else: 103 | replace_button.disabled = true 104 | replace_all_button.disabled = true 105 | 106 | self.result_index = clamp(default_result_index, 0, results.size() - 1) 107 | 108 | 109 | # Find text in a string and match case if requested 110 | func find_in_line(line: String, text: String, from_index: int = 0) -> int: 111 | if match_case_button.button_pressed: 112 | return line.find(text, from_index) 113 | else: 114 | return line.findn(text, from_index) 115 | 116 | 117 | #region Signals 118 | 119 | 120 | func _on_text_edit_gui_input(event: InputEvent) -> void: 121 | if event is InputEventKey and event.is_pressed(): 122 | match event.as_text(): 123 | "Ctrl+F", "Command+F": 124 | open_requested.emit() 125 | get_viewport().set_input_as_handled() 126 | "Ctrl+Shift+R", "Command+Shift+R": 127 | replace_check_button.set_pressed(true) 128 | open_requested.emit() 129 | get_viewport().set_input_as_handled() 130 | 131 | 132 | func _on_text_edit_text_changed() -> void: 133 | results.clear() 134 | 135 | 136 | func _on_search_and_replace_theme_changed() -> void: 137 | apply_theme() 138 | 139 | 140 | func _on_input_text_changed(new_text: String) -> void: 141 | search(new_text) 142 | 143 | 144 | func _on_previous_button_pressed() -> void: 145 | self.result_index = wrapi(result_index - 1, 0, results.size()) 146 | 147 | 148 | func _on_next_button_pressed() -> void: 149 | self.result_index = wrapi(result_index + 1, 0, results.size()) 150 | 151 | 152 | func _on_search_and_replace_visibility_changed() -> void: 153 | if is_instance_valid(input): 154 | if visible: 155 | input.grab_focus() 156 | var selection = code_edit.get_selected_text() 157 | if input.text == "" and selection != "": 158 | input.text = selection 159 | search(selection) 160 | else: 161 | search() 162 | else: 163 | input.text = "" 164 | 165 | 166 | func _on_input_gui_input(event: InputEvent) -> void: 167 | if event is InputEventKey and event.is_pressed(): 168 | match event.as_text(): 169 | "Enter": 170 | search(input.text) 171 | "Escape": 172 | emit_signal("close_requested") 173 | 174 | 175 | func _on_replace_button_pressed() -> void: 176 | if result_index == -1: return 177 | 178 | # Replace the selection at result index 179 | var r: Array = results[result_index] 180 | code_edit.begin_complex_operation() 181 | var lines: PackedStringArray = code_edit.text.split("\n") 182 | var line: String = lines[r[0]] 183 | line = line.substr(0, r[1]) + replace_input.text + line.substr(r[1] + r[2]) 184 | lines[r[0]] = line 185 | code_edit.text = "\n".join(lines) 186 | code_edit.end_complex_operation() 187 | code_edit.text_changed.emit() 188 | 189 | search(input.text, result_index) 190 | 191 | 192 | func _on_replace_all_button_pressed() -> void: 193 | if match_case_button.button_pressed: 194 | code_edit.text = code_edit.text.replace(input.text, replace_input.text) 195 | else: 196 | code_edit.text = code_edit.text.replacen(input.text, replace_input.text) 197 | search() 198 | code_edit.text_changed.emit() 199 | 200 | 201 | func _on_replace_check_button_toggled(button_pressed: bool) -> void: 202 | replace_panel.visible = button_pressed 203 | if button_pressed: 204 | replace_input.grab_focus() 205 | 206 | 207 | func _on_input_focus_entered() -> void: 208 | if results.size() == 0: 209 | search() 210 | else: 211 | self.result_index = result_index 212 | 213 | 214 | func _on_match_case_check_box_toggled(button_pressed: bool) -> void: 215 | search() 216 | 217 | 218 | #endregion 219 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/search_and_replace.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cijsmjkq21cdq 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/search_and_replace.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://gr8nakpbrhby"] 2 | 3 | [ext_resource type="Script" uid="uid://cijsmjkq21cdq" path="res://addons/dialogue_manager/components/search_and_replace.gd" id="1_8oj1f"] 4 | 5 | [node name="SearchAndReplace" type="VBoxContainer"] 6 | visible = false 7 | anchors_preset = 10 8 | anchor_right = 1.0 9 | offset_bottom = 31.0 10 | grow_horizontal = 2 11 | size_flags_horizontal = 3 12 | script = ExtResource("1_8oj1f") 13 | 14 | [node name="Search" type="HBoxContainer" parent="."] 15 | layout_mode = 2 16 | 17 | [node name="Input" type="LineEdit" parent="Search"] 18 | layout_mode = 2 19 | size_flags_horizontal = 3 20 | placeholder_text = "Text to search for" 21 | metadata/_edit_use_custom_anchors = true 22 | 23 | [node name="MatchCaseCheckBox" type="CheckBox" parent="Search"] 24 | layout_mode = 2 25 | text = "Match case" 26 | 27 | [node name="VSeparator" type="VSeparator" parent="Search"] 28 | layout_mode = 2 29 | 30 | [node name="PreviousButton" type="Button" parent="Search"] 31 | layout_mode = 2 32 | tooltip_text = "Previous" 33 | flat = true 34 | 35 | [node name="ResultLabel" type="Label" parent="Search"] 36 | layout_mode = 2 37 | text = "0 of 0" 38 | 39 | [node name="NextButton" type="Button" parent="Search"] 40 | layout_mode = 2 41 | tooltip_text = "Next" 42 | flat = true 43 | 44 | [node name="VSeparator2" type="VSeparator" parent="Search"] 45 | layout_mode = 2 46 | 47 | [node name="ReplaceCheckButton" type="CheckButton" parent="Search"] 48 | layout_mode = 2 49 | text = "Replace" 50 | 51 | [node name="Replace" type="HBoxContainer" parent="."] 52 | visible = false 53 | layout_mode = 2 54 | 55 | [node name="ReplaceLabel" type="Label" parent="Replace"] 56 | layout_mode = 2 57 | text = "Replace with:" 58 | 59 | [node name="Input" type="LineEdit" parent="Replace"] 60 | layout_mode = 2 61 | size_flags_horizontal = 3 62 | 63 | [node name="ReplaceButton" type="Button" parent="Replace"] 64 | layout_mode = 2 65 | disabled = true 66 | text = "Replace" 67 | flat = true 68 | 69 | [node name="ReplaceAllButton" type="Button" parent="Replace"] 70 | layout_mode = 2 71 | disabled = true 72 | text = "Replace all" 73 | flat = true 74 | 75 | [connection signal="theme_changed" from="." to="." method="_on_search_and_replace_theme_changed"] 76 | [connection signal="visibility_changed" from="." to="." method="_on_search_and_replace_visibility_changed"] 77 | [connection signal="focus_entered" from="Search/Input" to="." method="_on_input_focus_entered"] 78 | [connection signal="gui_input" from="Search/Input" to="." method="_on_input_gui_input"] 79 | [connection signal="text_changed" from="Search/Input" to="." method="_on_input_text_changed"] 80 | [connection signal="toggled" from="Search/MatchCaseCheckBox" to="." method="_on_match_case_check_box_toggled"] 81 | [connection signal="pressed" from="Search/PreviousButton" to="." method="_on_previous_button_pressed"] 82 | [connection signal="pressed" from="Search/NextButton" to="." method="_on_next_button_pressed"] 83 | [connection signal="toggled" from="Search/ReplaceCheckButton" to="." method="_on_replace_check_button_toggled"] 84 | [connection signal="focus_entered" from="Replace/Input" to="." method="_on_input_focus_entered"] 85 | [connection signal="gui_input" from="Replace/Input" to="." method="_on_input_gui_input"] 86 | [connection signal="pressed" from="Replace/ReplaceButton" to="." method="_on_replace_button_pressed"] 87 | [connection signal="pressed" from="Replace/ReplaceAllButton" to="." method="_on_replace_all_button_pressed"] 88 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/title_list.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends VBoxContainer 3 | 4 | signal title_selected(title: String) 5 | 6 | 7 | const DialogueConstants = preload("../constants.gd") 8 | 9 | 10 | @onready var filter_edit: LineEdit = $FilterEdit 11 | @onready var list: ItemList = $List 12 | 13 | var titles: PackedStringArray: 14 | set(next_titles): 15 | titles = next_titles 16 | apply_filter() 17 | get: 18 | return titles 19 | 20 | var filter: String: 21 | set(next_filter): 22 | filter = next_filter 23 | apply_filter() 24 | get: 25 | return filter 26 | 27 | 28 | func _ready() -> void: 29 | apply_theme() 30 | 31 | filter_edit.placeholder_text = DialogueConstants.translate(&"titles_list.filter") 32 | 33 | 34 | func select_title(title: String) -> void: 35 | list.deselect_all() 36 | for i in range(0, list.get_item_count()): 37 | if list.get_item_text(i) == title.strip_edges(): 38 | list.select(i) 39 | 40 | 41 | func apply_filter() -> void: 42 | list.clear() 43 | for title in titles: 44 | if filter == "" or filter.to_lower() in title.to_lower(): 45 | list.add_item(title.strip_edges()) 46 | 47 | 48 | func apply_theme() -> void: 49 | if is_instance_valid(filter_edit): 50 | filter_edit.right_icon = get_theme_icon("Search", "EditorIcons") 51 | if is_instance_valid(list): 52 | list.add_theme_stylebox_override("panel", get_theme_stylebox("panel", "Panel")) 53 | 54 | 55 | ### Signals 56 | 57 | 58 | func _on_theme_changed() -> void: 59 | apply_theme() 60 | 61 | 62 | func _on_filter_edit_text_changed(new_text: String) -> void: 63 | self.filter = new_text 64 | 65 | 66 | func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void: 67 | if mouse_button_index == MOUSE_BUTTON_LEFT: 68 | var title = list.get_item_text(index) 69 | title_selected.emit(title) 70 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/title_list.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d0k2wndjj0ifm 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/title_list.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://ctns6ouwwd68i"] 2 | 3 | [ext_resource type="Script" uid="uid://d0k2wndjj0ifm" path="res://addons/dialogue_manager/components/title_list.gd" id="1_5qqmd"] 4 | 5 | [node name="TitleList" type="VBoxContainer"] 6 | anchors_preset = 15 7 | anchor_right = 1.0 8 | anchor_bottom = 1.0 9 | grow_horizontal = 2 10 | grow_vertical = 2 11 | size_flags_horizontal = 3 12 | size_flags_vertical = 3 13 | script = ExtResource("1_5qqmd") 14 | 15 | [node name="FilterEdit" type="LineEdit" parent="."] 16 | layout_mode = 2 17 | placeholder_text = "Filter titles" 18 | clear_button_enabled = true 19 | 20 | [node name="List" type="ItemList" parent="."] 21 | layout_mode = 2 22 | size_flags_vertical = 3 23 | allow_reselect = true 24 | 25 | [connection signal="theme_changed" from="." to="." method="_on_theme_changed"] 26 | [connection signal="text_changed" from="FilterEdit" to="." method="_on_filter_edit_text_changed"] 27 | [connection signal="item_clicked" from="List" to="." method="_on_list_item_clicked"] 28 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/update_button.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Button 3 | 4 | const DialogueConstants = preload("../constants.gd") 5 | const DialogueSettings = preload("../settings.gd") 6 | 7 | const REMOTE_RELEASES_URL = "https://api.github.com/repos/nathanhoad/godot_dialogue_manager/releases" 8 | 9 | 10 | @onready var http_request: HTTPRequest = $HTTPRequest 11 | @onready var download_dialog: AcceptDialog = $DownloadDialog 12 | @onready var download_update_panel = $DownloadDialog/DownloadUpdatePanel 13 | @onready var needs_reload_dialog: AcceptDialog = $NeedsReloadDialog 14 | @onready var update_failed_dialog: AcceptDialog = $UpdateFailedDialog 15 | @onready var timer: Timer = $Timer 16 | 17 | var needs_reload: bool = false 18 | 19 | # A lambda that gets called just before refreshing the plugin. Return false to stop the reload. 20 | var on_before_refresh: Callable = func(): return true 21 | 22 | 23 | func _ready() -> void: 24 | hide() 25 | apply_theme() 26 | 27 | # Check for updates on GitHub 28 | check_for_update() 29 | 30 | # Check again every few hours 31 | timer.start(60 * 60 * 12) 32 | 33 | 34 | # Convert a version number to an actually comparable number 35 | func version_to_number(version: String) -> int: 36 | var bits = version.split(".") 37 | return bits[0].to_int() * 1000000 + bits[1].to_int() * 1000 + bits[2].to_int() 38 | 39 | 40 | func apply_theme() -> void: 41 | var color: Color = get_theme_color("success_color", "Editor") 42 | 43 | if needs_reload: 44 | color = get_theme_color("error_color", "Editor") 45 | icon = get_theme_icon("Reload", "EditorIcons") 46 | add_theme_color_override("icon_normal_color", color) 47 | add_theme_color_override("icon_focus_color", color) 48 | add_theme_color_override("icon_hover_color", color) 49 | 50 | add_theme_color_override("font_color", color) 51 | add_theme_color_override("font_focus_color", color) 52 | add_theme_color_override("font_hover_color", color) 53 | 54 | 55 | func check_for_update() -> void: 56 | if DialogueSettings.get_user_value("check_for_updates", true): 57 | http_request.request(REMOTE_RELEASES_URL) 58 | 59 | 60 | ### Signals 61 | 62 | 63 | func _on_http_request_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: 64 | if result != HTTPRequest.RESULT_SUCCESS: return 65 | 66 | var current_version: String = Engine.get_meta("DialogueManagerPlugin").get_version() 67 | 68 | # Work out the next version from the releases information on GitHub 69 | var response = JSON.parse_string(body.get_string_from_utf8()) 70 | if typeof(response) != TYPE_ARRAY: return 71 | 72 | # GitHub releases are in order of creation, not order of version 73 | var versions = (response as Array).filter(func(release): 74 | var version: String = release.tag_name.substr(1) 75 | var major_version: int = version.split(".")[0].to_int() 76 | var current_major_version: int = current_version.split(".")[0].to_int() 77 | return major_version == current_major_version and version_to_number(version) > version_to_number(current_version) 78 | ) 79 | if versions.size() > 0: 80 | download_update_panel.next_version_release = versions[0] 81 | text = DialogueConstants.translate(&"update.available").format({ version = versions[0].tag_name.substr(1) }) 82 | show() 83 | 84 | 85 | func _on_update_button_pressed() -> void: 86 | if needs_reload: 87 | var will_refresh = on_before_refresh.call() 88 | if will_refresh: 89 | EditorInterface.restart_editor(true) 90 | else: 91 | var scale: float = EditorInterface.get_editor_scale() 92 | download_dialog.min_size = Vector2(300, 250) * scale 93 | download_dialog.popup_centered() 94 | 95 | 96 | func _on_download_dialog_close_requested() -> void: 97 | download_dialog.hide() 98 | 99 | 100 | func _on_download_update_panel_updated(updated_to_version: String) -> void: 101 | download_dialog.hide() 102 | 103 | needs_reload_dialog.dialog_text = DialogueConstants.translate(&"update.needs_reload") 104 | needs_reload_dialog.ok_button_text = DialogueConstants.translate(&"update.reload_ok_button") 105 | needs_reload_dialog.cancel_button_text = DialogueConstants.translate(&"update.reload_cancel_button") 106 | needs_reload_dialog.popup_centered() 107 | 108 | needs_reload = true 109 | text = DialogueConstants.translate(&"update.reload_project") 110 | apply_theme() 111 | 112 | 113 | func _on_download_update_panel_failed() -> void: 114 | download_dialog.hide() 115 | update_failed_dialog.dialog_text = DialogueConstants.translate(&"update.failed") 116 | update_failed_dialog.popup_centered() 117 | 118 | 119 | func _on_needs_reload_dialog_confirmed() -> void: 120 | EditorInterface.restart_editor(true) 121 | 122 | 123 | func _on_timer_timeout() -> void: 124 | if not needs_reload: 125 | check_for_update() 126 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/update_button.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cr1tt12dh5ecr 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/components/update_button.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://co8yl23idiwbi"] 2 | 3 | [ext_resource type="Script" uid="uid://cr1tt12dh5ecr" path="res://addons/dialogue_manager/components/update_button.gd" id="1_d2tpb"] 4 | [ext_resource type="PackedScene" uid="uid://qdxrxv3c3hxk" path="res://addons/dialogue_manager/components/download_update_panel.tscn" id="2_iwm7r"] 5 | 6 | [node name="UpdateButton" type="Button"] 7 | visible = false 8 | offset_right = 8.0 9 | offset_bottom = 8.0 10 | theme_override_colors/font_color = Color(0, 0, 0, 1) 11 | theme_override_colors/font_hover_color = Color(0, 0, 0, 1) 12 | theme_override_colors/font_focus_color = Color(0, 0, 0, 1) 13 | text = "v2.9.0 available" 14 | flat = true 15 | script = ExtResource("1_d2tpb") 16 | 17 | [node name="HTTPRequest" type="HTTPRequest" parent="."] 18 | 19 | [node name="DownloadDialog" type="AcceptDialog" parent="."] 20 | title = "Download update" 21 | size = Vector2i(400, 300) 22 | unresizable = true 23 | min_size = Vector2i(300, 250) 24 | ok_button_text = "Close" 25 | 26 | [node name="DownloadUpdatePanel" parent="DownloadDialog" instance=ExtResource("2_iwm7r")] 27 | 28 | [node name="UpdateFailedDialog" type="AcceptDialog" parent="."] 29 | dialog_text = "You have been updated to version 2.4.3" 30 | 31 | [node name="NeedsReloadDialog" type="ConfirmationDialog" parent="."] 32 | 33 | [node name="Timer" type="Timer" parent="."] 34 | wait_time = 14400.0 35 | 36 | [connection signal="pressed" from="." to="." method="_on_update_button_pressed"] 37 | [connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"] 38 | [connection signal="close_requested" from="DownloadDialog" to="." method="_on_download_dialog_close_requested"] 39 | [connection signal="failed" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_failed"] 40 | [connection signal="updated" from="DownloadDialog/DownloadUpdatePanel" to="." method="_on_download_update_panel_updated"] 41 | [connection signal="confirmed" from="NeedsReloadDialog" to="." method="_on_needs_reload_dialog_confirmed"] 42 | [connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"] 43 | -------------------------------------------------------------------------------- /addons/dialogue_manager/constants.gd: -------------------------------------------------------------------------------- 1 | class_name DMConstants extends RefCounted 2 | 3 | 4 | const USER_CONFIG_PATH = "user://dialogue_manager_user_config.json" 5 | const CACHE_PATH = "user://dialogue_manager_cache.json" 6 | 7 | 8 | enum MutationBehaviour { 9 | Wait, 10 | DoNotWait, 11 | Skip 12 | } 13 | 14 | enum TranslationSource { 15 | None, 16 | Guess, 17 | CSV, 18 | PO 19 | } 20 | 21 | # Token types 22 | 23 | const TOKEN_FUNCTION = &"function" 24 | const TOKEN_DICTIONARY_REFERENCE = &"dictionary_reference" 25 | const TOKEN_DICTIONARY_NESTED_REFERENCE = &"dictionary_nested_reference" 26 | const TOKEN_GROUP = &"group" 27 | const TOKEN_ARRAY = &"array" 28 | const TOKEN_DICTIONARY = &"dictionary" 29 | const TOKEN_PARENS_OPEN = &"parens_open" 30 | const TOKEN_PARENS_CLOSE = &"parens_close" 31 | const TOKEN_BRACKET_OPEN = &"bracket_open" 32 | const TOKEN_BRACKET_CLOSE = &"bracket_close" 33 | const TOKEN_BRACE_OPEN = &"brace_open" 34 | const TOKEN_BRACE_CLOSE = &"brace_close" 35 | const TOKEN_COLON = &"colon" 36 | const TOKEN_COMPARISON = &"comparison" 37 | const TOKEN_ASSIGNMENT = &"assignment" 38 | const TOKEN_OPERATOR = &"operator" 39 | const TOKEN_COMMA = &"comma" 40 | const TOKEN_NULL_COALESCE = &"null_coalesce" 41 | const TOKEN_DOT = &"dot" 42 | const TOKEN_CONDITION = &"condition" 43 | const TOKEN_BOOL = &"bool" 44 | const TOKEN_NOT = &"not" 45 | const TOKEN_AND_OR = &"and_or" 46 | const TOKEN_STRING = &"string" 47 | const TOKEN_NUMBER = &"number" 48 | const TOKEN_VARIABLE = &"variable" 49 | const TOKEN_COMMENT = &"comment" 50 | 51 | const TOKEN_VALUE = &"value" 52 | const TOKEN_ERROR = &"error" 53 | 54 | # Line types 55 | 56 | const TYPE_UNKNOWN = &"" 57 | const TYPE_IMPORT = &"import" 58 | const TYPE_USING = &"using" 59 | const TYPE_COMMENT = &"comment" 60 | const TYPE_RESPONSE = &"response" 61 | const TYPE_TITLE = &"title" 62 | const TYPE_CONDITION = &"condition" 63 | const TYPE_WHILE = &"while" 64 | const TYPE_MATCH = &"match" 65 | const TYPE_WHEN = &"when" 66 | const TYPE_MUTATION = &"mutation" 67 | const TYPE_GOTO = &"goto" 68 | const TYPE_DIALOGUE = &"dialogue" 69 | const TYPE_RANDOM = &"random" 70 | const TYPE_ERROR = &"error" 71 | 72 | # Line IDs 73 | 74 | const ID_NULL = &"" 75 | const ID_ERROR = &"error" 76 | const ID_ERROR_INVALID_TITLE = &"invalid title" 77 | const ID_ERROR_TITLE_HAS_NO_BODY = &"title has no body" 78 | const ID_END = &"end" 79 | const ID_END_CONVERSATION = &"end!" 80 | 81 | # Errors 82 | 83 | const ERR_ERRORS_IN_IMPORTED_FILE = 100 84 | const ERR_FILE_ALREADY_IMPORTED = 101 85 | const ERR_DUPLICATE_IMPORT_NAME = 102 86 | const ERR_EMPTY_TITLE = 103 87 | const ERR_DUPLICATE_TITLE = 104 88 | const ERR_TITLE_INVALID_CHARACTERS = 106 89 | const ERR_UNKNOWN_TITLE = 107 90 | const ERR_INVALID_TITLE_REFERENCE = 108 91 | const ERR_TITLE_REFERENCE_HAS_NO_CONTENT = 109 92 | const ERR_INVALID_EXPRESSION = 110 93 | const ERR_UNEXPECTED_CONDITION = 111 94 | const ERR_DUPLICATE_ID = 112 95 | const ERR_MISSING_ID = 113 96 | const ERR_INVALID_INDENTATION = 114 97 | const ERR_INVALID_CONDITION_INDENTATION = 115 98 | const ERR_INCOMPLETE_EXPRESSION = 116 99 | const ERR_INVALID_EXPRESSION_FOR_VALUE = 117 100 | const ERR_UNKNOWN_LINE_SYNTAX = 118 101 | const ERR_TITLE_BEGINS_WITH_NUMBER = 119 102 | const ERR_UNEXPECTED_END_OF_EXPRESSION = 120 103 | const ERR_UNEXPECTED_FUNCTION = 121 104 | const ERR_UNEXPECTED_BRACKET = 122 105 | const ERR_UNEXPECTED_CLOSING_BRACKET = 123 106 | const ERR_MISSING_CLOSING_BRACKET = 124 107 | const ERR_UNEXPECTED_OPERATOR = 125 108 | const ERR_UNEXPECTED_COMMA = 126 109 | const ERR_UNEXPECTED_COLON = 127 110 | const ERR_UNEXPECTED_DOT = 128 111 | const ERR_UNEXPECTED_BOOLEAN = 129 112 | const ERR_UNEXPECTED_STRING = 130 113 | const ERR_UNEXPECTED_NUMBER = 131 114 | const ERR_UNEXPECTED_VARIABLE = 132 115 | const ERR_INVALID_INDEX = 133 116 | const ERR_UNEXPECTED_ASSIGNMENT = 134 117 | const ERR_UNKNOWN_USING = 135 118 | const ERR_EXPECTED_WHEN_OR_ELSE = 136 119 | const ERR_ONLY_ONE_ELSE_ALLOWED = 137 120 | const ERR_WHEN_MUST_BELONG_TO_MATCH = 138 121 | const ERR_CONCURRENT_LINE_WITHOUT_ORIGIN = 139 122 | const ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES = 140 123 | const ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE = 141 124 | const ERR_NESTED_DIALOGUE_INVALID_JUMP = 142 125 | 126 | 127 | static var _current_locale: String = "" 128 | static var _current_translation: Translation 129 | 130 | 131 | ## Get the error message 132 | static func get_error_message(error: int) -> String: 133 | match error: 134 | ERR_ERRORS_IN_IMPORTED_FILE: 135 | return translate(&"errors.import_errors") 136 | ERR_FILE_ALREADY_IMPORTED: 137 | return translate(&"errors.already_imported") 138 | ERR_DUPLICATE_IMPORT_NAME: 139 | return translate(&"errors.duplicate_import") 140 | ERR_EMPTY_TITLE: 141 | return translate(&"errors.empty_title") 142 | ERR_DUPLICATE_TITLE: 143 | return translate(&"errors.duplicate_title") 144 | ERR_TITLE_INVALID_CHARACTERS: 145 | return translate(&"errors.invalid_title_string") 146 | ERR_TITLE_BEGINS_WITH_NUMBER: 147 | return translate(&"errors.invalid_title_number") 148 | ERR_UNKNOWN_TITLE: 149 | return translate(&"errors.unknown_title") 150 | ERR_INVALID_TITLE_REFERENCE: 151 | return translate(&"errors.jump_to_invalid_title") 152 | ERR_TITLE_REFERENCE_HAS_NO_CONTENT: 153 | return translate(&"errors.title_has_no_content") 154 | ERR_INVALID_EXPRESSION: 155 | return translate(&"errors.invalid_expression") 156 | ERR_UNEXPECTED_CONDITION: 157 | return translate(&"errors.unexpected_condition") 158 | ERR_DUPLICATE_ID: 159 | return translate(&"errors.duplicate_id") 160 | ERR_MISSING_ID: 161 | return translate(&"errors.missing_id") 162 | ERR_INVALID_INDENTATION: 163 | return translate(&"errors.invalid_indentation") 164 | ERR_INVALID_CONDITION_INDENTATION: 165 | return translate(&"errors.condition_has_no_content") 166 | ERR_INCOMPLETE_EXPRESSION: 167 | return translate(&"errors.incomplete_expression") 168 | ERR_INVALID_EXPRESSION_FOR_VALUE: 169 | return translate(&"errors.invalid_expression_for_value") 170 | ERR_FILE_NOT_FOUND: 171 | return translate(&"errors.file_not_found") 172 | ERR_UNEXPECTED_END_OF_EXPRESSION: 173 | return translate(&"errors.unexpected_end_of_expression") 174 | ERR_UNEXPECTED_FUNCTION: 175 | return translate(&"errors.unexpected_function") 176 | ERR_UNEXPECTED_BRACKET: 177 | return translate(&"errors.unexpected_bracket") 178 | ERR_UNEXPECTED_CLOSING_BRACKET: 179 | return translate(&"errors.unexpected_closing_bracket") 180 | ERR_MISSING_CLOSING_BRACKET: 181 | return translate(&"errors.missing_closing_bracket") 182 | ERR_UNEXPECTED_OPERATOR: 183 | return translate(&"errors.unexpected_operator") 184 | ERR_UNEXPECTED_COMMA: 185 | return translate(&"errors.unexpected_comma") 186 | ERR_UNEXPECTED_COLON: 187 | return translate(&"errors.unexpected_colon") 188 | ERR_UNEXPECTED_DOT: 189 | return translate(&"errors.unexpected_dot") 190 | ERR_UNEXPECTED_BOOLEAN: 191 | return translate(&"errors.unexpected_boolean") 192 | ERR_UNEXPECTED_STRING: 193 | return translate(&"errors.unexpected_string") 194 | ERR_UNEXPECTED_NUMBER: 195 | return translate(&"errors.unexpected_number") 196 | ERR_UNEXPECTED_VARIABLE: 197 | return translate(&"errors.unexpected_variable") 198 | ERR_INVALID_INDEX: 199 | return translate(&"errors.invalid_index") 200 | ERR_UNEXPECTED_ASSIGNMENT: 201 | return translate(&"errors.unexpected_assignment") 202 | ERR_UNKNOWN_USING: 203 | return translate(&"errors.unknown_using") 204 | ERR_EXPECTED_WHEN_OR_ELSE: 205 | return translate(&"errors.expected_when_or_else") 206 | ERR_ONLY_ONE_ELSE_ALLOWED: 207 | return translate(&"errors.only_one_else_allowed") 208 | ERR_WHEN_MUST_BELONG_TO_MATCH: 209 | return translate(&"errors.when_must_belong_to_match") 210 | ERR_CONCURRENT_LINE_WITHOUT_ORIGIN: 211 | return translate(&"errors.concurrent_line_without_origin") 212 | ERR_GOTO_NOT_ALLOWED_ON_CONCURRECT_LINES: 213 | return translate(&"errors.goto_not_allowed_on_concurrect_lines") 214 | ERR_UNEXPECTED_SYNTAX_ON_NESTED_DIALOGUE_LINE: 215 | return translate(&"errors.unexpected_syntax_on_nested_dialogue_line") 216 | ERR_NESTED_DIALOGUE_INVALID_JUMP: 217 | return translate(&"errors.err_nested_dialogue_invalid_jump") 218 | 219 | return translate(&"errors.unknown") 220 | 221 | 222 | static func translate(string: String) -> String: 223 | var locale: String = TranslationServer.get_tool_locale() 224 | if _current_translation == null or _current_locale != locale: 225 | var base_path: String = new().get_script().resource_path.get_base_dir() 226 | var translation_path: String = "%s/l10n/%s.po" % [base_path, locale] 227 | var fallback_translation_path: String = "%s/l10n/%s.po" % [base_path, locale.substr(0, 2)] 228 | var en_translation_path: String = "%s/l10n/en.po" % base_path 229 | _current_translation = load(translation_path if FileAccess.file_exists(translation_path) else (fallback_translation_path if FileAccess.file_exists(fallback_translation_path) else en_translation_path)) 230 | _current_locale = locale 231 | return _current_translation.get_message(string) 232 | -------------------------------------------------------------------------------- /addons/dialogue_manager/constants.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b1oarbmjtyesf 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_label.gd: -------------------------------------------------------------------------------- 1 | @icon("./assets/icon.svg") 2 | 3 | @tool 4 | 5 | ## A RichTextLabel specifically for use with [b]Dialogue Manager[/b] dialogue. 6 | class_name DialogueLabel extends RichTextLabel 7 | 8 | 9 | ## Emitted for each letter typed out. 10 | signal spoke(letter: String, letter_index: int, speed: float) 11 | 12 | ## Emitted when typing paused for a `[wait]` 13 | signal paused_typing(duration: float) 14 | 15 | ## Emitted when the player skips the typing of dialogue. 16 | signal skipped_typing() 17 | 18 | ## Emitted when typing finishes. 19 | signal finished_typing() 20 | 21 | 22 | # The action to press to skip typing. 23 | @export var skip_action: StringName = &"ui_cancel" 24 | 25 | ## The speed with which the text types out. 26 | @export var seconds_per_step: float = 0.02 27 | 28 | ## Automatically have a brief pause when these characters are encountered. 29 | @export var pause_at_characters: String = ".?!" 30 | 31 | ## Don't auto pause if the character after the pause is one of these. 32 | @export var skip_pause_at_character_if_followed_by: String = ")\"" 33 | 34 | ## Don't auto pause after these abbreviations (only if "." is in `pause_at_characters`).[br] 35 | ## Abbreviations are limitted to 5 characters in length [br] 36 | ## Does not support multi-period abbreviations (ex. "p.m.") 37 | @export var skip_pause_at_abbreviations: PackedStringArray = ["Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex"] 38 | 39 | ## The amount of time to pause when exposing a character present in `pause_at_characters`. 40 | @export var seconds_per_pause_step: float = 0.3 41 | 42 | var _already_mutated_indices: PackedInt32Array = [] 43 | 44 | 45 | ## The current line of dialogue. 46 | var dialogue_line: 47 | set(next_dialogue_line): 48 | dialogue_line = next_dialogue_line 49 | custom_minimum_size = Vector2.ZERO 50 | text = "" 51 | text = dialogue_line.text 52 | get: 53 | return dialogue_line 54 | 55 | ## Whether the label is currently typing itself out. 56 | var is_typing: bool = false: 57 | set(value): 58 | var is_finished: bool = is_typing != value and value == false 59 | is_typing = value 60 | if is_finished: 61 | finished_typing.emit() 62 | get: 63 | return is_typing 64 | 65 | var _last_wait_index: int = -1 66 | var _last_mutation_index: int = -1 67 | var _waiting_seconds: float = 0 68 | var _is_awaiting_mutation: bool = false 69 | 70 | 71 | func _process(delta: float) -> void: 72 | if self.is_typing: 73 | # Type out text 74 | if visible_ratio < 1: 75 | # See if we are waiting 76 | if _waiting_seconds > 0: 77 | _waiting_seconds = _waiting_seconds - delta 78 | # If we are no longer waiting then keep typing 79 | if _waiting_seconds <= 0: 80 | _type_next(delta, _waiting_seconds) 81 | else: 82 | # Make sure any mutations at the end of the line get run 83 | _mutate_inline_mutations(get_total_character_count()) 84 | self.is_typing = false 85 | 86 | 87 | func _unhandled_input(event: InputEvent) -> void: 88 | # Note: this will no longer be reached if using Dialogue Manager > 2.32.2. To make skip handling 89 | # simpler (so all of mouse/keyboard/joypad are together) it is now the responsibility of the 90 | # dialogue balloon. 91 | if self.is_typing and visible_ratio < 1 and InputMap.has_action(skip_action) and event.is_action_pressed(skip_action): 92 | get_viewport().set_input_as_handled() 93 | skip_typing() 94 | 95 | 96 | ## Start typing out the text 97 | func type_out() -> void: 98 | text = dialogue_line.text 99 | visible_characters = 0 100 | visible_ratio = 0 101 | _waiting_seconds = 0 102 | _last_wait_index = -1 103 | _last_mutation_index = -1 104 | _already_mutated_indices.clear() 105 | 106 | self.is_typing = true 107 | 108 | # Allow typing listeners a chance to connect 109 | await get_tree().process_frame 110 | 111 | if get_total_character_count() == 0: 112 | self.is_typing = false 113 | elif seconds_per_step == 0: 114 | _mutate_remaining_mutations() 115 | visible_characters = get_total_character_count() 116 | self.is_typing = false 117 | 118 | 119 | ## Stop typing out the text and jump right to the end 120 | func skip_typing() -> void: 121 | _mutate_remaining_mutations() 122 | visible_characters = get_total_character_count() 123 | self.is_typing = false 124 | skipped_typing.emit() 125 | 126 | 127 | # Type out the next character(s) 128 | func _type_next(delta: float, seconds_needed: float) -> void: 129 | if _is_awaiting_mutation: return 130 | 131 | if visible_characters == get_total_character_count(): 132 | return 133 | 134 | if _last_mutation_index != visible_characters: 135 | _last_mutation_index = visible_characters 136 | _mutate_inline_mutations(visible_characters) 137 | if _is_awaiting_mutation: return 138 | 139 | var additional_waiting_seconds: float = _get_pause(visible_characters) 140 | 141 | # Pause on characters like "." 142 | if _should_auto_pause(): 143 | additional_waiting_seconds += seconds_per_pause_step 144 | 145 | # Pause at literal [wait] directives 146 | if _last_wait_index != visible_characters and additional_waiting_seconds > 0: 147 | _last_wait_index = visible_characters 148 | _waiting_seconds += additional_waiting_seconds 149 | paused_typing.emit(_get_pause(visible_characters)) 150 | else: 151 | visible_characters += 1 152 | if visible_characters <= get_total_character_count(): 153 | spoke.emit(get_parsed_text()[visible_characters - 1], visible_characters - 1, _get_speed(visible_characters)) 154 | # See if there's time to type out some more in this frame 155 | seconds_needed += seconds_per_step * (1.0 / _get_speed(visible_characters)) 156 | if seconds_needed > delta: 157 | _waiting_seconds += seconds_needed 158 | else: 159 | _type_next(delta, seconds_needed) 160 | 161 | 162 | # Get the pause for the current typing position if there is one 163 | func _get_pause(at_index: int) -> float: 164 | return dialogue_line.pauses.get(at_index, 0) 165 | 166 | 167 | # Get the speed for the current typing position 168 | func _get_speed(at_index: int) -> float: 169 | var speed: float = 1 170 | for index in dialogue_line.speeds: 171 | if index > at_index: 172 | return speed 173 | speed = dialogue_line.speeds[index] 174 | return speed 175 | 176 | 177 | # Run any inline mutations that haven't been run yet 178 | func _mutate_remaining_mutations() -> void: 179 | for i in range(visible_characters, get_total_character_count() + 1): 180 | _mutate_inline_mutations(i) 181 | 182 | 183 | # Run any mutations at the current typing position 184 | func _mutate_inline_mutations(index: int) -> void: 185 | for inline_mutation in dialogue_line.inline_mutations: 186 | # inline mutations are an array of arrays in the form of [character index, resolvable function] 187 | if inline_mutation[0] > index: 188 | return 189 | if inline_mutation[0] == index and not _already_mutated_indices.has(index): 190 | _is_awaiting_mutation = true 191 | # The DialogueManager can't be referenced directly here so we need to get it by its path 192 | await Engine.get_singleton("DialogueManager")._mutate(inline_mutation[1], dialogue_line.extra_game_states, true) 193 | _is_awaiting_mutation = false 194 | 195 | _already_mutated_indices.append(index) 196 | 197 | 198 | # Determine if the current autopause character at the cursor should qualify to pause typing. 199 | func _should_auto_pause() -> bool: 200 | if visible_characters == 0: return false 201 | 202 | var parsed_text: String = get_parsed_text() 203 | 204 | # Avoid outofbounds when the label auto-translates and the text changes to one shorter while typing out 205 | # Note: visible characters can be larger than parsed_text after a translation event 206 | if visible_characters >= parsed_text.length(): return false 207 | 208 | # Ignore pause characters if they are next to a non-pause character 209 | if parsed_text[visible_characters] in skip_pause_at_character_if_followed_by.split(): 210 | return false 211 | 212 | # Ignore "." if it's between two numbers 213 | if visible_characters > 3 and parsed_text[visible_characters - 1] == ".": 214 | var possible_number: String = parsed_text.substr(visible_characters - 2, 3) 215 | if str(float(possible_number)).pad_decimals(1) == possible_number: 216 | return false 217 | 218 | # Ignore "." if it's used in an abbreviation 219 | # Note: does NOT support multi-period abbreviations (ex. p.m.) 220 | if "." in pause_at_characters and parsed_text[visible_characters - 1] == ".": 221 | for abbreviation in skip_pause_at_abbreviations: 222 | if visible_characters >= abbreviation.length(): 223 | var previous_characters: String = parsed_text.substr(visible_characters - abbreviation.length() - 1, abbreviation.length()) 224 | if previous_characters == abbreviation: 225 | return false 226 | 227 | # Ignore two non-"." characters next to each other 228 | var other_pause_characters: PackedStringArray = pause_at_characters.replace(".", "").split() 229 | if visible_characters > 1 and parsed_text[visible_characters - 1] in other_pause_characters and parsed_text[visible_characters] in other_pause_characters: 230 | return false 231 | 232 | return parsed_text[visible_characters - 1] in pause_at_characters.split() 233 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_label.gd.uid: -------------------------------------------------------------------------------- 1 | uid://g32um0mltv5d 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_label.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://ckvgyvclnwggo"] 2 | 3 | [ext_resource type="Script" uid="uid://g32um0mltv5d" path="res://addons/dialogue_manager/dialogue_label.gd" id="1_cital"] 4 | 5 | [node name="DialogueLabel" type="RichTextLabel"] 6 | anchors_preset = 10 7 | anchor_right = 1.0 8 | grow_horizontal = 2 9 | mouse_filter = 1 10 | bbcode_enabled = true 11 | fit_content = true 12 | scroll_active = false 13 | shortcut_keys_enabled = false 14 | meta_underlined = false 15 | hint_underlined = false 16 | deselect_on_focus_loss_enabled = false 17 | visible_characters_behavior = 1 18 | script = ExtResource("1_cital") 19 | skip_pause_at_abbreviations = PackedStringArray("Mr", "Mrs", "Ms", "Dr", "etc", "eg", "ex") 20 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_line.gd: -------------------------------------------------------------------------------- 1 | ## A line of dialogue returned from [code]DialogueManager[/code]. 2 | class_name DialogueLine extends RefCounted 3 | 4 | 5 | ## The ID of this line 6 | var id: String 7 | 8 | ## The internal type of this dialogue object. One of [code]TYPE_DIALOGUE[/code] or [code]TYPE_MUTATION[/code] 9 | var type: String = DMConstants.TYPE_DIALOGUE 10 | 11 | ## The next line ID after this line. 12 | var next_id: String = "" 13 | 14 | ## The character name that is saying this line. 15 | var character: String = "" 16 | 17 | ## A dictionary of variable replacements fo the character name. Generally for internal use only. 18 | var character_replacements: Array[Dictionary] = [] 19 | 20 | ## The dialogue being spoken. 21 | var text: String = "" 22 | 23 | ## A dictionary of replacements for the text. Generally for internal use only. 24 | var text_replacements: Array[Dictionary] = [] 25 | 26 | ## The key to use for translating this line. 27 | var translation_key: String = "" 28 | 29 | ## A map for when and for how long to pause while typing out the dialogue text. 30 | var pauses: Dictionary = {} 31 | 32 | ## A map for speed changes when typing out the dialogue text. 33 | var speeds: Dictionary = {} 34 | 35 | ## A map of any mutations to run while typing out the dialogue text. 36 | var inline_mutations: Array[Array] = [] 37 | 38 | ## A list of responses attached to this line of dialogue. 39 | var responses: Array = [] 40 | 41 | ## A list of lines that are spoken simultaneously with this one. 42 | var concurrent_lines: Array[DialogueLine] = [] 43 | 44 | ## A list of any extra game states to check when resolving variables and mutations. 45 | var extra_game_states: Array = [] 46 | 47 | ## How long to show this line before advancing to the next. Either a float (of seconds), [code]"auto"[/code], or [code]null[/code]. 48 | var time: String = "" 49 | 50 | ## Any #tags that were included in the line 51 | var tags: PackedStringArray = [] 52 | 53 | ## The mutation details if this is a mutation line (where [code]type == TYPE_MUTATION[/code]). 54 | var mutation: Dictionary = {} 55 | 56 | ## The conditions to check before including this line in the flow of dialogue. If failed the line will be skipped over. 57 | var conditions: Dictionary = {} 58 | 59 | 60 | func _init(data: Dictionary = {}) -> void: 61 | if data.size() > 0: 62 | id = data.id 63 | next_id = data.next_id 64 | type = data.type 65 | extra_game_states = data.get("extra_game_states", []) 66 | 67 | match type: 68 | DMConstants.TYPE_DIALOGUE: 69 | character = data.character 70 | character_replacements = data.get("character_replacements", [] as Array[Dictionary]) 71 | text = data.text 72 | text_replacements = data.get("text_replacements", [] as Array[Dictionary]) 73 | translation_key = data.get("translation_key", data.text) 74 | pauses = data.get("pauses", {}) 75 | speeds = data.get("speeds", {}) 76 | inline_mutations = data.get("inline_mutations", [] as Array[Array]) 77 | time = data.get("time", "") 78 | tags = data.get("tags", []) 79 | concurrent_lines = data.get("concurrent_lines", [] as Array[DialogueLine]) 80 | 81 | DMConstants.TYPE_MUTATION: 82 | mutation = data.mutation 83 | 84 | 85 | func _to_string() -> String: 86 | match type: 87 | DMConstants.TYPE_DIALOGUE: 88 | return "" % [character, text] 89 | DMConstants.TYPE_MUTATION: 90 | return "" 91 | return "" 92 | 93 | 94 | func get_tag_value(tag_name: String) -> String: 95 | var wrapped := "%s=" % tag_name 96 | for t in tags: 97 | if t.begins_with(wrapped): 98 | return t.replace(wrapped, "").strip_edges() 99 | return "" 100 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_line.gd.uid: -------------------------------------------------------------------------------- 1 | uid://rhuq0eyf8ar2 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_manager.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c3rodes2l3gxb 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_resource.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("./assets/icon.svg") 3 | 4 | ## A collection of dialogue lines for use with [code]DialogueManager[/code]. 5 | class_name DialogueResource extends Resource 6 | 7 | 8 | const DialogueLine = preload("./dialogue_line.gd") 9 | 10 | ## A list of state shortcuts 11 | @export var using_states: PackedStringArray = [] 12 | 13 | ## A map of titles and the lines they point to. 14 | @export var titles: Dictionary = {} 15 | 16 | ## A list of character names. 17 | @export var character_names: PackedStringArray = [] 18 | 19 | ## The first title in the file. 20 | @export var first_title: String = "" 21 | 22 | ## A map of the encoded lines of dialogue. 23 | @export var lines: Dictionary = {} 24 | 25 | ## raw version of the text 26 | @export var raw_text: String 27 | 28 | 29 | ## Get the next printable line of dialogue, starting from a referenced line ([code]title[/code] can 30 | ## be a title string or a stringified line number). Runs any mutations along the way and then returns 31 | ## the first dialogue line encountered. 32 | func get_next_dialogue_line(title: String = "", extra_game_states: Array = [], mutation_behaviour: DMConstants.MutationBehaviour = DMConstants.MutationBehaviour.Wait) -> DialogueLine: 33 | return await Engine.get_singleton("DialogueManager").get_next_dialogue_line(self, title, extra_game_states, mutation_behaviour) 34 | 35 | 36 | ## Get the list of any titles found in the file. 37 | func get_titles() -> PackedStringArray: 38 | return titles.keys() 39 | 40 | 41 | func _to_string() -> String: 42 | return "" % [",".join(titles.keys())] 43 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_resource.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dbs4435dsf3ry 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_response.gd: -------------------------------------------------------------------------------- 1 | ## A response to a line of dialogue, usualy attached to a [code]DialogueLine[/code]. 2 | class_name DialogueResponse extends RefCounted 3 | 4 | 5 | ## The ID of this response 6 | var id: String 7 | 8 | ## The internal type of this dialogue object, always set to [code]TYPE_RESPONSE[/code]. 9 | var type: String = DMConstants.TYPE_RESPONSE 10 | 11 | ## The next line ID to use if this response is selected by the player. 12 | var next_id: String = "" 13 | 14 | ## [code]true[/code] if the condition of this line was met. 15 | var is_allowed: bool = true 16 | 17 | ## The original condition text. 18 | var condition_as_text: String = "" 19 | 20 | ## A character (depending on the "characters in responses" behaviour setting). 21 | var character: String = "" 22 | 23 | ## A dictionary of varialbe replaces for the character name. Generally for internal use only. 24 | var character_replacements: Array[Dictionary] = [] 25 | 26 | ## The prompt for this response. 27 | var text: String = "" 28 | 29 | ## A dictionary of variable replaces for the text. Generally for internal use only. 30 | var text_replacements: Array[Dictionary] = [] 31 | 32 | ## Any #tags 33 | var tags: PackedStringArray = [] 34 | 35 | ## The key to use for translating the text. 36 | var translation_key: String = "" 37 | 38 | 39 | func _init(data: Dictionary = {}) -> void: 40 | if data.size() > 0: 41 | id = data.id 42 | type = data.type 43 | next_id = data.next_id 44 | is_allowed = data.is_allowed 45 | character = data.character 46 | character_replacements = data.character_replacements 47 | text = data.text 48 | text_replacements = data.text_replacements 49 | tags = data.tags 50 | translation_key = data.translation_key 51 | condition_as_text = data.condition_as_text 52 | 53 | 54 | func _to_string() -> String: 55 | return "" % text 56 | 57 | 58 | func get_tag_value(tag_name: String) -> String: 59 | var wrapped := "%s=" % tag_name 60 | for t in tags: 61 | if t.begins_with(wrapped): 62 | return t.replace(wrapped, "").strip_edges() 63 | return "" 64 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_response.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cm0xpfeywpqid 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_responses_menu.gd: -------------------------------------------------------------------------------- 1 | @icon("./assets/responses_menu.svg") 2 | 3 | ## A [Container] for dialogue responses provided by [b]Dialogue Manager[/b]. 4 | class_name DialogueResponsesMenu extends Container 5 | 6 | 7 | ## Emitted when a response is selected. 8 | signal response_selected(response) 9 | 10 | 11 | ## Optionally specify a control to duplicate for each response 12 | @export var response_template: Control 13 | 14 | ## The action for accepting a response (is possibly overridden by parent dialogue balloon). 15 | @export var next_action: StringName = &"" 16 | 17 | ## Hide any responses where [code]is_allowed[/code] is false 18 | @export var hide_failed_responses: bool = false 19 | 20 | ## The list of dialogue responses. 21 | var responses: Array = []: 22 | get: 23 | return responses 24 | set(value): 25 | responses = value 26 | 27 | # Remove any current items 28 | for item in get_children(): 29 | if item == response_template: continue 30 | 31 | remove_child(item) 32 | item.queue_free() 33 | 34 | # Add new items 35 | if responses.size() > 0: 36 | for response in responses: 37 | if hide_failed_responses and not response.is_allowed: continue 38 | 39 | var item: Control 40 | if is_instance_valid(response_template): 41 | item = response_template.duplicate(DUPLICATE_GROUPS | DUPLICATE_SCRIPTS | DUPLICATE_SIGNALS) 42 | item.show() 43 | else: 44 | item = Button.new() 45 | item.name = "Response%d" % get_child_count() 46 | if not response.is_allowed: 47 | item.name = item.name + &"Disallowed" 48 | item.disabled = true 49 | 50 | # If the item has a response property then use that 51 | if "response" in item: 52 | item.response = response 53 | # Otherwise assume we can just set the text 54 | else: 55 | item.text = response.text 56 | 57 | item.set_meta("response", response) 58 | 59 | add_child(item) 60 | 61 | _configure_focus() 62 | 63 | 64 | func _ready() -> void: 65 | visibility_changed.connect(func(): 66 | if visible and get_menu_items().size() > 0: 67 | var first_item: Control = get_menu_items()[0] 68 | if first_item.is_inside_tree(): 69 | first_item.grab_focus() 70 | ) 71 | 72 | if is_instance_valid(response_template): 73 | response_template.hide() 74 | 75 | 76 | ## Get the selectable items in the menu. 77 | func get_menu_items() -> Array: 78 | var items: Array = [] 79 | for child in get_children(): 80 | if not child.visible: continue 81 | if "Disallowed" in child.name: continue 82 | items.append(child) 83 | 84 | return items 85 | 86 | 87 | #region Internal 88 | 89 | 90 | # Prepare the menu for keyboard and mouse navigation. 91 | func _configure_focus() -> void: 92 | var items = get_menu_items() 93 | for i in items.size(): 94 | var item: Control = items[i] 95 | 96 | item.focus_mode = Control.FOCUS_ALL 97 | 98 | item.focus_neighbor_left = item.get_path() 99 | item.focus_neighbor_right = item.get_path() 100 | 101 | if i == 0: 102 | item.focus_neighbor_top = item.get_path() 103 | item.focus_neighbor_left = item.get_path() 104 | item.focus_previous = item.get_path() 105 | else: 106 | item.focus_neighbor_top = items[i - 1].get_path() 107 | item.focus_neighbor_left = items[i - 1].get_path() 108 | item.focus_previous = items[i - 1].get_path() 109 | 110 | if i == items.size() - 1: 111 | item.focus_neighbor_bottom = item.get_path() 112 | item.focus_neighbor_right = item.get_path() 113 | item.focus_next = item.get_path() 114 | else: 115 | item.focus_neighbor_bottom = items[i + 1].get_path() 116 | item.focus_neighbor_right = items[i + 1].get_path() 117 | item.focus_next = items[i + 1].get_path() 118 | 119 | item.mouse_entered.connect(_on_response_mouse_entered.bind(item)) 120 | item.gui_input.connect(_on_response_gui_input.bind(item, item.get_meta("response"))) 121 | 122 | items[0].grab_focus() 123 | 124 | 125 | #endregion 126 | 127 | #region Signals 128 | 129 | 130 | func _on_response_mouse_entered(item: Control) -> void: 131 | if "Disallowed" in item.name: return 132 | 133 | item.grab_focus() 134 | 135 | 136 | func _on_response_gui_input(event: InputEvent, item: Control, response) -> void: 137 | if "Disallowed" in item.name: return 138 | 139 | if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: 140 | get_viewport().set_input_as_handled() 141 | response_selected.emit(response) 142 | elif event.is_action_pressed(&"ui_accept" if next_action.is_empty() else next_action) and item in get_menu_items(): 143 | get_viewport().set_input_as_handled() 144 | response_selected.emit(response) 145 | 146 | 147 | #endregion 148 | -------------------------------------------------------------------------------- /addons/dialogue_manager/dialogue_responses_menu.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bb52rsfwhkxbn 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/editor_translation_parser_plugin.gd: -------------------------------------------------------------------------------- 1 | class_name DMTranslationParserPlugin extends EditorTranslationParserPlugin 2 | 3 | 4 | ## Cached result of parsing a dialogue file. 5 | var data: DMCompilerResult 6 | ## List of characters that were added. 7 | var translated_character_names: PackedStringArray = [] 8 | var translated_lines: Array[Dictionary] = [] 9 | 10 | 11 | func _parse_file(path: String) -> Array[PackedStringArray]: 12 | var msgs: Array[PackedStringArray] = [] 13 | var file: FileAccess = FileAccess.open(path, FileAccess.READ) 14 | var text: String = file.get_as_text() 15 | 16 | data = DMCompiler.compile_string(text, path) 17 | 18 | var known_keys: PackedStringArray = PackedStringArray([]) 19 | 20 | # Add all character names if settings ask for it 21 | if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTERS_IN_TRANSLATABLE_STRINGS_LIST, true): 22 | translated_character_names = [] as Array[DialogueLine] 23 | for character_name: String in data.character_names: 24 | if character_name in known_keys: continue 25 | 26 | known_keys.append(character_name) 27 | 28 | translated_character_names.append(character_name) 29 | msgs.append(PackedStringArray([character_name.replace('"', '\"'), "dialogue", "", DMConstants.translate("translation_plugin.character_name")])) 30 | 31 | # Add all dialogue lines and responses 32 | var dialogue: Dictionary = data.lines 33 | for key: String in dialogue.keys(): 34 | var line: Dictionary = dialogue.get(key) 35 | 36 | if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue 37 | 38 | var translation_key: String = line.get(&"translation_key", line.text) 39 | 40 | if translation_key in known_keys: continue 41 | 42 | known_keys.append(translation_key) 43 | translated_lines.append(line) 44 | if translation_key == line.text: 45 | msgs.append(PackedStringArray([line.text.replace('"', '\"'), "", "", line.get("notes", "")])) 46 | else: 47 | msgs.append(PackedStringArray([line.text.replace('"', '\"'), line.translation_key.replace('"', '\"'), "", line.get("notes", "")])) 48 | 49 | return msgs 50 | 51 | 52 | func _get_recognized_extensions() -> PackedStringArray: 53 | return ["dialogue"] 54 | -------------------------------------------------------------------------------- /addons/dialogue_manager/editor_translation_parser_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c6bya881h1egb 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/example_balloon/ExampleBalloon.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using Godot.Collections; 3 | 4 | namespace DialogueManagerRuntime 5 | { 6 | public partial class ExampleBalloon : CanvasLayer 7 | { 8 | [Export] public string NextAction = "ui_accept"; 9 | [Export] public string SkipAction = "ui_cancel"; 10 | 11 | 12 | Control balloon; 13 | RichTextLabel characterLabel; 14 | RichTextLabel dialogueLabel; 15 | VBoxContainer responsesMenu; 16 | 17 | Resource resource; 18 | Array temporaryGameStates = new Array(); 19 | bool isWaitingForInput = false; 20 | bool willHideBalloon = false; 21 | 22 | DialogueLine dialogueLine; 23 | DialogueLine DialogueLine 24 | { 25 | get => dialogueLine; 26 | set 27 | { 28 | if (value == null) 29 | { 30 | QueueFree(); 31 | return; 32 | } 33 | 34 | dialogueLine = value; 35 | ApplyDialogueLine(); 36 | } 37 | } 38 | 39 | Timer MutationCooldown = new Timer(); 40 | 41 | 42 | public override void _Ready() 43 | { 44 | balloon = GetNode("%Balloon"); 45 | characterLabel = GetNode("%CharacterLabel"); 46 | dialogueLabel = GetNode("%DialogueLabel"); 47 | responsesMenu = GetNode("%ResponsesMenu"); 48 | 49 | balloon.Hide(); 50 | 51 | balloon.GuiInput += (@event) => 52 | { 53 | if ((bool)dialogueLabel.Get("is_typing")) 54 | { 55 | bool mouseWasClicked = @event is InputEventMouseButton && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left && @event.IsPressed(); 56 | bool skipButtonWasPressed = @event.IsActionPressed(SkipAction); 57 | if (mouseWasClicked || skipButtonWasPressed) 58 | { 59 | GetViewport().SetInputAsHandled(); 60 | dialogueLabel.Call("skip_typing"); 61 | return; 62 | } 63 | } 64 | 65 | if (!isWaitingForInput) return; 66 | if (dialogueLine.Responses.Count > 0) return; 67 | 68 | GetViewport().SetInputAsHandled(); 69 | 70 | if (@event is InputEventMouseButton && @event.IsPressed() && (@event as InputEventMouseButton).ButtonIndex == MouseButton.Left) 71 | { 72 | Next(dialogueLine.NextId); 73 | } 74 | else if (@event.IsActionPressed(NextAction) && GetViewport().GuiGetFocusOwner() == balloon) 75 | { 76 | Next(dialogueLine.NextId); 77 | } 78 | }; 79 | 80 | if (string.IsNullOrEmpty((string)responsesMenu.Get("next_action"))) 81 | { 82 | responsesMenu.Set("next_action", NextAction); 83 | } 84 | responsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => 85 | { 86 | Next(response.NextId); 87 | })); 88 | 89 | 90 | // Hide the balloon when a mutation is running 91 | MutationCooldown.Timeout += () => 92 | { 93 | if (willHideBalloon) 94 | { 95 | willHideBalloon = false; 96 | balloon.Hide(); 97 | } 98 | }; 99 | AddChild(MutationCooldown); 100 | 101 | DialogueManager.Mutated += OnMutated; 102 | } 103 | 104 | 105 | public override void _ExitTree() 106 | { 107 | DialogueManager.Mutated -= OnMutated; 108 | } 109 | 110 | 111 | public override void _UnhandledInput(InputEvent @event) 112 | { 113 | // Only the balloon is allowed to handle input while it's showing 114 | GetViewport().SetInputAsHandled(); 115 | } 116 | 117 | 118 | public override async void _Notification(int what) 119 | { 120 | // Detect a change of locale and update the current dialogue line to show the new language 121 | if (what == NotificationTranslationChanged && IsInstanceValid(dialogueLabel)) 122 | { 123 | float visibleRatio = dialogueLabel.VisibleRatio; 124 | DialogueLine = await DialogueManager.GetNextDialogueLine(resource, DialogueLine.Id, temporaryGameStates); 125 | if (visibleRatio < 1.0f) 126 | { 127 | dialogueLabel.Call("skip_typing"); 128 | } 129 | } 130 | } 131 | 132 | 133 | public async void Start(Resource dialogueResource, string title, Array extraGameStates = null) 134 | { 135 | temporaryGameStates = new Array { this } + (extraGameStates ?? new Array()); 136 | isWaitingForInput = false; 137 | resource = dialogueResource; 138 | 139 | DialogueLine = await DialogueManager.GetNextDialogueLine(resource, title, temporaryGameStates); 140 | } 141 | 142 | 143 | public async void Next(string nextId) 144 | { 145 | DialogueLine = await DialogueManager.GetNextDialogueLine(resource, nextId, temporaryGameStates); 146 | } 147 | 148 | 149 | #region Helpers 150 | 151 | 152 | private async void ApplyDialogueLine() 153 | { 154 | MutationCooldown.Stop(); 155 | 156 | isWaitingForInput = false; 157 | balloon.FocusMode = Control.FocusModeEnum.All; 158 | balloon.GrabFocus(); 159 | 160 | // Set up the character name 161 | characterLabel.Visible = !string.IsNullOrEmpty(dialogueLine.Character); 162 | characterLabel.Text = Tr(dialogueLine.Character, "dialogue"); 163 | 164 | // Set up the dialogue 165 | dialogueLabel.Hide(); 166 | dialogueLabel.Set("dialogue_line", dialogueLine); 167 | 168 | // Set up the responses 169 | responsesMenu.Hide(); 170 | responsesMenu.Set("responses", dialogueLine.Responses); 171 | 172 | // Type out the text 173 | balloon.Show(); 174 | willHideBalloon = false; 175 | dialogueLabel.Show(); 176 | if (!string.IsNullOrEmpty(dialogueLine.Text)) 177 | { 178 | dialogueLabel.Call("type_out"); 179 | await ToSignal(dialogueLabel, "finished_typing"); 180 | } 181 | 182 | // Wait for input 183 | if (dialogueLine.Responses.Count > 0) 184 | { 185 | balloon.FocusMode = Control.FocusModeEnum.None; 186 | responsesMenu.Show(); 187 | } 188 | else if (!string.IsNullOrEmpty(dialogueLine.Time)) 189 | { 190 | float time = 0f; 191 | if (!float.TryParse(dialogueLine.Time, out time)) 192 | { 193 | time = dialogueLine.Text.Length * 0.02f; 194 | } 195 | await ToSignal(GetTree().CreateTimer(time), "timeout"); 196 | Next(dialogueLine.NextId); 197 | } 198 | else 199 | { 200 | isWaitingForInput = true; 201 | balloon.FocusMode = Control.FocusModeEnum.All; 202 | balloon.GrabFocus(); 203 | } 204 | } 205 | 206 | 207 | #endregion 208 | 209 | 210 | #region signals 211 | 212 | 213 | private void OnMutated(Dictionary _mutation) 214 | { 215 | isWaitingForInput = false; 216 | willHideBalloon = true; 217 | MutationCooldown.Start(0.1f); 218 | } 219 | 220 | 221 | #endregion 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /addons/dialogue_manager/example_balloon/ExampleBalloon.cs.uid: -------------------------------------------------------------------------------- 1 | uid://5b3w40kwakl3 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/example_balloon/example_balloon.gd: -------------------------------------------------------------------------------- 1 | class_name DialogueManagerExampleBalloon extends CanvasLayer 2 | ## A basic dialogue balloon for use with Dialogue Manager. 3 | 4 | ## The action to use for advancing the dialogue 5 | @export var next_action: StringName = &"ui_accept" 6 | 7 | ## The action to use to skip typing the dialogue 8 | @export var skip_action: StringName = &"ui_cancel" 9 | 10 | ## The dialogue resource 11 | var resource: DialogueResource 12 | 13 | ## Temporary game states 14 | var temporary_game_states: Array = [] 15 | 16 | ## See if we are waiting for the player 17 | var is_waiting_for_input: bool = false 18 | 19 | ## See if we are running a long mutation and should hide the balloon 20 | var will_hide_balloon: bool = false 21 | 22 | ## A dictionary to store any ephemeral variables 23 | var locals: Dictionary = {} 24 | 25 | var _locale: String = TranslationServer.get_locale() 26 | 27 | ## The current line 28 | var dialogue_line: DialogueLine: 29 | set(value): 30 | if value: 31 | dialogue_line = value 32 | apply_dialogue_line() 33 | else: 34 | # The dialogue has finished so close the balloon 35 | queue_free() 36 | get: 37 | return dialogue_line 38 | 39 | ## A cooldown timer for delaying the balloon hide when encountering a mutation. 40 | var mutation_cooldown: Timer = Timer.new() 41 | 42 | ## The base balloon anchor 43 | @onready var balloon: Control = %Balloon 44 | 45 | ## The label showing the name of the currently speaking character 46 | @onready var character_label: RichTextLabel = %CharacterLabel 47 | 48 | ## The label showing the currently spoken dialogue 49 | @onready var dialogue_label: DialogueLabel = %DialogueLabel 50 | 51 | ## The menu of responses 52 | @onready var responses_menu: DialogueResponsesMenu = %ResponsesMenu 53 | 54 | 55 | func _ready() -> void: 56 | balloon.hide() 57 | Engine.get_singleton("DialogueManager").mutated.connect(_on_mutated) 58 | 59 | # If the responses menu doesn't have a next action set, use this one 60 | if responses_menu.next_action.is_empty(): 61 | responses_menu.next_action = next_action 62 | 63 | mutation_cooldown.timeout.connect(_on_mutation_cooldown_timeout) 64 | add_child(mutation_cooldown) 65 | 66 | 67 | func _unhandled_input(_event: InputEvent) -> void: 68 | # Only the balloon is allowed to handle input while it's showing 69 | get_viewport().set_input_as_handled() 70 | 71 | 72 | func _notification(what: int) -> void: 73 | ## Detect a change of locale and update the current dialogue line to show the new language 74 | if what == NOTIFICATION_TRANSLATION_CHANGED and _locale != TranslationServer.get_locale() and is_instance_valid(dialogue_label): 75 | _locale = TranslationServer.get_locale() 76 | var visible_ratio = dialogue_label.visible_ratio 77 | self.dialogue_line = await resource.get_next_dialogue_line(dialogue_line.id) 78 | if visible_ratio < 1: 79 | dialogue_label.skip_typing() 80 | 81 | 82 | ## Start some dialogue 83 | func start(dialogue_resource: DialogueResource, title: String, extra_game_states: Array = []) -> void: 84 | temporary_game_states = [self] + extra_game_states 85 | is_waiting_for_input = false 86 | resource = dialogue_resource 87 | self.dialogue_line = await resource.get_next_dialogue_line(title, temporary_game_states) 88 | 89 | 90 | ## Apply any changes to the balloon given a new [DialogueLine]. 91 | func apply_dialogue_line() -> void: 92 | mutation_cooldown.stop() 93 | 94 | is_waiting_for_input = false 95 | balloon.focus_mode = Control.FOCUS_ALL 96 | balloon.grab_focus() 97 | 98 | character_label.visible = not dialogue_line.character.is_empty() 99 | character_label.text = tr(dialogue_line.character, "dialogue") 100 | 101 | dialogue_label.hide() 102 | dialogue_label.dialogue_line = dialogue_line 103 | 104 | responses_menu.hide() 105 | responses_menu.responses = dialogue_line.responses 106 | 107 | # Show our balloon 108 | balloon.show() 109 | will_hide_balloon = false 110 | 111 | dialogue_label.show() 112 | if not dialogue_line.text.is_empty(): 113 | dialogue_label.type_out() 114 | await dialogue_label.finished_typing 115 | 116 | # Wait for input 117 | if dialogue_line.responses.size() > 0: 118 | balloon.focus_mode = Control.FOCUS_NONE 119 | responses_menu.show() 120 | elif dialogue_line.time != "": 121 | var time = dialogue_line.text.length() * 0.02 if dialogue_line.time == "auto" else dialogue_line.time.to_float() 122 | await get_tree().create_timer(time).timeout 123 | next(dialogue_line.next_id) 124 | else: 125 | is_waiting_for_input = true 126 | balloon.focus_mode = Control.FOCUS_ALL 127 | balloon.grab_focus() 128 | 129 | 130 | ## Go to the next line 131 | func next(next_id: String) -> void: 132 | self.dialogue_line = await resource.get_next_dialogue_line(next_id, temporary_game_states) 133 | 134 | 135 | #region Signals 136 | 137 | 138 | func _on_mutation_cooldown_timeout() -> void: 139 | if will_hide_balloon: 140 | will_hide_balloon = false 141 | balloon.hide() 142 | 143 | 144 | func _on_mutated(_mutation: Dictionary) -> void: 145 | is_waiting_for_input = false 146 | will_hide_balloon = true 147 | mutation_cooldown.start(0.1) 148 | 149 | 150 | func _on_balloon_gui_input(event: InputEvent) -> void: 151 | # See if we need to skip typing of the dialogue 152 | if dialogue_label.is_typing: 153 | var mouse_was_clicked: bool = event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() 154 | var skip_button_was_pressed: bool = event.is_action_pressed(skip_action) 155 | if mouse_was_clicked or skip_button_was_pressed: 156 | get_viewport().set_input_as_handled() 157 | dialogue_label.skip_typing() 158 | return 159 | 160 | if not is_waiting_for_input: return 161 | if dialogue_line.responses.size() > 0: return 162 | 163 | # When there are no response options the balloon itself is the clickable thing 164 | get_viewport().set_input_as_handled() 165 | 166 | if event is InputEventMouseButton and event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT: 167 | next(dialogue_line.next_id) 168 | elif event.is_action_pressed(next_action) and get_viewport().gui_get_focus_owner() == balloon: 169 | next(dialogue_line.next_id) 170 | 171 | 172 | func _on_responses_menu_response_selected(response: DialogueResponse) -> void: 173 | next(response.next_id) 174 | 175 | 176 | #endregion 177 | -------------------------------------------------------------------------------- /addons/dialogue_manager/example_balloon/example_balloon.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d1wt4ma6055l8 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/example_balloon/example_balloon.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://73jm5qjy52vq"] 2 | 3 | [ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_36de5"] 4 | [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_a8ve6"] 5 | [ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_72ixx"] 6 | 7 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_spyqn"] 8 | bg_color = Color(0, 0, 0, 1) 9 | border_width_left = 3 10 | border_width_top = 3 11 | border_width_right = 3 12 | border_width_bottom = 3 13 | border_color = Color(0.329412, 0.329412, 0.329412, 1) 14 | corner_radius_top_left = 5 15 | corner_radius_top_right = 5 16 | corner_radius_bottom_right = 5 17 | corner_radius_bottom_left = 5 18 | 19 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ri4m3"] 20 | bg_color = Color(0.121569, 0.121569, 0.121569, 1) 21 | border_width_left = 3 22 | border_width_top = 3 23 | border_width_right = 3 24 | border_width_bottom = 3 25 | border_color = Color(1, 1, 1, 1) 26 | corner_radius_top_left = 5 27 | corner_radius_top_right = 5 28 | corner_radius_bottom_right = 5 29 | corner_radius_bottom_left = 5 30 | 31 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e0njw"] 32 | bg_color = Color(0, 0, 0, 1) 33 | border_width_left = 3 34 | border_width_top = 3 35 | border_width_right = 3 36 | border_width_bottom = 3 37 | border_color = Color(0.6, 0.6, 0.6, 1) 38 | corner_radius_top_left = 5 39 | corner_radius_top_right = 5 40 | corner_radius_bottom_right = 5 41 | corner_radius_bottom_left = 5 42 | 43 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qkmqt"] 44 | bg_color = Color(0, 0, 0, 1) 45 | border_width_left = 3 46 | border_width_top = 3 47 | border_width_right = 3 48 | border_width_bottom = 3 49 | corner_radius_top_left = 5 50 | corner_radius_top_right = 5 51 | corner_radius_bottom_right = 5 52 | corner_radius_bottom_left = 5 53 | 54 | [sub_resource type="Theme" id="Theme_qq3yp"] 55 | default_font_size = 20 56 | Button/styles/disabled = SubResource("StyleBoxFlat_spyqn") 57 | Button/styles/focus = SubResource("StyleBoxFlat_ri4m3") 58 | Button/styles/hover = SubResource("StyleBoxFlat_e0njw") 59 | Button/styles/normal = SubResource("StyleBoxFlat_e0njw") 60 | MarginContainer/constants/margin_bottom = 15 61 | MarginContainer/constants/margin_left = 30 62 | MarginContainer/constants/margin_right = 30 63 | MarginContainer/constants/margin_top = 15 64 | PanelContainer/styles/panel = SubResource("StyleBoxFlat_qkmqt") 65 | 66 | [node name="ExampleBalloon" type="CanvasLayer"] 67 | layer = 100 68 | script = ExtResource("1_36de5") 69 | 70 | [node name="Balloon" type="Control" parent="."] 71 | unique_name_in_owner = true 72 | layout_mode = 3 73 | anchors_preset = 15 74 | anchor_right = 1.0 75 | anchor_bottom = 1.0 76 | grow_horizontal = 2 77 | grow_vertical = 2 78 | theme = SubResource("Theme_qq3yp") 79 | 80 | [node name="MarginContainer" type="MarginContainer" parent="Balloon"] 81 | layout_mode = 1 82 | anchors_preset = 12 83 | anchor_top = 1.0 84 | anchor_right = 1.0 85 | anchor_bottom = 1.0 86 | offset_top = -219.0 87 | grow_horizontal = 2 88 | grow_vertical = 0 89 | 90 | [node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] 91 | clip_children = 2 92 | layout_mode = 2 93 | mouse_filter = 1 94 | 95 | [node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] 96 | layout_mode = 2 97 | 98 | [node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] 99 | layout_mode = 2 100 | 101 | [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] 102 | unique_name_in_owner = true 103 | modulate = Color(1, 1, 1, 0.501961) 104 | layout_mode = 2 105 | mouse_filter = 1 106 | bbcode_enabled = true 107 | text = "Character" 108 | fit_content = true 109 | scroll_active = false 110 | 111 | [node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_a8ve6")] 112 | unique_name_in_owner = true 113 | layout_mode = 2 114 | size_flags_vertical = 3 115 | text = "Dialogue..." 116 | 117 | [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon" node_paths=PackedStringArray("response_template")] 118 | unique_name_in_owner = true 119 | layout_mode = 1 120 | anchors_preset = 8 121 | anchor_left = 0.5 122 | anchor_top = 0.5 123 | anchor_right = 0.5 124 | anchor_bottom = 0.5 125 | offset_left = -290.5 126 | offset_top = -35.0 127 | offset_right = 290.5 128 | offset_bottom = 35.0 129 | grow_horizontal = 2 130 | grow_vertical = 2 131 | size_flags_vertical = 8 132 | theme_override_constants/separation = 2 133 | alignment = 1 134 | script = ExtResource("3_72ixx") 135 | response_template = NodePath("ResponseExample") 136 | 137 | [node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] 138 | layout_mode = 2 139 | text = "Response example" 140 | 141 | [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] 142 | [connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] 143 | -------------------------------------------------------------------------------- /addons/dialogue_manager/example_balloon/small_example_balloon.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=10 format=3 uid="uid://13s5spsk34qu"] 2 | 3 | [ext_resource type="Script" uid="uid://d1wt4ma6055l8" path="res://addons/dialogue_manager/example_balloon/example_balloon.gd" id="1_s2gbs"] 4 | [ext_resource type="PackedScene" uid="uid://ckvgyvclnwggo" path="res://addons/dialogue_manager/dialogue_label.tscn" id="2_hfvdi"] 5 | [ext_resource type="Script" uid="uid://bb52rsfwhkxbn" path="res://addons/dialogue_manager/dialogue_responses_menu.gd" id="3_1j1j0"] 6 | 7 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_235ry"] 8 | content_margin_left = 6.0 9 | content_margin_top = 3.0 10 | content_margin_right = 6.0 11 | content_margin_bottom = 3.0 12 | bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) 13 | border_width_left = 1 14 | border_width_top = 1 15 | border_width_right = 1 16 | border_width_bottom = 1 17 | border_color = Color(0.345098, 0.345098, 0.345098, 1) 18 | corner_radius_top_left = 3 19 | corner_radius_top_right = 3 20 | corner_radius_bottom_right = 3 21 | corner_radius_bottom_left = 3 22 | 23 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ufjut"] 24 | content_margin_left = 6.0 25 | content_margin_top = 3.0 26 | content_margin_right = 6.0 27 | content_margin_bottom = 3.0 28 | bg_color = Color(0.227451, 0.227451, 0.227451, 1) 29 | border_width_left = 1 30 | border_width_top = 1 31 | border_width_right = 1 32 | border_width_bottom = 1 33 | border_color = Color(1, 1, 1, 1) 34 | corner_radius_top_left = 3 35 | corner_radius_top_right = 3 36 | corner_radius_bottom_right = 3 37 | corner_radius_bottom_left = 3 38 | 39 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fcbqo"] 40 | content_margin_left = 6.0 41 | content_margin_top = 3.0 42 | content_margin_right = 6.0 43 | content_margin_bottom = 3.0 44 | bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) 45 | border_width_left = 1 46 | border_width_top = 1 47 | border_width_right = 1 48 | border_width_bottom = 1 49 | corner_radius_top_left = 3 50 | corner_radius_top_right = 3 51 | corner_radius_bottom_right = 3 52 | corner_radius_bottom_left = 3 53 | 54 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t6i7a"] 55 | content_margin_left = 6.0 56 | content_margin_top = 3.0 57 | content_margin_right = 6.0 58 | content_margin_bottom = 3.0 59 | bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1) 60 | border_width_left = 1 61 | border_width_top = 1 62 | border_width_right = 1 63 | border_width_bottom = 1 64 | corner_radius_top_left = 3 65 | corner_radius_top_right = 3 66 | corner_radius_bottom_right = 3 67 | corner_radius_bottom_left = 3 68 | 69 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i6nbm"] 70 | bg_color = Color(0, 0, 0, 1) 71 | border_width_left = 1 72 | border_width_top = 1 73 | border_width_right = 1 74 | border_width_bottom = 1 75 | corner_radius_top_left = 3 76 | corner_radius_top_right = 3 77 | corner_radius_bottom_right = 3 78 | corner_radius_bottom_left = 3 79 | 80 | [sub_resource type="Theme" id="Theme_qq3yp"] 81 | default_font_size = 8 82 | Button/styles/disabled = SubResource("StyleBoxFlat_235ry") 83 | Button/styles/focus = SubResource("StyleBoxFlat_ufjut") 84 | Button/styles/hover = SubResource("StyleBoxFlat_fcbqo") 85 | Button/styles/normal = SubResource("StyleBoxFlat_t6i7a") 86 | MarginContainer/constants/margin_bottom = 4 87 | MarginContainer/constants/margin_left = 8 88 | MarginContainer/constants/margin_right = 8 89 | MarginContainer/constants/margin_top = 4 90 | PanelContainer/styles/panel = SubResource("StyleBoxFlat_i6nbm") 91 | 92 | [node name="ExampleBalloon" type="CanvasLayer"] 93 | layer = 100 94 | script = ExtResource("1_s2gbs") 95 | 96 | [node name="Balloon" type="Control" parent="."] 97 | unique_name_in_owner = true 98 | layout_mode = 3 99 | anchors_preset = 15 100 | anchor_right = 1.0 101 | anchor_bottom = 1.0 102 | grow_horizontal = 2 103 | grow_vertical = 2 104 | theme = SubResource("Theme_qq3yp") 105 | 106 | [node name="MarginContainer" type="MarginContainer" parent="Balloon"] 107 | layout_mode = 1 108 | anchors_preset = 12 109 | anchor_top = 1.0 110 | anchor_right = 1.0 111 | anchor_bottom = 1.0 112 | offset_top = -71.0 113 | grow_horizontal = 2 114 | grow_vertical = 0 115 | 116 | [node name="PanelContainer" type="PanelContainer" parent="Balloon/MarginContainer"] 117 | clip_children = 2 118 | layout_mode = 2 119 | mouse_filter = 1 120 | 121 | [node name="MarginContainer" type="MarginContainer" parent="Balloon/MarginContainer/PanelContainer"] 122 | layout_mode = 2 123 | 124 | [node name="VBoxContainer" type="VBoxContainer" parent="Balloon/MarginContainer/PanelContainer/MarginContainer"] 125 | layout_mode = 2 126 | 127 | [node name="CharacterLabel" type="RichTextLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer"] 128 | unique_name_in_owner = true 129 | modulate = Color(1, 1, 1, 0.501961) 130 | layout_mode = 2 131 | mouse_filter = 1 132 | bbcode_enabled = true 133 | text = "Character" 134 | fit_content = true 135 | scroll_active = false 136 | 137 | [node name="DialogueLabel" parent="Balloon/MarginContainer/PanelContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_hfvdi")] 138 | unique_name_in_owner = true 139 | layout_mode = 2 140 | size_flags_vertical = 3 141 | text = "Dialogue..." 142 | 143 | [node name="ResponsesMenu" type="VBoxContainer" parent="Balloon"] 144 | unique_name_in_owner = true 145 | layout_mode = 1 146 | anchors_preset = 8 147 | anchor_left = 0.5 148 | anchor_top = 0.5 149 | anchor_right = 0.5 150 | anchor_bottom = 0.5 151 | offset_left = -116.5 152 | offset_top = -9.0 153 | offset_right = 116.5 154 | offset_bottom = 9.0 155 | grow_horizontal = 2 156 | grow_vertical = 2 157 | size_flags_vertical = 8 158 | theme_override_constants/separation = 2 159 | script = ExtResource("3_1j1j0") 160 | 161 | [node name="ResponseExample" type="Button" parent="Balloon/ResponsesMenu"] 162 | layout_mode = 2 163 | text = "Response Example" 164 | 165 | [connection signal="gui_input" from="Balloon" to="." method="_on_balloon_gui_input"] 166 | [connection signal="response_selected" from="Balloon/ResponsesMenu" to="." method="_on_responses_menu_response_selected"] 167 | -------------------------------------------------------------------------------- /addons/dialogue_manager/export_plugin.gd: -------------------------------------------------------------------------------- 1 | class_name DMExportPlugin extends EditorExportPlugin 2 | 3 | const IGNORED_PATHS = [ 4 | "/assets", 5 | "/components", 6 | "/views", 7 | "inspector_plugin", 8 | "test_scene" 9 | ] 10 | 11 | 12 | func _get_name() -> String: 13 | return "Dialogue Manager Export Plugin" 14 | 15 | 16 | func _export_file(path: String, type: String, features: PackedStringArray) -> void: 17 | var plugin_path: String = Engine.get_meta("DialogueManagerPlugin").get_plugin_path() 18 | 19 | # Ignore any editor stuff 20 | for ignored_path: String in IGNORED_PATHS: 21 | if path.begins_with(plugin_path + ignored_path): 22 | skip() 23 | 24 | # Ignore C# stuff it not using dotnet 25 | if path.begins_with(plugin_path) and not DMSettings.check_for_dotnet_solution() and path.ends_with(".cs"): 26 | skip() 27 | -------------------------------------------------------------------------------- /addons/dialogue_manager/export_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://sa55ra11ji2q 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/import_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name DMImportPlugin extends EditorImportPlugin 3 | 4 | 5 | signal compiled_resource(resource: Resource) 6 | 7 | 8 | const COMPILER_VERSION = 15 9 | 10 | 11 | func _get_importer_name() -> String: 12 | return "dialogue_manager" 13 | 14 | 15 | func _get_format_version() -> int: 16 | return COMPILER_VERSION 17 | 18 | 19 | func _get_visible_name() -> String: 20 | return "Dialogue" 21 | 22 | 23 | func _get_import_order() -> int: 24 | return -1000 25 | 26 | 27 | func _get_priority() -> float: 28 | return 1000.0 29 | 30 | 31 | func _get_resource_type(): 32 | return "Resource" 33 | 34 | 35 | func _get_recognized_extensions() -> PackedStringArray: 36 | return PackedStringArray(["dialogue"]) 37 | 38 | 39 | func _get_save_extension(): 40 | return "tres" 41 | 42 | 43 | func _get_preset_count() -> int: 44 | return 0 45 | 46 | 47 | func _get_preset_name(preset_index: int) -> String: 48 | return "Unknown" 49 | 50 | 51 | func _get_import_options(path: String, preset_index: int) -> Array: 52 | # When the options array is empty there is a misleading error on export 53 | # that actually means nothing so let's just have an invisible option. 54 | return [{ 55 | name = "defaults", 56 | default_value = true 57 | }] 58 | 59 | 60 | func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool: 61 | return false 62 | 63 | 64 | func _import(source_file: String, save_path: String, options: Dictionary, platform_variants: Array[String], gen_files: Array[String]) -> Error: 65 | var cache = Engine.get_meta("DMCache") 66 | 67 | # Get the raw file contents 68 | if not FileAccess.file_exists(source_file): return ERR_FILE_NOT_FOUND 69 | 70 | var file: FileAccess = FileAccess.open(source_file, FileAccess.READ) 71 | var raw_text: String = file.get_as_text() 72 | 73 | cache.file_content_changed.emit(source_file, raw_text) 74 | 75 | # Compile the text 76 | var result: DMCompilerResult = DMCompiler.compile_string(raw_text, source_file) 77 | if result.errors.size() > 0: 78 | printerr("%d errors found in %s" % [result.errors.size(), source_file]) 79 | cache.add_errors_to_file(source_file, result.errors) 80 | return OK 81 | 82 | # Get the current addon version 83 | var config: ConfigFile = ConfigFile.new() 84 | config.load("res://addons/dialogue_manager/plugin.cfg") 85 | var version: String = config.get_value("plugin", "version") 86 | 87 | # Save the results to a resource 88 | var resource: DialogueResource = DialogueResource.new() 89 | resource.set_meta("dialogue_manager_version", version) 90 | 91 | resource.using_states = result.using_states 92 | resource.titles = result.titles 93 | resource.first_title = result.first_title 94 | resource.character_names = result.character_names 95 | resource.lines = result.lines 96 | resource.raw_text = result.raw_text 97 | 98 | # Clear errors and possibly trigger any cascade recompiles 99 | cache.add_file(source_file, result) 100 | 101 | var err: Error = ResourceSaver.save(resource, "%s.%s" % [save_path, _get_save_extension()]) 102 | 103 | compiled_resource.emit(resource) 104 | 105 | # Recompile any dependencies 106 | var dependent_paths: PackedStringArray = cache.get_dependent_paths_for_reimport(source_file) 107 | for path in dependent_paths: 108 | append_import_external_resource(path) 109 | 110 | return err 111 | -------------------------------------------------------------------------------- /addons/dialogue_manager/import_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dhwpj6ed8soyq 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/inspector_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name DMInspectorPlugin extends EditorInspectorPlugin 3 | 4 | 5 | const DialogueEditorProperty = preload("./components/editor_property/editor_property.gd") 6 | 7 | 8 | func _can_handle(object) -> bool: 9 | if object is GDScript: return false 10 | if not object is Node: return false 11 | if "name" in object and object.name == "Dialogue Manager": return false 12 | return true 13 | 14 | 15 | func _parse_property(object: Object, type, name: String, hint_type, hint_string: String, usage_flags: int, wide: bool) -> bool: 16 | if hint_string == "DialogueResource" or ("dialogue" in name.to_lower() and hint_string == "Resource"): 17 | var property_editor = DialogueEditorProperty.new() 18 | add_property_editor(name, property_editor) 19 | return true 20 | 21 | return false 22 | -------------------------------------------------------------------------------- /addons/dialogue_manager/inspector_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://0x31sbqbikov 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/l10n/translations.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Dialogue Manager\n" 4 | "MIME-Version: 1.0\n" 5 | "Content-Type: text/plain; charset=UTF-8\n" 6 | "Content-Transfer-Encoding: 8-bit\n" 7 | 8 | msgid "start_a_new_file" 9 | msgstr "" 10 | 11 | msgid "open_a_file" 12 | msgstr "" 13 | 14 | msgid "open.open" 15 | msgstr "" 16 | 17 | msgid "open.quick_open" 18 | msgstr "" 19 | 20 | msgid "open.no_recent_files" 21 | msgstr "" 22 | 23 | msgid "open.clear_recent_files" 24 | msgstr "" 25 | 26 | msgid "save_all_files" 27 | msgstr "" 28 | 29 | msgid "all" 30 | msgstr "" 31 | 32 | msgid "find_in_files" 33 | msgstr "" 34 | 35 | msgid "test_dialogue" 36 | msgstr "" 37 | 38 | msgid "test_dialogue_from_line" 39 | msgstr "" 40 | 41 | msgid "search_for_text" 42 | msgstr "" 43 | 44 | msgid "insert" 45 | msgstr "" 46 | 47 | msgid "translations" 48 | msgstr "" 49 | 50 | msgid "sponsor" 51 | msgstr "" 52 | 53 | msgid "show_support" 54 | msgstr "" 55 | 56 | msgid "docs" 57 | msgstr "" 58 | 59 | msgid "insert.wave_bbcode" 60 | msgstr "" 61 | 62 | msgid "insert.shake_bbcode" 63 | msgstr "" 64 | 65 | msgid "insert.typing_pause" 66 | msgstr "" 67 | 68 | msgid "insert.typing_speed_change" 69 | msgstr "" 70 | 71 | msgid "insert.auto_advance" 72 | msgstr "" 73 | 74 | msgid "insert.templates" 75 | msgstr "" 76 | 77 | msgid "insert.title" 78 | msgstr "" 79 | 80 | msgid "insert.dialogue" 81 | msgstr "" 82 | 83 | msgid "insert.response" 84 | msgstr "" 85 | 86 | msgid "insert.random_lines" 87 | msgstr "" 88 | 89 | msgid "insert.random_text" 90 | msgstr "" 91 | 92 | msgid "insert.actions" 93 | msgstr "" 94 | 95 | msgid "insert.jump" 96 | msgstr "" 97 | 98 | msgid "insert.end_dialogue" 99 | msgstr "" 100 | 101 | msgid "generate_line_ids" 102 | msgstr "" 103 | 104 | msgid "save_to_csv" 105 | msgstr "" 106 | 107 | msgid "import_from_csv" 108 | msgstr "" 109 | 110 | msgid "confirm_close" 111 | msgstr "" 112 | 113 | msgid "confirm_close.save" 114 | msgstr "" 115 | 116 | msgid "confirm_close.discard" 117 | msgstr "" 118 | 119 | msgid "buffer.save" 120 | msgstr "" 121 | 122 | msgid "buffer.save_as" 123 | msgstr "" 124 | 125 | msgid "buffer.close" 126 | msgstr "" 127 | 128 | msgid "buffer.close_all" 129 | msgstr "" 130 | 131 | msgid "buffer.close_other_files" 132 | msgstr "" 133 | 134 | msgid "buffer.copy_file_path" 135 | msgstr "" 136 | 137 | msgid "buffer.show_in_filesystem" 138 | msgstr "" 139 | 140 | msgid "n_of_n" 141 | msgstr "" 142 | 143 | msgid "search.find" 144 | msgstr "" 145 | 146 | msgid "search.find_all" 147 | msgstr "" 148 | 149 | msgid "search.placeholder" 150 | msgstr "" 151 | 152 | msgid "search.replace_placeholder" 153 | msgstr "" 154 | 155 | msgid "search.replace_selected" 156 | msgstr "" 157 | 158 | msgid "search.previous" 159 | msgstr "" 160 | 161 | msgid "search.next" 162 | msgstr "" 163 | 164 | msgid "search.match_case" 165 | msgstr "" 166 | 167 | msgid "search.toggle_replace" 168 | msgstr "" 169 | 170 | msgid "search.replace_with" 171 | msgstr "" 172 | 173 | msgid "search.replace" 174 | msgstr "" 175 | 176 | msgid "search.replace_all" 177 | msgstr "" 178 | 179 | msgid "files_list.filter" 180 | msgstr "" 181 | 182 | msgid "titles_list.filter" 183 | msgstr "" 184 | 185 | msgid "errors.key_not_found" 186 | msgstr "" 187 | 188 | msgid "errors.line_and_message" 189 | msgstr "" 190 | 191 | msgid "errors_in_script" 192 | msgstr "" 193 | 194 | msgid "errors_with_build" 195 | msgstr "" 196 | 197 | msgid "errors.import_errors" 198 | msgstr "" 199 | 200 | msgid "errors.already_imported" 201 | msgstr "" 202 | 203 | msgid "errors.duplicate_import" 204 | msgstr "" 205 | 206 | msgid "errors.unknown_using" 207 | msgstr "" 208 | 209 | msgid "errors.empty_title" 210 | msgstr "" 211 | 212 | msgid "errors.duplicate_title" 213 | msgstr "" 214 | 215 | msgid "errors.invalid_title_string" 216 | msgstr "" 217 | 218 | msgid "errors.invalid_title_number" 219 | msgstr "" 220 | 221 | msgid "errors.unknown_title" 222 | msgstr "" 223 | 224 | msgid "errors.jump_to_invalid_title" 225 | msgstr "" 226 | 227 | msgid "errors.title_has_no_content" 228 | msgstr "" 229 | 230 | msgid "errors.invalid_expression" 231 | msgstr "" 232 | 233 | msgid "errors.unexpected_condition" 234 | msgstr "" 235 | 236 | msgid "errors.duplicate_id" 237 | msgstr "" 238 | 239 | msgid "errors.missing_id" 240 | msgstr "" 241 | 242 | msgid "errors.invalid_indentation" 243 | msgstr "" 244 | 245 | msgid "errors.condition_has_no_content" 246 | msgstr "" 247 | 248 | msgid "errors.incomplete_expression" 249 | msgstr "" 250 | 251 | msgid "errors.invalid_expression_for_value" 252 | msgstr "" 253 | 254 | msgid "errors.file_not_found" 255 | msgstr "" 256 | 257 | msgid "errors.unexpected_end_of_expression" 258 | msgstr "" 259 | 260 | msgid "errors.unexpected_function" 261 | msgstr "" 262 | 263 | msgid "errors.unexpected_bracket" 264 | msgstr "" 265 | 266 | msgid "errors.unexpected_closing_bracket" 267 | msgstr "" 268 | 269 | msgid "errors.missing_closing_bracket" 270 | msgstr "" 271 | 272 | msgid "errors.unexpected_operator" 273 | msgstr "" 274 | 275 | msgid "errors.unexpected_comma" 276 | msgstr "" 277 | 278 | msgid "errors.unexpected_colon" 279 | msgstr "" 280 | 281 | msgid "errors.unexpected_dot" 282 | msgstr "" 283 | 284 | msgid "errors.unexpected_boolean" 285 | msgstr "" 286 | 287 | msgid "errors.unexpected_string" 288 | msgstr "" 289 | 290 | msgid "errors.unexpected_number" 291 | msgstr "" 292 | 293 | msgid "errors.unexpected_variable" 294 | msgstr "" 295 | 296 | msgid "errors.invalid_index" 297 | msgstr "" 298 | 299 | msgid "errors.unexpected_assignment" 300 | msgstr "" 301 | 302 | msgid "errors.expected_when_or_else" 303 | msgstr "" 304 | 305 | msgid "errors.only_one_else_allowed" 306 | msgstr "" 307 | 308 | msgid "errors.when_must_belong_to_match" 309 | msgstr "" 310 | 311 | msgid "errors.concurrent_line_without_origin" 312 | msgstr "" 313 | 314 | msgid "errors.goto_not_allowed_on_concurrect_lines" 315 | msgstr "" 316 | 317 | msgid "errors.unexpected_syntax_on_nested_dialogue_line" 318 | msgstr "" 319 | 320 | msgid "errors.err_nested_dialogue_invalid_jump" 321 | msgstr "" 322 | 323 | msgid "errors.unknown" 324 | msgstr "" 325 | 326 | msgid "update.available" 327 | msgstr "" 328 | 329 | msgid "update.is_available_for_download" 330 | msgstr "" 331 | 332 | msgid "update.downloading" 333 | msgstr "" 334 | 335 | msgid "update.download_update" 336 | msgstr "" 337 | 338 | msgid "update.needs_reload" 339 | msgstr "" 340 | 341 | msgid "update.reload_ok_button" 342 | msgstr "" 343 | 344 | msgid "update.reload_cancel_button" 345 | msgstr "" 346 | 347 | msgid "update.reload_project" 348 | msgstr "" 349 | 350 | msgid "update.release_notes" 351 | msgstr "" 352 | 353 | msgid "update.success" 354 | msgstr "" 355 | 356 | msgid "update.failed" 357 | msgstr "" 358 | 359 | msgid "runtime.no_resource" 360 | msgstr "" 361 | 362 | msgid "runtime.no_content" 363 | msgstr "" 364 | 365 | msgid "runtime.errors" 366 | msgstr "" 367 | 368 | msgid "runtime.error_detail" 369 | msgstr "" 370 | 371 | msgid "runtime.errors_see_details" 372 | msgstr "" 373 | 374 | msgid "runtime.invalid_expression" 375 | msgstr "" 376 | 377 | msgid "runtime.array_index_out_of_bounds" 378 | msgstr "" 379 | 380 | msgid "runtime.left_hand_size_cannot_be_assigned_to" 381 | msgstr "" 382 | 383 | msgid "runtime.key_not_found" 384 | msgstr "" 385 | 386 | msgid "runtime.property_not_found" 387 | msgstr "" 388 | 389 | msgid "runtime.property_not_found_missing_export" 390 | msgstr "" 391 | 392 | msgid "runtime.method_not_found" 393 | msgstr "" 394 | 395 | msgid "runtime.signal_not_found" 396 | msgstr "" 397 | 398 | msgid "runtime.method_not_callable" 399 | msgstr "" 400 | 401 | msgid "runtime.unknown_operator" 402 | msgstr "" 403 | 404 | msgid "runtime.unknown_autoload" 405 | msgstr "" 406 | 407 | msgid "runtime.something_went_wrong" 408 | msgstr "" 409 | 410 | msgid "runtime.expected_n_got_n_args" 411 | msgstr "" 412 | 413 | msgid "runtime.unsupported_array_type" 414 | msgstr "" 415 | 416 | msgid "runtime.dialogue_balloon_missing_start_method" 417 | msgstr "" 418 | 419 | msgid "runtime.top_level_states_share_name" 420 | msgstr "" 421 | 422 | msgid "translation_plugin.character_name" 423 | msgstr "" -------------------------------------------------------------------------------- /addons/dialogue_manager/l10n/zh.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Dialogue Manager\n" 4 | "POT-Creation-Date: \n" 5 | "PO-Revision-Date: \n" 6 | "Last-Translator: \n" 7 | "Language-Team: penghao123456、憨憨羊の宇航鸽鸽、ABShinri\n" 8 | "Language: zh\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 3.4\n" 13 | 14 | msgid "start_a_new_file" 15 | msgstr "创建新文件" 16 | 17 | msgid "open_a_file" 18 | msgstr "打开已有文件" 19 | 20 | msgid "open.open" 21 | msgstr "打开……" 22 | 23 | msgid "open.no_recent_files" 24 | msgstr "无历史记录" 25 | 26 | msgid "open.clear_recent_files" 27 | msgstr "清空历史记录" 28 | 29 | msgid "save_all_files" 30 | msgstr "保存所有文件" 31 | 32 | msgid "find_in_files" 33 | msgstr "在文件中查找" 34 | 35 | msgid "test_dialogue" 36 | msgstr "测试对话" 37 | 38 | msgid "search_for_text" 39 | msgstr "查找……" 40 | 41 | msgid "insert" 42 | msgstr "插入" 43 | 44 | msgid "translations" 45 | msgstr "翻译" 46 | 47 | msgid "show_support" 48 | msgstr "支持 Dialogue Manager" 49 | 50 | msgid "docs" 51 | msgstr "文档" 52 | 53 | msgid "insert.wave_bbcode" 54 | msgstr "波浪效果" 55 | 56 | msgid "insert.shake_bbcode" 57 | msgstr "抖动效果" 58 | 59 | msgid "insert.typing_pause" 60 | msgstr "输入间隔" 61 | 62 | msgid "insert.typing_speed_change" 63 | msgstr "输入速度变更" 64 | 65 | msgid "insert.auto_advance" 66 | msgstr "自动切行" 67 | 68 | msgid "insert.templates" 69 | msgstr "模板" 70 | 71 | msgid "insert.title" 72 | msgstr "标题" 73 | 74 | msgid "insert.dialogue" 75 | msgstr "对话" 76 | 77 | msgid "insert.response" 78 | msgstr "回复选项" 79 | 80 | msgid "insert.random_lines" 81 | msgstr "随机行" 82 | 83 | msgid "insert.random_text" 84 | msgstr "随机文本" 85 | 86 | msgid "insert.actions" 87 | msgstr "操作" 88 | 89 | msgid "insert.jump" 90 | msgstr "标题间跳转" 91 | 92 | msgid "insert.end_dialogue" 93 | msgstr "结束对话" 94 | 95 | msgid "generate_line_ids" 96 | msgstr "生成行 ID" 97 | 98 | msgid "save_characters_to_csv" 99 | msgstr "保存角色到 CSV" 100 | 101 | msgid "save_to_csv" 102 | msgstr "生成 CSV" 103 | 104 | msgid "import_from_csv" 105 | msgstr "从 CSV 导入" 106 | 107 | msgid "confirm_close" 108 | msgstr "是否要保存到“{path}”?" 109 | 110 | msgid "confirm_close.save" 111 | msgstr "保存" 112 | 113 | msgid "confirm_close.discard" 114 | msgstr "不保存" 115 | 116 | msgid "buffer.save" 117 | msgstr "保存" 118 | 119 | msgid "buffer.save_as" 120 | msgstr "另存为……" 121 | 122 | msgid "buffer.close" 123 | msgstr "关闭" 124 | 125 | msgid "buffer.close_all" 126 | msgstr "全部关闭" 127 | 128 | msgid "buffer.close_other_files" 129 | msgstr "关闭其他文件" 130 | 131 | msgid "buffer.copy_file_path" 132 | msgstr "复制文件路径" 133 | 134 | msgid "buffer.show_in_filesystem" 135 | msgstr "在 Godot 侧边栏中显示" 136 | 137 | msgid "n_of_n" 138 | msgstr "第{index}个,共{total}个" 139 | 140 | msgid "search.find" 141 | msgstr "查找:" 142 | 143 | msgid "search.find_all" 144 | msgstr "查找全部..." 145 | 146 | msgid "search.placeholder" 147 | msgstr "请输入查找的内容" 148 | 149 | msgid "search.replace_placeholder" 150 | msgstr "请输入替换的内容" 151 | 152 | msgid "search.replace_selected" 153 | msgstr "替换勾选" 154 | 155 | msgid "search.previous" 156 | msgstr "查找上一个" 157 | 158 | msgid "search.next" 159 | msgstr "查找下一个" 160 | 161 | msgid "search.match_case" 162 | msgstr "大小写敏感" 163 | 164 | msgid "search.toggle_replace" 165 | msgstr "替换" 166 | 167 | msgid "search.replace_with" 168 | msgstr "替换为" 169 | 170 | msgid "search.replace" 171 | msgstr "替换" 172 | 173 | msgid "search.replace_all" 174 | msgstr "全部替换" 175 | 176 | msgid "files_list.filter" 177 | msgstr "查找文件" 178 | 179 | msgid "titles_list.filter" 180 | msgstr "查找标题" 181 | 182 | msgid "errors.key_not_found" 183 | msgstr "键“{key}”未找到" 184 | 185 | msgid "errors.line_and_message" 186 | msgstr "第{line}行第{colume}列发生错误:{message}" 187 | 188 | msgid "errors_in_script" 189 | msgstr "你的脚本中存在错误。请修复错误,然后重试。" 190 | 191 | msgid "errors_with_build" 192 | msgstr "请先解决 Dialogue 中的错误。" 193 | 194 | msgid "errors.import_errors" 195 | msgstr "被导入的文件存在问题。" 196 | 197 | msgid "errors.already_imported" 198 | msgstr "文件已被导入。" 199 | 200 | msgid "errors.duplicate_import" 201 | msgstr "导入名不能重复。" 202 | 203 | msgid "errors.empty_title" 204 | msgstr "标题名不能为空。" 205 | 206 | msgid "errors.duplicate_title" 207 | msgstr "标题名不能重复。" 208 | 209 | msgid "errors.invalid_title_string" 210 | msgstr "标题名无效。" 211 | 212 | msgid "errors.invalid_title_number" 213 | msgstr "标题不能以数字开始。" 214 | 215 | msgid "errors.unknown_title" 216 | msgstr "标题未定义。" 217 | 218 | msgid "errors.jump_to_invalid_title" 219 | msgstr "标题名无效。" 220 | 221 | msgid "errors.title_has_no_content" 222 | msgstr "目标标题为空。请替换为“=> END”。" 223 | 224 | msgid "errors.invalid_expression" 225 | msgstr "表达式无效。" 226 | 227 | msgid "errors.unexpected_condition" 228 | msgstr "未知条件。" 229 | 230 | msgid "errors.duplicate_id" 231 | msgstr "ID 重复。" 232 | 233 | msgid "errors.missing_id" 234 | msgstr "ID 不存在。" 235 | 236 | msgid "errors.invalid_indentation" 237 | msgstr "缩进无效。" 238 | 239 | msgid "errors.condition_has_no_content" 240 | msgstr "条件下方不能为空。" 241 | 242 | msgid "errors.incomplete_expression" 243 | msgstr "不完整的表达式。" 244 | 245 | msgid "errors.invalid_expression_for_value" 246 | msgstr "无效的赋值表达式。" 247 | 248 | msgid "errors.file_not_found" 249 | msgstr "文件不存在。" 250 | 251 | msgid "errors.unexpected_end_of_expression" 252 | msgstr "表达式 end 不应存在。" 253 | 254 | msgid "errors.unexpected_function" 255 | msgstr "函数不应存在。" 256 | 257 | msgid "errors.unexpected_bracket" 258 | msgstr "方括号不应存在。" 259 | 260 | msgid "errors.unexpected_closing_bracket" 261 | msgstr "方括号不应存在。" 262 | 263 | msgid "errors.missing_closing_bracket" 264 | msgstr "闭方括号不存在。" 265 | 266 | msgid "errors.unexpected_operator" 267 | msgstr "操作符不应存在。" 268 | 269 | msgid "errors.unexpected_comma" 270 | msgstr "逗号不应存在。" 271 | 272 | msgid "errors.unexpected_colon" 273 | msgstr "冒号不应存在。" 274 | 275 | msgid "errors.unexpected_dot" 276 | msgstr "句号不应存在。" 277 | 278 | msgid "errors.unexpected_boolean" 279 | msgstr "布尔值不应存在。" 280 | 281 | msgid "errors.unexpected_string" 282 | msgstr "字符串不应存在。" 283 | 284 | msgid "errors.unexpected_number" 285 | msgstr "数字不应存在。" 286 | 287 | msgid "errors.unexpected_variable" 288 | msgstr "标识符不应存在。" 289 | 290 | msgid "errors.invalid_index" 291 | msgstr "索引无效。" 292 | 293 | msgid "errors.unexpected_assignment" 294 | msgstr "不应在条件判断中使用 = ,应使用 == 。" 295 | 296 | msgid "errors.unknown" 297 | msgstr "语法错误。" 298 | 299 | msgid "update.available" 300 | msgstr "v{version} 更新可用。" 301 | 302 | msgid "update.is_available_for_download" 303 | msgstr "v%s 已经可以下载。" 304 | 305 | msgid "update.downloading" 306 | msgstr "正在下载更新……" 307 | 308 | msgid "update.download_update" 309 | msgstr "下载" 310 | 311 | msgid "update.needs_reload" 312 | msgstr "需要重新加载项目以应用更新。" 313 | 314 | msgid "update.reload_ok_button" 315 | msgstr "重新加载" 316 | 317 | msgid "update.reload_cancel_button" 318 | msgstr "暂不重新加载" 319 | 320 | msgid "update.reload_project" 321 | msgstr "重新加载" 322 | 323 | msgid "update.release_notes" 324 | msgstr "查看发行注记" 325 | 326 | msgid "update.success" 327 | msgstr "v{version} 已成功安装并应用。" 328 | 329 | msgid "update.failed" 330 | msgstr "更新失败。" 331 | 332 | msgid "runtime.no_resource" 333 | msgstr "找不到资源。" 334 | 335 | msgid "runtime.no_content" 336 | msgstr "资源“{file_path}”为空。" 337 | 338 | msgid "runtime.errors" 339 | msgstr "文件中存在{errrors}个错误。" 340 | 341 | msgid "runtime.error_detail" 342 | msgstr "第{index}行:{message}" 343 | 344 | msgid "runtime.errors_see_details" 345 | msgstr "文件中存在{errrors}个错误。请查看调试输出。" 346 | 347 | msgid "runtime.invalid_expression" 348 | msgstr "表达式“{expression}”无效:{error}" 349 | 350 | msgid "runtime.array_index_out_of_bounds" 351 | msgstr "数组索引“{index}”越界。(数组名:“{array}”)" 352 | 353 | msgid "runtime.left_hand_size_cannot_be_assigned_to" 354 | msgstr "表达式左侧的变量无法被赋值。" 355 | 356 | msgid "runtime.key_not_found" 357 | msgstr "键“{key}”在字典“{dictionary}”中不存在。" 358 | 359 | msgid "runtime.property_not_found" 360 | msgstr "“{property}”不存在。(全局变量:{states})" 361 | 362 | msgid "runtime.property_not_found_missing_export" 363 | msgstr "“{property}”不存在。(全局变量:{states})你可能需要添加一个修饰词 [Export]。" 364 | 365 | msgid "runtime.method_not_found" 366 | msgstr "“{method}”不存在。(全局变量:{states})" 367 | 368 | msgid "runtime.signal_not_found" 369 | msgstr "“{sighal_name}”不存在。(全局变量:{states})" 370 | 371 | msgid "runtime.method_not_callable" 372 | msgstr "{method}不是对象“{object}”上的函数。" 373 | 374 | msgid "runtime.unknown_operator" 375 | msgstr "未知操作符。" 376 | 377 | msgid "runtime.something_went_wrong" 378 | msgstr "有什么出错了。" 379 | -------------------------------------------------------------------------------- /addons/dialogue_manager/l10n/zh_TW.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Dialogue Manager\n" 4 | "POT-Creation-Date: \n" 5 | "PO-Revision-Date: \n" 6 | "Last-Translator: \n" 7 | "Language-Team: 憨憨羊の宇航鴿鴿、ABShinri\n" 8 | "Language: zh_TW\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 3.4\n" 13 | 14 | msgid "start_a_new_file" 15 | msgstr "創建新檔案" 16 | 17 | msgid "open_a_file" 18 | msgstr "開啟已有檔案" 19 | 20 | msgid "open.open" 21 | msgstr "開啟……" 22 | 23 | msgid "open.no_recent_files" 24 | msgstr "無歷史記錄" 25 | 26 | msgid "open.clear_recent_files" 27 | msgstr "清空歷史記錄" 28 | 29 | msgid "save_all_files" 30 | msgstr "儲存所有檔案" 31 | 32 | msgid "find_in_files" 33 | msgstr "在檔案中查找" 34 | 35 | msgid "test_dialogue" 36 | msgstr "測試對話" 37 | 38 | msgid "search_for_text" 39 | msgstr "搜尋……" 40 | 41 | msgid "insert" 42 | msgstr "插入" 43 | 44 | msgid "translations" 45 | msgstr "翻譯" 46 | 47 | msgid "show_support" 48 | msgstr "支援 Dialogue Manager" 49 | 50 | msgid "docs" 51 | msgstr "文檔" 52 | 53 | msgid "insert.wave_bbcode" 54 | msgstr "波浪特效" 55 | 56 | msgid "insert.shake_bbcode" 57 | msgstr "震動特效" 58 | 59 | msgid "insert.typing_pause" 60 | msgstr "輸入間隔" 61 | 62 | msgid "insert.typing_speed_change" 63 | msgstr "輸入速度變更" 64 | 65 | msgid "insert.auto_advance" 66 | msgstr "自動切行" 67 | 68 | msgid "insert.templates" 69 | msgstr "模板" 70 | 71 | msgid "insert.title" 72 | msgstr "標題" 73 | 74 | msgid "insert.dialogue" 75 | msgstr "對話" 76 | 77 | msgid "insert.response" 78 | msgstr "回覆選項" 79 | 80 | msgid "insert.random_lines" 81 | msgstr "隨機行" 82 | 83 | msgid "insert.random_text" 84 | msgstr "隨機文本" 85 | 86 | msgid "insert.actions" 87 | msgstr "操作" 88 | 89 | msgid "insert.jump" 90 | msgstr "標題間跳轉" 91 | 92 | msgid "insert.end_dialogue" 93 | msgstr "結束對話" 94 | 95 | msgid "generate_line_ids" 96 | msgstr "生成行 ID" 97 | 98 | msgid "save_characters_to_csv" 99 | msgstr "保存角色到 CSV" 100 | 101 | msgid "save_to_csv" 102 | msgstr "生成 CSV" 103 | 104 | msgid "import_from_csv" 105 | msgstr "從 CSV 匯入" 106 | 107 | msgid "confirm_close" 108 | msgstr "是否要儲存到“{path}”?" 109 | 110 | msgid "confirm_close.save" 111 | msgstr "儲存" 112 | 113 | msgid "confirm_close.discard" 114 | msgstr "不儲存" 115 | 116 | msgid "buffer.save" 117 | msgstr "儲存" 118 | 119 | msgid "buffer.save_as" 120 | msgstr "儲存爲……" 121 | 122 | msgid "buffer.close" 123 | msgstr "關閉" 124 | 125 | msgid "buffer.close_all" 126 | msgstr "全部關閉" 127 | 128 | msgid "buffer.close_other_files" 129 | msgstr "關閉其他檔案" 130 | 131 | msgid "buffer.copy_file_path" 132 | msgstr "複製檔案位置" 133 | 134 | msgid "buffer.show_in_filesystem" 135 | msgstr "在 Godot 側邊欄中顯示" 136 | 137 | msgid "n_of_n" 138 | msgstr "第{index}個,共{total}個" 139 | 140 | msgid "search.find" 141 | msgstr "搜尋:" 142 | 143 | msgid "search.find_all" 144 | msgstr "搜尋全部..." 145 | 146 | msgid "search.placeholder" 147 | msgstr "請輸入搜尋的內容" 148 | 149 | msgid "search.replace_placeholder" 150 | msgstr "請輸入替換的內容" 151 | 152 | msgid "search.replace_selected" 153 | msgstr "替換勾選" 154 | 155 | msgid "search.previous" 156 | msgstr "搜尋上一個" 157 | 158 | msgid "search.next" 159 | msgstr "搜尋下一個" 160 | 161 | msgid "search.match_case" 162 | msgstr "大小寫敏感" 163 | 164 | msgid "search.toggle_replace" 165 | msgstr "替換" 166 | 167 | msgid "search.replace_with" 168 | msgstr "替換爲" 169 | 170 | msgid "search.replace" 171 | msgstr "替換" 172 | 173 | msgid "search.replace_all" 174 | msgstr "全部替換" 175 | 176 | msgid "files_list.filter" 177 | msgstr "搜尋檔案" 178 | 179 | msgid "titles_list.filter" 180 | msgstr "搜尋標題" 181 | 182 | msgid "errors.key_not_found" 183 | msgstr "鍵“{key}”未找到" 184 | 185 | msgid "errors.line_and_message" 186 | msgstr "第{line}行第{colume}列發生錯誤:{message}" 187 | 188 | msgid "errors_in_script" 189 | msgstr "你的腳本中存在錯誤。請修復錯誤,然後重試。" 190 | 191 | msgid "errors_with_build" 192 | msgstr "請先解決 Dialogue 中的錯誤。" 193 | 194 | msgid "errors.import_errors" 195 | msgstr "被匯入的檔案存在問題。" 196 | 197 | msgid "errors.already_imported" 198 | msgstr "檔案已被匯入。" 199 | 200 | msgid "errors.duplicate_import" 201 | msgstr "匯入名不能重複。" 202 | 203 | msgid "errors.empty_title" 204 | msgstr "標題名不能爲空。" 205 | 206 | msgid "errors.duplicate_title" 207 | msgstr "標題名不能重複。" 208 | 209 | msgid "errors.invalid_title_string" 210 | msgstr "標題名無效。" 211 | 212 | msgid "errors.invalid_title_number" 213 | msgstr "標題不能以數字開始。" 214 | 215 | msgid "errors.unknown_title" 216 | msgstr "標題未定義。" 217 | 218 | msgid "errors.jump_to_invalid_title" 219 | msgstr "標題名無效。" 220 | 221 | msgid "errors.title_has_no_content" 222 | msgstr "目標標題爲空。請替換爲“=> END”。" 223 | 224 | msgid "errors.invalid_expression" 225 | msgstr "表達式無效。" 226 | 227 | msgid "errors.unexpected_condition" 228 | msgstr "未知條件。" 229 | 230 | msgid "errors.duplicate_id" 231 | msgstr "ID 重複。" 232 | 233 | msgid "errors.missing_id" 234 | msgstr "ID 不存在。" 235 | 236 | msgid "errors.invalid_indentation" 237 | msgstr "縮進無效。" 238 | 239 | msgid "errors.condition_has_no_content" 240 | msgstr "條件下方不能爲空。" 241 | 242 | msgid "errors.incomplete_expression" 243 | msgstr "不完整的表達式。" 244 | 245 | msgid "errors.invalid_expression_for_value" 246 | msgstr "無效的賦值表達式。" 247 | 248 | msgid "errors.file_not_found" 249 | msgstr "檔案不存在。" 250 | 251 | msgid "errors.unexpected_end_of_expression" 252 | msgstr "表達式 end 不應存在。" 253 | 254 | msgid "errors.unexpected_function" 255 | msgstr "函數不應存在。" 256 | 257 | msgid "errors.unexpected_bracket" 258 | msgstr "方括號不應存在。" 259 | 260 | msgid "errors.unexpected_closing_bracket" 261 | msgstr "方括號不應存在。" 262 | 263 | msgid "errors.missing_closing_bracket" 264 | msgstr "閉方括號不存在。" 265 | 266 | msgid "errors.unexpected_operator" 267 | msgstr "操作符不應存在。" 268 | 269 | msgid "errors.unexpected_comma" 270 | msgstr "逗號不應存在。" 271 | 272 | msgid "errors.unexpected_colon" 273 | msgstr "冒號不應存在。" 274 | 275 | msgid "errors.unexpected_dot" 276 | msgstr "句號不應存在。" 277 | 278 | msgid "errors.unexpected_boolean" 279 | msgstr "布爾值不應存在。" 280 | 281 | msgid "errors.unexpected_string" 282 | msgstr "字符串不應存在。" 283 | 284 | msgid "errors.unexpected_number" 285 | msgstr "數字不應存在。" 286 | 287 | msgid "errors.unexpected_variable" 288 | msgstr "標識符不應存在。" 289 | 290 | msgid "errors.invalid_index" 291 | msgstr "索引無效。" 292 | 293 | msgid "errors.unexpected_assignment" 294 | msgstr "不應在條件判斷中使用 = ,應使用 == 。" 295 | 296 | msgid "errors.unknown" 297 | msgstr "語法錯誤。" 298 | 299 | msgid "update.available" 300 | msgstr "v{version} 更新可用。" 301 | 302 | msgid "update.is_available_for_download" 303 | msgstr "v%s 已經可以下載。" 304 | 305 | msgid "update.downloading" 306 | msgstr "正在下載更新……" 307 | 308 | msgid "update.download_update" 309 | msgstr "下載" 310 | 311 | msgid "update.needs_reload" 312 | msgstr "需要重新加載項目以套用更新。" 313 | 314 | msgid "update.reload_ok_button" 315 | msgstr "重新加載" 316 | 317 | msgid "update.reload_cancel_button" 318 | msgstr "暫不重新加載" 319 | 320 | msgid "update.reload_project" 321 | msgstr "重新加載" 322 | 323 | msgid "update.release_notes" 324 | msgstr "查看發行註記" 325 | 326 | msgid "update.success" 327 | msgstr "v{version} 已成功安裝並套用。" 328 | 329 | msgid "update.failed" 330 | msgstr "更新失敗。" 331 | 332 | msgid "runtime.no_resource" 333 | msgstr "找不到資源。" 334 | 335 | msgid "runtime.no_content" 336 | msgstr "資源“{file_path}”爲空。" 337 | 338 | msgid "runtime.errors" 339 | msgstr "檔案中存在{errrors}個錯誤。" 340 | 341 | msgid "runtime.error_detail" 342 | msgstr "第{index}行:{message}" 343 | 344 | msgid "runtime.errors_see_details" 345 | msgstr "檔案中存在{errrors}個錯誤。請查看調試輸出。" 346 | 347 | msgid "runtime.invalid_expression" 348 | msgstr "表達式“{expression}”無效:{error}" 349 | 350 | msgid "runtime.array_index_out_of_bounds" 351 | msgstr "數組索引“{index}”越界。(數組名:“{array}”)" 352 | 353 | msgid "runtime.left_hand_size_cannot_be_assigned_to" 354 | msgstr "表達式左側的變量無法被賦值。" 355 | 356 | msgid "runtime.key_not_found" 357 | msgstr "鍵“{key}”在字典“{dictionary}”中不存在。" 358 | 359 | msgid "runtime.property_not_found" 360 | msgstr "“{property}”不存在。(全局變量:{states})" 361 | 362 | msgid "runtime.method_not_found" 363 | msgstr "“{method}”不存在。(全局變量:{states})" 364 | 365 | msgid "runtime.signal_not_found" 366 | msgstr "“{sighal_name}”不存在。(全局變量:{states})" 367 | 368 | msgid "runtime.property_not_found_missing_export" 369 | msgstr "“{property}”不存在。(全局變量:{states})你可能需要添加一個修飾詞 [Export]。" 370 | 371 | msgid "runtime.method_not_callable" 372 | msgstr "{method}不是對象“{object}”上的函數。" 373 | 374 | msgid "runtime.unknown_operator" 375 | msgstr "未知操作符。" 376 | 377 | msgid "runtime.something_went_wrong" 378 | msgstr "有什麼出錯了。" 379 | -------------------------------------------------------------------------------- /addons/dialogue_manager/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Dialogue Manager" 4 | description="A powerful nonlinear dialogue system" 5 | author="Nathan Hoad" 6 | version="3.6.3" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/dialogue_manager/plugin.cfg.uid: -------------------------------------------------------------------------------- 1 | uid://hrny2utekhei 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bpv426rpvrafa 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/settings.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ce1nk88365m52 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/test_scene.gd: -------------------------------------------------------------------------------- 1 | class_name BaseDialogueTestScene extends Node2D 2 | 3 | 4 | const DialogueSettings = preload("./settings.gd") 5 | const DialogueResource = preload("./dialogue_resource.gd") 6 | 7 | 8 | @onready var title: String = DialogueSettings.get_user_value("run_title") 9 | @onready var resource: DialogueResource = load(DialogueSettings.get_user_value("run_resource_path")) 10 | 11 | 12 | func _ready(): 13 | if not Engine.is_embedded_in_editor: 14 | var window: Window = get_viewport() 15 | var screen_index: int = DisplayServer.get_primary_screen() 16 | window.position = Vector2(DisplayServer.screen_get_position(screen_index)) + (DisplayServer.screen_get_size(screen_index) - window.size) * 0.5 17 | window.mode = Window.MODE_WINDOWED 18 | 19 | # Normally you can just call DialogueManager directly but doing so before the plugin has been 20 | # enabled in settings will throw a compiler error here so I'm using `get_singleton` instead. 21 | var dialogue_manager = Engine.get_singleton("DialogueManager") 22 | dialogue_manager.dialogue_ended.connect(_on_dialogue_ended) 23 | dialogue_manager.show_dialogue_balloon(resource, title if not title.is_empty() else resource.first_title) 24 | 25 | 26 | func _enter_tree() -> void: 27 | DialogueSettings.set_user_value("is_running_test_scene", false) 28 | 29 | 30 | #region Signals 31 | 32 | 33 | func _on_dialogue_ended(_resource: DialogueResource): 34 | get_tree().quit() 35 | 36 | 37 | #endregion 38 | -------------------------------------------------------------------------------- /addons/dialogue_manager/test_scene.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c8e16qdgu40wo 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/test_scene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://ugd552efvil0"] 2 | 3 | [ext_resource type="Script" uid="uid://c8e16qdgu40wo" path="res://addons/dialogue_manager/test_scene.gd" id="1_yupoh"] 4 | 5 | [node name="TestScene" type="Node2D"] 6 | script = ExtResource("1_yupoh") 7 | -------------------------------------------------------------------------------- /addons/dialogue_manager/utilities/builtins.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bnfhuubdv5k20 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/utilities/dialogue_cache.gd: -------------------------------------------------------------------------------- 1 | class_name DMCache extends Node 2 | 3 | 4 | signal file_content_changed(path: String, new_content: String) 5 | 6 | 7 | # Keep track of errors and dependencies 8 | # { 9 | # = { 10 | # path = , 11 | # dependencies = [, ], 12 | # errors = [, ] 13 | # } 14 | # } 15 | var _cache: Dictionary = {} 16 | 17 | var _update_dependency_timer: Timer = Timer.new() 18 | var _update_dependency_paths: PackedStringArray = [] 19 | 20 | var _files_marked_for_reimport: PackedStringArray = [] 21 | 22 | 23 | func _ready() -> void: 24 | add_child(_update_dependency_timer) 25 | _update_dependency_timer.timeout.connect(_on_update_dependency_timeout) 26 | 27 | _build_cache() 28 | 29 | 30 | func mark_files_for_reimport(files: PackedStringArray) -> void: 31 | for file in files: 32 | if not _files_marked_for_reimport.has(file): 33 | _files_marked_for_reimport.append(file) 34 | 35 | 36 | func reimport_files(and_files: PackedStringArray = []) -> void: 37 | for file in and_files: 38 | if not _files_marked_for_reimport.has(file): 39 | _files_marked_for_reimport.append(file) 40 | 41 | if _files_marked_for_reimport.is_empty(): return 42 | 43 | EditorInterface.get_resource_filesystem().reimport_files(_files_marked_for_reimport) 44 | _files_marked_for_reimport.clear() 45 | 46 | 47 | ## Add a dialogue file to the cache. 48 | func add_file(path: String, compile_result: DMCompilerResult = null) -> void: 49 | _cache[path] = { 50 | path = path, 51 | dependencies = [], 52 | errors = [] 53 | } 54 | 55 | if compile_result != null: 56 | _cache[path].dependencies = Array(compile_result.imported_paths).filter(func(d): return d != path) 57 | _cache[path].compiled_at = Time.get_ticks_msec() 58 | 59 | # If this is a fresh cache entry, check for dependencies 60 | if compile_result == null and not _update_dependency_paths.has(path): 61 | queue_updating_dependencies(path) 62 | 63 | 64 | ## Get the file paths in the cache 65 | func get_files() -> PackedStringArray: 66 | return _cache.keys() 67 | 68 | 69 | ## Check if a file is known to the cache 70 | func has_file(path: String) -> bool: 71 | return _cache.has(path) 72 | 73 | 74 | ## Remember any errors in a dialogue file 75 | func add_errors_to_file(path: String, errors: Array[Dictionary]) -> void: 76 | if _cache.has(path): 77 | _cache[path].errors = errors 78 | else: 79 | _cache[path] = { 80 | path = path, 81 | resource_path = "", 82 | dependencies = [], 83 | errors = errors 84 | } 85 | 86 | 87 | ## Get a list of files that have errors 88 | func get_files_with_errors() -> Array[Dictionary]: 89 | var files_with_errors: Array[Dictionary] = [] 90 | for dialogue_file in _cache.values(): 91 | if dialogue_file and dialogue_file.errors.size() > 0: 92 | files_with_errors.append(dialogue_file) 93 | return files_with_errors 94 | 95 | 96 | ## Queue a file to have its dependencies checked 97 | func queue_updating_dependencies(of_path: String) -> void: 98 | _update_dependency_timer.stop() 99 | if not _update_dependency_paths.has(of_path): 100 | _update_dependency_paths.append(of_path) 101 | _update_dependency_timer.start(0.5) 102 | 103 | 104 | ## Update any references to a file path that has moved 105 | func move_file_path(from_path: String, to_path: String) -> void: 106 | if not _cache.has(from_path): return 107 | 108 | if to_path != "": 109 | _cache[to_path] = _cache[from_path].duplicate() 110 | _cache.erase(from_path) 111 | 112 | 113 | ## Get every dialogue file that imports on a file of a given path 114 | func get_files_with_dependency(imported_path: String) -> Array: 115 | return _cache.values().filter(func(d): return d.dependencies.has(imported_path)) 116 | 117 | 118 | ## Get any paths that are dependent on a given path 119 | func get_dependent_paths_for_reimport(on_path: String) -> PackedStringArray: 120 | return get_files_with_dependency(on_path) \ 121 | .filter(func(d): return Time.get_ticks_msec() - d.get("compiled_at", 0) > 3000) \ 122 | .map(func(d): return d.path) 123 | 124 | 125 | # Build the initial cache for dialogue files 126 | func _build_cache() -> void: 127 | var current_files: PackedStringArray = _get_dialogue_files_in_filesystem() 128 | for file in current_files: 129 | add_file(file) 130 | 131 | 132 | # Recursively find any dialogue files in a directory 133 | func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray: 134 | var files: PackedStringArray = [] 135 | 136 | if DirAccess.dir_exists_absolute(path): 137 | var dir = DirAccess.open(path) 138 | dir.list_dir_begin() 139 | var file_name = dir.get_next() 140 | while file_name != "": 141 | var file_path: String = (path + "/" + file_name).simplify_path() 142 | if dir.current_is_dir(): 143 | if not file_name in [".godot", ".tmp"]: 144 | files.append_array(_get_dialogue_files_in_filesystem(file_path)) 145 | elif file_name.get_extension() == "dialogue": 146 | files.append(file_path) 147 | file_name = dir.get_next() 148 | 149 | return files 150 | 151 | 152 | #region Signals 153 | 154 | 155 | func _on_update_dependency_timeout() -> void: 156 | _update_dependency_timer.stop() 157 | var import_regex: RegEx = RegEx.create_from_string("import \"(?.*?)\"") 158 | var file: FileAccess 159 | var found_imports: Array[RegExMatch] 160 | for path in _update_dependency_paths: 161 | # Open the file and check for any "import" lines 162 | file = FileAccess.open(path, FileAccess.READ) 163 | found_imports = import_regex.search_all(file.get_as_text()) 164 | var dependencies: PackedStringArray = [] 165 | for found in found_imports: 166 | dependencies.append(found.strings[found.names.path]) 167 | _cache[path].dependencies = dependencies 168 | _update_dependency_paths.clear() 169 | 170 | 171 | #endregion 172 | -------------------------------------------------------------------------------- /addons/dialogue_manager/utilities/dialogue_cache.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d3c83yd6bjp43 2 | -------------------------------------------------------------------------------- /addons/dialogue_manager/views/main_view.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cipjcc7bkh1pc 2 | --------------------------------------------------------------------------------