SupaLidlGame/addons/dialogue_manager/plugin.gd

303 lines
10 KiB
GDScript

@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)