804 lines
30 KiB
GDScript
804 lines
30 KiB
GDScript
@tool
|
|
class_name DialogicUtil
|
|
|
|
## Script that container helper methods for both editor and game execution.
|
|
## Used whenever the same thing is needed in different parts of the plugin.
|
|
|
|
#region EDITOR
|
|
|
|
## This method should be used instead of EditorInterface.get_editor_scale(), because if you use that
|
|
## it will run perfectly fine from the editor, but crash when the game is exported.
|
|
static func get_editor_scale() -> float:
|
|
if Engine.is_editor_hint():
|
|
return get_dialogic_plugin().get_editor_interface().get_editor_scale()
|
|
return 1.0
|
|
|
|
|
|
## Although this does in fact always return a EditorPlugin node,
|
|
## that class is apparently not present in export and referencing it here creates a crash.
|
|
static func get_dialogic_plugin() -> Node:
|
|
for child in Engine.get_main_loop().get_root().get_children():
|
|
if child.get_class() == "EditorNode":
|
|
return child.get_node('DialogicPlugin')
|
|
return null
|
|
|
|
#endregion
|
|
|
|
|
|
## Returns the autoload when in-game.
|
|
static func autoload() -> DialogicGameHandler:
|
|
if Engine.is_editor_hint():
|
|
return null
|
|
if not Engine.get_main_loop().root.has_node("Dialogic"):
|
|
return null
|
|
return Engine.get_main_loop().root.get_node("Dialogic")
|
|
|
|
|
|
#region FILE SYSTEM
|
|
################################################################################
|
|
static func listdir(path: String, files_only:= true, _throw_error:= true, full_file_path:= false, include_imports := false) -> Array:
|
|
var files: Array = []
|
|
if path.is_empty(): path = "res://"
|
|
if DirAccess.dir_exists_absolute(path):
|
|
var dir := DirAccess.open(path)
|
|
dir.list_dir_begin()
|
|
var file_name := dir.get_next()
|
|
while file_name != "":
|
|
if not file_name.begins_with("."):
|
|
if files_only:
|
|
if not dir.current_is_dir() and (not file_name.ends_with('.import') or include_imports):
|
|
if full_file_path:
|
|
files.append(path.path_join(file_name))
|
|
else:
|
|
files.append(file_name)
|
|
else:
|
|
if full_file_path:
|
|
files.append(path.path_join(file_name))
|
|
else:
|
|
files.append(file_name)
|
|
file_name = dir.get_next()
|
|
dir.list_dir_end()
|
|
return files
|
|
|
|
|
|
static func get_module_path(name:String, builtin:=true) -> String:
|
|
if builtin:
|
|
return "res://addons/dialogic/Modules".path_join(name)
|
|
else:
|
|
return ProjectSettings.get_setting('dialogic/extensions_folder', 'res://addons/dialogic_additions').path_join(name)
|
|
|
|
|
|
## This is a private and editor-only function.
|
|
##
|
|
## Populates the [class DialogicGameHandler] with new custom subsystems by
|
|
## directly manipulating the file's content and then importing the file.
|
|
static func _update_autoload_subsystem_access() -> void:
|
|
if not Engine.is_editor_hint():
|
|
printerr("[Dialogic] This function is only available in the editor.")
|
|
return
|
|
|
|
var script: Script = load("res://addons/dialogic/Core/DialogicGameHandler.gd")
|
|
var new_subsystem_access_list := "#region SUBSYSTEMS\n"
|
|
var subsystems_sorted := []
|
|
|
|
for indexer: DialogicIndexer in get_indexers(true, true):
|
|
|
|
for subsystem: Dictionary in indexer._get_subsystems().duplicate(true):
|
|
subsystems_sorted.append(subsystem)
|
|
|
|
subsystems_sorted.sort_custom(func (a: Dictionary, b: Dictionary) -> bool:
|
|
return a.name < b.name
|
|
)
|
|
|
|
for subsystem: Dictionary in subsystems_sorted:
|
|
new_subsystem_access_list += '\nvar {name} := preload("{script}").new():\n\tget: return get_subsystem("{name}")\n'.format(subsystem)
|
|
|
|
new_subsystem_access_list += "\n#endregion"
|
|
script.source_code = RegEx.create_from_string(r"#region SUBSYSTEMS\n#*\n((?!#endregion)(.*\n))*#endregion").sub(script.source_code, new_subsystem_access_list)
|
|
ResourceSaver.save(script)
|
|
Engine.get_singleton("EditorInterface").get_resource_filesystem().reimport_files(["res://addons/dialogic/Core/DialogicGameHandler.gd"])
|
|
|
|
|
|
static func get_indexers(include_custom := true, force_reload := false) -> Array[DialogicIndexer]:
|
|
if Engine.get_main_loop().has_meta('dialogic_indexers') and not force_reload:
|
|
return Engine.get_main_loop().get_meta('dialogic_indexers')
|
|
|
|
var indexers: Array[DialogicIndexer] = []
|
|
for file in listdir(DialogicUtil.get_module_path(''), false):
|
|
var possible_script: String = DialogicUtil.get_module_path(file).path_join("index.gd")
|
|
if ResourceLoader.exists(possible_script):
|
|
indexers.append(load(possible_script).new())
|
|
|
|
if include_custom:
|
|
var extensions_folder: String = ProjectSettings.get_setting('dialogic/extensions_folder', "res://addons/dialogic_additions/")
|
|
for file in listdir(extensions_folder, false, false):
|
|
var possible_script: String = extensions_folder.path_join(file + "/index.gd")
|
|
if ResourceLoader.exists(possible_script):
|
|
indexers.append(load(possible_script).new())
|
|
|
|
Engine.get_main_loop().set_meta('dialogic_indexers', indexers)
|
|
return indexers
|
|
|
|
|
|
|
|
## Turns a [param file_path] from `some_file.png` to `Some File`.
|
|
static func pretty_name(file_path: String) -> String:
|
|
var _name := file_path.get_file().trim_suffix("." + file_path.get_extension())
|
|
_name = _name.replace('_', ' ')
|
|
_name = _name.capitalize()
|
|
|
|
return _name
|
|
|
|
#endregion
|
|
|
|
|
|
#region EDITOR SETTINGS & COLORS
|
|
################################################################################
|
|
|
|
static func set_editor_setting(setting:String, value:Variant) -> void:
|
|
var cfg := ConfigFile.new()
|
|
if FileAccess.file_exists('user://dialogic/editor_settings.cfg'):
|
|
cfg.load('user://dialogic/editor_settings.cfg')
|
|
|
|
cfg.set_value('DES', setting, value)
|
|
|
|
if !DirAccess.dir_exists_absolute('user://dialogic'):
|
|
DirAccess.make_dir_absolute('user://dialogic')
|
|
cfg.save('user://dialogic/editor_settings.cfg')
|
|
|
|
|
|
static func get_editor_setting(setting:String, default:Variant=null) -> Variant:
|
|
var cfg := ConfigFile.new()
|
|
if !FileAccess.file_exists('user://dialogic/editor_settings.cfg'):
|
|
return default
|
|
|
|
if !cfg.load('user://dialogic/editor_settings.cfg') == OK:
|
|
return default
|
|
|
|
return cfg.get_value('DES', setting, default)
|
|
|
|
|
|
static func get_color_palette(default:bool = false) -> Dictionary:
|
|
var defaults := {
|
|
'Color1': Color('#3b8bf2'), # Blue
|
|
'Color2': Color('#00b15f'), # Green
|
|
'Color3': Color('#e868e2'), # Pink
|
|
'Color4': Color('#9468e8'), # Purple
|
|
'Color5': Color('#574fb0'), # DarkPurple
|
|
'Color6': Color('#1fa3a3'), # Aquamarine
|
|
'Color7': Color('#fa952a'), # Orange
|
|
'Color8': Color('#de5c5c'), # Red
|
|
'Color9': Color('#7c7c7c'), # Gray
|
|
}
|
|
if default:
|
|
return defaults
|
|
return get_editor_setting('color_palette', defaults)
|
|
|
|
|
|
static func get_color(value:String) -> Color:
|
|
var colors := get_color_palette()
|
|
return colors[value]
|
|
|
|
#endregion
|
|
|
|
|
|
#region TIMER PROCESS MODE
|
|
################################################################################
|
|
static func is_physics_timer() -> bool:
|
|
return ProjectSettings.get_setting('dialogic/timer/process_in_physics', false)
|
|
|
|
|
|
static func update_timer_process_callback(timer:Timer) -> void:
|
|
timer.process_callback = Timer.TIMER_PROCESS_PHYSICS if is_physics_timer() else Timer.TIMER_PROCESS_IDLE
|
|
|
|
#endregion
|
|
|
|
|
|
#region MULTITWEEN
|
|
################################################################################
|
|
static func multitween(tweened_value:Variant, item:Node, property:String, part:String) -> void:
|
|
var parts: Dictionary = item.get_meta(property+'_parts', {})
|
|
parts[part] = tweened_value
|
|
|
|
if not item.has_meta(property+'_base_value') and not 'base' in parts:
|
|
item.set_meta(property+'_base_value', item.get(property))
|
|
|
|
var final_value: Variant = parts.get('base', item.get_meta(property+'_base_value', item.get(property)))
|
|
|
|
for key in parts:
|
|
if key == 'base':
|
|
continue
|
|
else:
|
|
final_value += parts[key]
|
|
|
|
item.set(property, final_value)
|
|
item.set_meta(property+'_parts', parts)
|
|
|
|
#endregion
|
|
|
|
|
|
#region TRANSLATIONS
|
|
################################################################################
|
|
|
|
static func get_next_translation_id() -> String:
|
|
ProjectSettings.set_setting('dialogic/translation/id_counter', ProjectSettings.get_setting('dialogic/translation/id_counter', 16)+1)
|
|
return '%x' % ProjectSettings.get_setting('dialogic/translation/id_counter', 16)
|
|
|
|
#endregion
|
|
|
|
|
|
#region VARIABLES
|
|
################################################################################
|
|
|
|
enum VarTypes {ANY, STRING, FLOAT, INT, BOOL}
|
|
|
|
|
|
static func get_default_variables() -> Dictionary:
|
|
return ProjectSettings.get_setting('dialogic/variables', {})
|
|
|
|
|
|
# helper that converts a nested variable dictionary into an array with paths
|
|
static func list_variables(dict:Dictionary, path := "", type:=VarTypes.ANY) -> Array:
|
|
var array := []
|
|
for key in dict.keys():
|
|
if typeof(dict[key]) == TYPE_DICTIONARY:
|
|
array.append_array(list_variables(dict[key], path+key+".", type))
|
|
else:
|
|
if type == VarTypes.ANY or get_variable_value_type(dict[key]) == type:
|
|
array.append(path+key)
|
|
return array
|
|
|
|
|
|
static func get_variable_value_type(value:Variant) -> VarTypes:
|
|
match typeof(value):
|
|
TYPE_STRING:
|
|
return VarTypes.STRING
|
|
TYPE_FLOAT:
|
|
return VarTypes.FLOAT
|
|
TYPE_INT:
|
|
return VarTypes.INT
|
|
TYPE_BOOL:
|
|
return VarTypes.BOOL
|
|
return VarTypes.ANY
|
|
|
|
|
|
static func get_variable_type(path:String, dict:Dictionary={}) -> VarTypes:
|
|
if dict.is_empty():
|
|
dict = get_default_variables()
|
|
return get_variable_value_type(_get_value_in_dictionary(path, dict))
|
|
|
|
|
|
## This will set a value in a dictionary (or a sub-dictionary based on the path)
|
|
## e.g. it could set "Something.Something.Something" in {'Something':{'Something':{'Someting':"value"}}}
|
|
static func _set_value_in_dictionary(path:String, dictionary:Dictionary, value):
|
|
if '.' in path:
|
|
var from := path.split('.')[0]
|
|
if from in dictionary.keys():
|
|
dictionary[from] = _set_value_in_dictionary(path.trim_prefix(from+"."), dictionary[from], value)
|
|
else:
|
|
if path in dictionary.keys():
|
|
dictionary[path] = value
|
|
return dictionary
|
|
|
|
|
|
## This will get a value in a dictionary (or a sub-dictionary based on the path)
|
|
## e.g. it could get "Something.Something.Something" in {'Something':{'Something':{'Someting':"value"}}}
|
|
static func _get_value_in_dictionary(path:String, dictionary:Dictionary, default= null) -> Variant:
|
|
if '.' in path:
|
|
var from := path.split('.')[0]
|
|
if from in dictionary.keys():
|
|
return _get_value_in_dictionary(path.trim_prefix(from+"."), dictionary[from], default)
|
|
else:
|
|
if path in dictionary.keys():
|
|
return dictionary[path]
|
|
return default
|
|
|
|
#endregion
|
|
|
|
|
|
#region SCENE EXPORT OVERRIDES
|
|
################################################################################
|
|
|
|
static func apply_scene_export_overrides(node:Node, export_overrides:Dictionary, apply := true) -> void:
|
|
var default_info := get_scene_export_defaults(node)
|
|
if !node.script:
|
|
return
|
|
var property_info: Array[Dictionary] = node.script.get_script_property_list()
|
|
for i in property_info:
|
|
if i['usage'] & PROPERTY_USAGE_EDITOR:
|
|
if i['name'] in export_overrides:
|
|
if str_to_var(export_overrides[i['name']]) == null and typeof(node.get(i['name'])) == TYPE_STRING:
|
|
node.set(i['name'], export_overrides[i['name']])
|
|
else:
|
|
node.set(i['name'], str_to_var(export_overrides[i['name']]))
|
|
elif i['name'] in default_info:
|
|
node.set(i['name'], default_info.get(i['name']))
|
|
if apply:
|
|
if node.has_method('apply_export_overrides'):
|
|
node.apply_export_overrides()
|
|
|
|
|
|
static func get_scene_export_defaults(node:Node) -> Dictionary:
|
|
if !node.script:
|
|
return {}
|
|
|
|
if Engine.get_main_loop().has_meta('dialogic_scene_export_defaults') and \
|
|
node.script.resource_path in Engine.get_main_loop().get_meta('dialogic_scene_export_defaults'):
|
|
return Engine.get_main_loop().get_meta('dialogic_scene_export_defaults')[node.script.resource_path]
|
|
|
|
if !Engine.get_main_loop().has_meta('dialogic_scene_export_defaults'):
|
|
Engine.get_main_loop().set_meta('dialogic_scene_export_defaults', {})
|
|
var defaults := {}
|
|
var property_info: Array[Dictionary] = node.script.get_script_property_list()
|
|
for i in property_info:
|
|
if i['usage'] & PROPERTY_USAGE_EDITOR:
|
|
defaults[i['name']] = node.get(i['name'])
|
|
Engine.get_main_loop().get_meta('dialogic_scene_export_defaults')[node.script.resource_path] = defaults
|
|
return defaults
|
|
|
|
#endregion
|
|
|
|
#region MAKE CUSTOM
|
|
|
|
static func make_file_custom(original_file:String, target_folder:String, new_file_name := "", new_folder_name := "") -> String:
|
|
if not ResourceLoader.exists(original_file):
|
|
push_error("[Dialogic] Unable to make file with invalid path custom!")
|
|
return ""
|
|
|
|
if new_folder_name:
|
|
target_folder = target_folder.path_join(new_folder_name)
|
|
DirAccess.make_dir_absolute(target_folder)
|
|
|
|
if new_file_name.is_empty():
|
|
new_file_name = "custom_" + original_file.get_file()
|
|
|
|
if not new_file_name.ends_with(original_file.get_extension()):
|
|
new_file_name += "." + original_file.get_extension()
|
|
|
|
var target_file := target_folder.path_join(new_file_name)
|
|
|
|
customize_file(original_file, target_file)
|
|
|
|
get_dialogic_plugin().get_editor_interface().get_resource_filesystem().scan_sources()
|
|
|
|
return target_file
|
|
|
|
|
|
static func customize_file(original_file:String, target_file:String) -> String:
|
|
#print("\nCUSTOMIZE FILE")
|
|
#printt(original_file, "->", target_file)
|
|
|
|
DirAccess.copy_absolute(original_file, target_file)
|
|
|
|
var file := FileAccess.open(target_file, FileAccess.READ)
|
|
var file_text := file.get_as_text()
|
|
file.close()
|
|
|
|
# If we are customizing a scene, we check for any resources used in that scene that are in the same folder.
|
|
# Those will be copied as well and the scene will be modified to point to them.
|
|
if file_text.begins_with('[gd_'):
|
|
var base_path: String = original_file.get_base_dir()
|
|
|
|
var remove_uuid_regex := r'\[gd_.* (?<uid>uid="uid:[^"]*")'
|
|
var result := RegEx.create_from_string(remove_uuid_regex).search(file_text)
|
|
if result:
|
|
file_text = file_text.replace(result.get_string("uid"), "")
|
|
|
|
# This regex also removes the UID referencing the original resource
|
|
var file_regex := r'(uid="[^"]*" )?\Qpath="'+base_path+r'\E(?<file>[^"]*)"'
|
|
result = RegEx.create_from_string(file_regex).search(file_text)
|
|
while result:
|
|
var found_file_name := result.get_string('file')
|
|
var found_file_path := base_path.path_join(found_file_name)
|
|
var target_file_path := target_file.get_base_dir().path_join(found_file_name)
|
|
|
|
# Files found in this file will ALSO be customized.
|
|
customize_file(found_file_path, target_file_path)
|
|
|
|
file_text = file_text.replace(found_file_path, target_file_path)
|
|
|
|
result = RegEx.create_from_string(file_regex).search(file_text)
|
|
|
|
file = FileAccess.open(target_file, FileAccess.WRITE)
|
|
file.store_string(file_text)
|
|
file.close()
|
|
|
|
return target_file
|
|
|
|
#endregion
|
|
|
|
#region INSPECTOR FIELDS
|
|
################################################################################
|
|
|
|
static func setup_script_property_edit_node(property_info: Dictionary, value:Variant, property_changed:Callable) -> Control:
|
|
var input: Control = null
|
|
match property_info['type']:
|
|
TYPE_BOOL:
|
|
input = CheckBox.new()
|
|
if value != null:
|
|
input.button_pressed = value
|
|
input.toggled.connect(DialogicUtil._on_export_bool_submitted.bind(property_info.name, property_changed))
|
|
TYPE_COLOR:
|
|
input = ColorPickerButton.new()
|
|
if value != null:
|
|
input.color = value
|
|
input.color_changed.connect(DialogicUtil._on_export_color_submitted.bind(property_info.name, property_changed))
|
|
input.custom_minimum_size.x = get_editor_scale() * 50
|
|
TYPE_INT:
|
|
if property_info['hint'] & PROPERTY_HINT_ENUM:
|
|
input = OptionButton.new()
|
|
for x in property_info['hint_string'].split(','):
|
|
input.add_item(x.split(':')[0])
|
|
if value != null:
|
|
input.select(value)
|
|
input.item_selected.connect(DialogicUtil._on_export_int_enum_submitted.bind(property_info.name, property_changed))
|
|
else:
|
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_number.tscn").instantiate()
|
|
input.property_name = property_info['name']
|
|
input.use_int_mode()
|
|
|
|
if ',' in property_info.hint_string:
|
|
input.min_value = int(property_info.hint_string.get_slice(',', 0))
|
|
input.max_value = int(property_info.hint_string.get_slice(',', 1))
|
|
if property_info.hint_string.count(',') > 1:
|
|
input.step = int(property_info.hint_string.get_slice(',', 2))
|
|
else:
|
|
input.step = 1
|
|
input.max_value = INF
|
|
input.min_value = -INF
|
|
|
|
if value != null:
|
|
input.set_value(value)
|
|
input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_changed))
|
|
TYPE_FLOAT:
|
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_number.tscn").instantiate()
|
|
input.property_name = property_info['name']
|
|
input.use_float_mode()
|
|
input.step = 0.01
|
|
if ',' in property_info.hint_string:
|
|
input.min_value = float(property_info.hint_string.get_slice(',', 0))
|
|
input.max_value = float(property_info.hint_string.get_slice(',', 1))
|
|
if property_info.hint_string.count(',') > 1:
|
|
input.step = float(property_info.hint_string.get_slice(',', 2))
|
|
if value != null:
|
|
input.set_value(value)
|
|
input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_changed))
|
|
TYPE_VECTOR2, TYPE_VECTOR3, TYPE_VECTOR4:
|
|
var vectorSize: String = type_string(typeof(value))[-1]
|
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_vector" + vectorSize + ".tscn").instantiate()
|
|
input.property_name = property_info['name']
|
|
input.set_value(value)
|
|
input.value_changed.connect(DialogicUtil._on_export_vector_submitted.bind(property_changed))
|
|
TYPE_VECTOR2I, TYPE_VECTOR3I, TYPE_VECTOR4I:
|
|
var vectorSize: String = type_string(typeof(value))[-2]
|
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_vector" + vectorSize + ".tscn").instantiate()
|
|
input.step = 1
|
|
input.property_name = property_info['name']
|
|
input.set_value(value)
|
|
input.value_changed.connect(DialogicUtil._on_export_vectori_submitted.bind(property_changed))
|
|
TYPE_STRING:
|
|
if property_info['hint'] & PROPERTY_HINT_FILE or property_info['hint'] & PROPERTY_HINT_DIR:
|
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_file.tscn").instantiate()
|
|
input.show_editing_button = true
|
|
input.file_filter = property_info['hint_string']
|
|
input.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
|
if property_info['hint'] == PROPERTY_HINT_DIR:
|
|
input.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
|
input.property_name = property_info['name']
|
|
input.placeholder = "Default"
|
|
input.hide_reset = true
|
|
if value != null:
|
|
input.set_value(value)
|
|
input.value_changed.connect(DialogicUtil._on_export_file_submitted.bind(property_changed))
|
|
elif property_info['hint'] & PROPERTY_HINT_ENUM:
|
|
input = OptionButton.new()
|
|
var options: PackedStringArray = []
|
|
for x in property_info['hint_string'].split(','):
|
|
options.append(x.split(':')[0].strip_edges())
|
|
input.add_item(options[-1])
|
|
if value != null:
|
|
input.select(options.find(value))
|
|
input.item_selected.connect(DialogicUtil._on_export_string_enum_submitted.bind(property_info.name, options, property_changed))
|
|
else:
|
|
input = LineEdit.new()
|
|
if value != null:
|
|
input.text = value
|
|
input.text_submitted.connect(DialogicUtil._on_export_input_text_submitted.bind(property_info.name, property_changed))
|
|
TYPE_DICTIONARY:
|
|
input = load("res://addons/dialogic/Editor/Events/Fields/field_dictionary.tscn").instantiate()
|
|
input.property_name = property_info["name"]
|
|
input.set_value(value)
|
|
input.value_changed.connect(_on_export_dict_submitted.bind(property_changed))
|
|
TYPE_OBJECT:
|
|
input = load("res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn").instantiate()
|
|
input.hint_text = "Objects/Resources as settings are currently not supported. \nUse @export_file('*.extension') instead and load the resource once needed."
|
|
|
|
_:
|
|
input = LineEdit.new()
|
|
if value != null:
|
|
input.text = value
|
|
input.text_submitted.connect(_on_export_input_text_submitted.bind(property_info.name, property_changed))
|
|
return input
|
|
|
|
|
|
static func _on_export_input_text_submitted(text:String, property_name:String, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(text))
|
|
|
|
static func _on_export_bool_submitted(value:bool, property_name:String, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(value))
|
|
|
|
static func _on_export_color_submitted(color:Color, property_name:String, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(color))
|
|
|
|
static func _on_export_int_enum_submitted(item:int, property_name:String, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(item))
|
|
|
|
static func _on_export_number_submitted(property_name:String, value:float, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(value))
|
|
|
|
static func _on_export_file_submitted(property_name:String, value:String, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(value))
|
|
|
|
static func _on_export_string_enum_submitted(value:int, property_name:String, list:PackedStringArray, callable: Callable):
|
|
callable.call(property_name, var_to_str(list[value]))
|
|
|
|
static func _on_export_vector_submitted(property_name:String, value:Variant, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(value))
|
|
|
|
static func _on_export_vectori_submitted(property_name:String, value:Variant, callable: Callable) -> void:
|
|
match typeof(value):
|
|
TYPE_VECTOR2: value = Vector2i(value)
|
|
TYPE_VECTOR3: value = Vector3i(value)
|
|
TYPE_VECTOR4: value = Vector4i(value)
|
|
callable.call(property_name, var_to_str(value))
|
|
|
|
static func _on_export_dict_submitted(property_name:String, value:Variant, callable: Callable) -> void:
|
|
callable.call(property_name, var_to_str(value))
|
|
|
|
#endregion
|
|
|
|
|
|
#region EVENT DEFAULTS
|
|
################################################################################
|
|
|
|
static func get_custom_event_defaults(event_name:String) -> Dictionary:
|
|
if Engine.is_editor_hint():
|
|
return ProjectSettings.get_setting('dialogic/event_default_overrides', {}).get(event_name, {})
|
|
else:
|
|
if !Engine.get_main_loop().has_meta('dialogic_event_defaults'):
|
|
Engine.get_main_loop().set_meta('dialogic_event_defaults', ProjectSettings.get_setting('dialogic/event_default_overrides', {}))
|
|
return Engine.get_main_loop().get_meta('dialogic_event_defaults').get(event_name, {})
|
|
|
|
#endregion
|
|
|
|
|
|
#region CONVERSION
|
|
################################################################################
|
|
|
|
static func str_to_bool(boolstring:String) -> bool:
|
|
return true if boolstring == "true" else false
|
|
|
|
|
|
static func logical_convert(value:Variant) -> Variant:
|
|
if typeof(value) == TYPE_STRING:
|
|
if value.is_valid_int():
|
|
return value.to_int()
|
|
if value.is_valid_float():
|
|
return value.to_float()
|
|
if value == 'true':
|
|
return true
|
|
if value == 'false':
|
|
return false
|
|
return value
|
|
|
|
|
|
## Takes [param source] and builds a dictionary of keys only.
|
|
## The values are `null`.
|
|
static func str_to_hash_set(source: String) -> Dictionary:
|
|
var dictionary := Dictionary()
|
|
|
|
for character in source:
|
|
dictionary[character] = null
|
|
|
|
return dictionary
|
|
|
|
#endregion
|
|
|
|
|
|
static func get_character_suggestions(_search_text:String, current_value:DialogicCharacter = null, allow_none := true, allow_all:= false, editor_node:Node = null) -> Dictionary:
|
|
var suggestions := {}
|
|
|
|
var icon := load("res://addons/dialogic/Editor/Images/Resources/character.svg")
|
|
|
|
if allow_none and current_value:
|
|
suggestions['(No one)'] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
|
|
|
|
if allow_all:
|
|
suggestions['ALL'] = {'value':'--All--', 'tooltip':'All currently joined characters leave', 'editor_icon':["GuiEllipsis", "EditorIcons"]}
|
|
|
|
# Get characters in the current timeline and place them at the top of suggestions.
|
|
if editor_node:
|
|
var recent_characters := []
|
|
var timeline_node := editor_node.get_parent().find_parent("Timeline") as DialogicEditor
|
|
for event_node in timeline_node.find_child("Timeline").get_children():
|
|
if event_node == editor_node:
|
|
break
|
|
if event_node.resource is DialogicCharacterEvent or event_node.resource is DialogicTextEvent:
|
|
recent_characters.append(event_node.resource.character)
|
|
|
|
recent_characters.reverse()
|
|
for character in recent_characters:
|
|
if character and not character.get_character_name() in suggestions:
|
|
suggestions[character.get_character_name()] = {'value': character.get_character_name(), 'tooltip': character.resource_path, 'icon': icon.duplicate()}
|
|
|
|
var character_directory := DialogicResourceUtil.get_character_directory()
|
|
for resource in character_directory.keys():
|
|
suggestions[resource] = {'value': resource, 'tooltip': character_directory[resource], 'icon': icon}
|
|
|
|
return suggestions
|
|
|
|
|
|
static func get_portrait_suggestions(search_text:String, character:DialogicCharacter, allow_empty := false, empty_text := "Don't Change") -> Dictionary:
|
|
var icon := load("res://addons/dialogic/Editor/Images/Resources/portrait.svg")
|
|
var suggestions := {}
|
|
|
|
if allow_empty:
|
|
suggestions[empty_text] = {'value':'', 'editor_icon':["GuiRadioUnchecked", "EditorIcons"]}
|
|
|
|
if "{" in search_text:
|
|
suggestions[search_text] = {'value':search_text, 'editor_icon':["Variant", "EditorIcons"]}
|
|
|
|
if character != null:
|
|
for portrait in character.portraits:
|
|
suggestions[portrait] = {'value':portrait, 'icon':icon}
|
|
|
|
return suggestions
|
|
|
|
|
|
static func get_portrait_position_suggestions(search_text := "") -> Dictionary:
|
|
var icon := load(DialogicUtil.get_module_path("Character").path_join('portrait_position.svg'))
|
|
|
|
var setting: String = ProjectSettings.get_setting('dialogic/portraits/position_suggestion_names', 'leftmost, left, center, right, rightmost')
|
|
|
|
var suggestions := {}
|
|
|
|
if not search_text.is_empty():
|
|
suggestions[search_text] = {'value':search_text.strip_edges(), 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
|
|
|
|
for position_id in setting.split(','):
|
|
suggestions[position_id.strip_edges()] = {'value':position_id.strip_edges(), 'icon':icon}
|
|
if not search_text.is_empty() and position_id.strip_edges().begins_with(search_text):
|
|
suggestions.erase(search_text)
|
|
|
|
return suggestions
|
|
|
|
|
|
static func get_autoload_suggestions(filter:String="") -> Dictionary:
|
|
var suggestions := {}
|
|
|
|
for prop in ProjectSettings.get_property_list():
|
|
if prop.name.begins_with('autoload/'):
|
|
var some_autoload: String = prop.name.trim_prefix('autoload/')
|
|
suggestions[some_autoload] = {'value': some_autoload, 'tooltip':some_autoload, 'editor_icon': ["Node", "EditorIcons"]}
|
|
if filter.begins_with(some_autoload):
|
|
suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
|
|
return suggestions
|
|
|
|
|
|
static func get_autoload_script_resource(autoload_name:String) -> Script:
|
|
var script: Script
|
|
if autoload_name and ProjectSettings.has_setting('autoload/'+autoload_name):
|
|
var loaded_autoload := load(ProjectSettings.get_setting('autoload/'+autoload_name).trim_prefix('*'))
|
|
|
|
if loaded_autoload is PackedScene:
|
|
var packed_scene: PackedScene = loaded_autoload
|
|
script = packed_scene.instantiate().get_script()
|
|
|
|
else:
|
|
script = loaded_autoload
|
|
return script
|
|
|
|
|
|
static func get_autoload_method_suggestions(filter:String, autoload_name:String) -> Dictionary:
|
|
var suggestions := {}
|
|
|
|
var script := get_autoload_script_resource(autoload_name)
|
|
if script:
|
|
for script_method in script.get_script_method_list():
|
|
if script_method.name.begins_with('@') or script_method.name.begins_with('_'):
|
|
continue
|
|
suggestions[script_method.name] = {'value': script_method.name, 'tooltip':script_method.name, 'editor_icon': ["Callable", "EditorIcons"]}
|
|
|
|
if not filter.is_empty():
|
|
suggestions[filter] = {'value': filter, 'editor_icon':["GuiScrollArrowRight", "EditorIcons"]}
|
|
|
|
return suggestions
|
|
|
|
|
|
static func get_autoload_property_suggestions(_filter:String, autoload_name:String) -> Dictionary:
|
|
var suggestions := {}
|
|
var script := get_autoload_script_resource(autoload_name)
|
|
if script:
|
|
for property in script.get_script_property_list():
|
|
if property.name.ends_with('.gd') or property.name.begins_with('_'):
|
|
continue
|
|
suggestions[property.name] = {'value': property.name, 'tooltip':property.name, 'editor_icon': ["MemberProperty", "EditorIcons"]}
|
|
|
|
return suggestions
|
|
|
|
|
|
static func get_audio_bus_suggestions(_filter:= "") -> Dictionary:
|
|
var bus_name_list := {}
|
|
for i in range(AudioServer.bus_count):
|
|
if i == 0:
|
|
bus_name_list[AudioServer.get_bus_name(i)] = {'value':''}
|
|
else:
|
|
bus_name_list[AudioServer.get_bus_name(i)] = {'value':AudioServer.get_bus_name(i)}
|
|
return bus_name_list
|
|
|
|
|
|
static func get_audio_channel_suggestions(_search_text:String) -> Dictionary:
|
|
var suggestions := {}
|
|
var channel_defaults := DialogicUtil.get_audio_channel_defaults()
|
|
var cached_names := DialogicResourceUtil.get_channel_list()
|
|
|
|
for i in channel_defaults.keys():
|
|
if not cached_names.has(i):
|
|
cached_names.append(i)
|
|
|
|
cached_names.sort()
|
|
|
|
for i in cached_names:
|
|
if i.is_empty():
|
|
continue
|
|
|
|
suggestions[i] = {'value': i}
|
|
|
|
if i in channel_defaults.keys():
|
|
suggestions[i]["editor_icon"] = ["ProjectList", "EditorIcons"]
|
|
suggestions[i]["tooltip"] = "A default channel defined in the settings."
|
|
|
|
else:
|
|
suggestions[i]["editor_icon"] = ["AudioStreamPlayer", "EditorIcons"]
|
|
suggestions[i]["tooltip"] = "A temporary channel without defaults."
|
|
|
|
return suggestions
|
|
|
|
|
|
static func get_audio_channel_defaults() -> Dictionary:
|
|
return ProjectSettings.get_setting('dialogic/audio/channel_defaults', {
|
|
"": {
|
|
'volume': 0.0,
|
|
'audio_bus': '',
|
|
'fade_length': 0.0,
|
|
'loop': false,
|
|
},
|
|
"music": {
|
|
'volume': 0.0,
|
|
'audio_bus': '',
|
|
'fade_length': 0.0,
|
|
'loop': true,
|
|
}})
|
|
|
|
|
|
static func validate_audio_channel_name(text: String) -> Dictionary:
|
|
var result := {}
|
|
var channel_name_regex := RegEx.create_from_string(r'(?<dash_only>^-$)|(?<invalid>[^\w-]{1})')
|
|
var matches := channel_name_regex.search_all(text)
|
|
var invalid_chars := []
|
|
|
|
for regex_match in matches:
|
|
if regex_match.get_string('dash_only'):
|
|
result['error_tooltip'] = "Channel name cannot be '-'."
|
|
result['valid_text'] = ''
|
|
else:
|
|
var invalid_char = regex_match.get_string('invalid')
|
|
if not invalid_char in invalid_chars:
|
|
invalid_chars.append(invalid_char)
|
|
|
|
if invalid_chars:
|
|
result['valid_text'] = channel_name_regex.sub(text, '', true)
|
|
result['error_tooltip'] = "Channel names cannot contain the following characters: " + "".join(invalid_chars)
|
|
|
|
return result
|