@tool extends EditorPlugin const DialogueConstants = preload("res://addons/dialogue_manager/constants.gd") const DialogueImportPlugin = preload("res://addons/dialogue_manager/import_plugin.gd") const DialogueTranslationParserPlugin = preload("res://addons/dialogue_manager/editor_translation_parser_plugin.gd") const DialogueSettings = preload("res://addons/dialogue_manager/components/settings.gd") const MainView = preload("res://addons/dialogue_manager/views/main_view.tscn") var import_plugin: DialogueImportPlugin var translation_parser_plugin: DialogueTranslationParserPlugin var main_view var dialogue_file_cache: Dictionary = {} func _enter_tree() -> void: add_autoload_singleton("DialogueManager", "res://addons/dialogue_manager/dialogue_manager.gd") add_custom_type("DialogueLabel", "RichTextLabel", preload("res://addons/dialogue_manager/dialogue_label.gd"), _get_plugin_icon()) if Engine.is_editor_hint(): DialogueSettings.prepare() import_plugin = DialogueImportPlugin.new() import_plugin.editor_plugin = self add_import_plugin(import_plugin) translation_parser_plugin = DialogueTranslationParserPlugin.new() add_translation_parser_plugin(translation_parser_plugin) main_view = MainView.instantiate() main_view.editor_plugin = self get_editor_interface().get_editor_main_screen().add_child(main_view) _make_visible(false) update_dialogue_file_cache() get_editor_interface().get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed) get_editor_interface().get_file_system_dock().files_moved.connect(_on_files_moved) get_editor_interface().get_file_system_dock().file_removed.connect(_on_file_removed) add_tool_menu_item("Create copy of dialogue example balloon...", _copy_dialogue_balloon) func _exit_tree() -> void: remove_autoload_singleton("DialogueManager") remove_custom_type("DialogueLabel") remove_import_plugin(import_plugin) import_plugin = null remove_translation_parser_plugin(translation_parser_plugin) translation_parser_plugin = null if is_instance_valid(main_view): main_view.queue_free() get_editor_interface().get_resource_filesystem().filesystem_changed.disconnect(_on_filesystem_changed) get_editor_interface().get_file_system_dock().files_moved.disconnect(_on_files_moved) remove_tool_menu_item("Create copy of dialogue example balloon...") func _has_main_screen() -> bool: return true func _make_visible(next_visible: bool) -> void: if is_instance_valid(main_view): main_view.visible = next_visible func _get_plugin_name() -> String: return "Dialogue" func _get_plugin_icon() -> Texture2D: return load("res://addons/dialogue_manager/assets/icon.svg") func _handles(object) -> bool: return object is DialogueResource func _edit(object) -> void: if is_instance_valid(main_view) and is_instance_valid(object): main_view.open_resource(object) func _apply_changes() -> void: if is_instance_valid(main_view): main_view.apply_changes() func _build() -> bool: # Ignore errors in other files if we are just running the test scene if DialogueSettings.get_user_value("is_running_test_scene", true): return true var can_build: bool = true var is_first_file: bool = true for dialogue_file in dialogue_file_cache.values(): if dialogue_file.errors.size() > 0: # Open the first file if is_first_file: get_editor_interface().edit_resource(load(dialogue_file.path)) main_view.show_build_error_dialog() is_first_file = false push_error("You have %d error(s) in %s" % [dialogue_file.errors.size(), dialogue_file.path]) can_build = false return can_build ## Keep track of known files and their dependencies func add_to_dialogue_file_cache(path: String, resource_path: String, parse_results: DialogueManagerParseResult) -> void: dialogue_file_cache[path] = { path = path, resource_path = resource_path, dependencies = Array(parse_results.imported_paths).filter(func(d): return d != path), errors = [] } save_dialogue_cache() recompile_dependent_files(path) ## Keep track of compile errors func add_errors_to_dialogue_file_cache(path: String, errors: Array[Dictionary]) -> void: if dialogue_file_cache.has(path): dialogue_file_cache[path]["errors"] = errors else: dialogue_file_cache[path] = { path = path, errors = errors } save_dialogue_cache() recompile_dependent_files(path) ## Update references to a moved file func update_import_paths(from_path: String, to_path: String) -> void: # Update its own reference in the cache if dialogue_file_cache.has(from_path): dialogue_file_cache[to_path] = dialogue_file_cache[from_path].duplicate() dialogue_file_cache.erase(from_path) # Reopen the file if it's already open if main_view.current_file_path == from_path: main_view.current_file_path = "" main_view.open_file(to_path) # Update any other files that import the moved file var dependents = dialogue_file_cache.values().filter(func(d): return from_path in d.dependencies) for dependent in dependents: dependent.dependencies.erase(from_path) dependent.dependencies.append(to_path) # Update the live buffer if main_view.current_file_path == dependent.path: main_view.code_edit.text = main_view.code_edit.text.replace(from_path, to_path) main_view.pristine_text = main_view.code_edit.text # Open the file and update the path var file: FileAccess = FileAccess.open(dependent.path, FileAccess.READ) var text = file.get_as_text().replace(from_path, to_path) file = FileAccess.open(dependent.path, FileAccess.WRITE) file.store_string(text) save_dialogue_cache() ## Rebuild any files that depend on this path func recompile_dependent_files(path: String) -> void: # Rebuild any files that depend on this one var dependents = dialogue_file_cache.values().filter(func(d): return path in d.dependencies) for dependent in dependents: if dependent.has("path") and dependent.has("resource_path"): import_plugin.compile_file(dependent.path, dependent.resource_path, false) ## Make sure the cache points to real files func update_dialogue_file_cache() -> void: var cache: Dictionary = {} # Open our cache file if it exists if FileAccess.file_exists(DialogueConstants.CACHE_PATH): var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.READ) cache = JSON.parse_string(file.get_as_text()) # Scan for dialogue files var current_files: PackedStringArray = _get_dialogue_files_in_filesystem() # Add any files to POT generation var files_for_pot: PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations_pot_files", []) var files_for_pot_changed: bool = false for path in current_files: if not files_for_pot.has(path): files_for_pot.append(path) files_for_pot_changed = true # Remove any files that don't exist any more for path in cache.keys(): if not path in current_files: cache.erase(path) DialogueSettings.remove_recent_file(path) # Remove missing files from POT generation if files_for_pot.has(path): files_for_pot.remove_at(files_for_pot.find(path)) files_for_pot_changed = true # Update project settings if POT changed if files_for_pot_changed: ProjectSettings.set_setting("internationalization/locale/translations_pot_files", files_for_pot) ProjectSettings.save() dialogue_file_cache = cache ## Persist the cache func save_dialogue_cache() -> void: var file: FileAccess = FileAccess.open(DialogueConstants.CACHE_PATH, FileAccess.WRITE) file.store_string(JSON.stringify(dialogue_file_cache)) ## Recursively find any dialogue files in a directory func _get_dialogue_files_in_filesystem(path: String = "res://") -> PackedStringArray: var files: PackedStringArray = [] if DirAccess.dir_exists_absolute(path): var dir = DirAccess.open(path) dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": var file_path: String = (path + "/" + file_name).simplify_path() if dir.current_is_dir(): if not file_name in [".godot", ".tmp"]: files.append_array(_get_dialogue_files_in_filesystem(file_path)) elif file_name.get_extension() == "dialogue": files.append(file_path) file_name = dir.get_next() return files ### Callbacks func _copy_dialogue_balloon() -> void: var scale: float = get_editor_interface().get_editor_scale() var directory_dialog: FileDialog = FileDialog.new() var label: Label = Label.new() label.text = "Dialogue balloon files will be copied into chosen directory." directory_dialog.get_vbox().add_child(label) directory_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR directory_dialog.min_size = Vector2(600, 500) * scale directory_dialog.dir_selected.connect(func(path): var file: FileAccess = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.tscn", FileAccess.READ) var file_contents: String = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd") file = FileAccess.open(path + "/balloon.tscn", FileAccess.WRITE) file.store_string(file_contents) file.close() file = FileAccess.open("res://addons/dialogue_manager/example_balloon/small_example_balloon.tscn", FileAccess.READ) file_contents = file.get_as_text().replace("res://addons/dialogue_manager/example_balloon/example_balloon.gd", path + "/balloon.gd") file = FileAccess.open(path + "/small_balloon.tscn", FileAccess.WRITE) file.store_string(file_contents) file.close() file = FileAccess.open("res://addons/dialogue_manager/example_balloon/example_balloon.gd", FileAccess.READ) file_contents = file.get_as_text() file = FileAccess.open(path + "/balloon.gd", FileAccess.WRITE) file.store_string(file_contents) file.close() get_editor_interface().get_resource_filesystem().scan() get_editor_interface().get_file_system_dock().call_deferred("navigate_to_path", path + "/balloon.tscn") directory_dialog.queue_free() ) get_editor_interface().get_base_control().add_child(directory_dialog) directory_dialog.popup_centered() ### Signals func _on_filesystem_changed() -> void: update_dialogue_file_cache() func _on_files_moved(old_file: String, new_file: String) -> void: update_import_paths(old_file, new_file) DialogueSettings.move_recent_file(old_file, new_file) func _on_file_removed(file: String) -> void: recompile_dependent_files(file) if is_instance_valid(main_view): main_view.close_file(file)