First work on dialogic, resized guild, and started implementing portraits.

This commit is contained in:
2025-08-14 10:26:24 -04:00
parent 95a7db036b
commit 3aeb3d44e6
959 changed files with 47688 additions and 46 deletions

View File

@@ -0,0 +1,433 @@
class_name DialogicGameHandler
extends Node
## Class that is used as the Dialogic autoload.
## Autoload script that allows you to interact with all of Dialogic's systems:[br]
## - Holds all important information about the current state of Dialogic.[br]
## - Provides access to all the subsystems.[br]
## - Has methods to start/end timelines.[br]
## States indicating different phases of dialog.
enum States {
IDLE, ## Dialogic is awaiting input to advance.
REVEALING_TEXT, ## Dialogic is currently revealing text.
ANIMATING, ## Some animation is happening.
AWAITING_CHOICE, ## Dialogic awaits the selection of a choice
WAITING ## Dialogic is currently awaiting something.
}
## Flags indicating what to clear when calling [method clear].
enum ClearFlags {
FULL_CLEAR = 0, ## Clears all subsystems
KEEP_VARIABLES = 1, ## Clears all subsystems and info except for variables
TIMELINE_INFO_ONLY = 2 ## Doesn't clear subsystems but current timeline and index
}
## Reference to the currently executed timeline.
var current_timeline: DialogicTimeline = null
## Copy of the [member current_timeline]'s events.
var current_timeline_events: Array = []
## Index of the event the timeline handling is currently at.
var current_event_idx: int = 0
## Contains all information that subsystems consider relevant for
## the current situation
var current_state_info: Dictionary = {}
## Current state (see [member States] enum).
var current_state := States.IDLE:
get:
return current_state
set(new_state):
current_state = new_state
state_changed.emit(new_state)
## Emitted when [member current_state] change.
signal state_changed(new_state:States)
## When `true`, many dialogic processes won't continue until it's `false` again.
var paused := false:
set(value):
paused = value
if paused:
for subsystem in get_children():
if subsystem is DialogicSubsystem:
(subsystem as DialogicSubsystem).pause()
dialogic_paused.emit()
else:
for subsystem in get_children():
if subsystem is DialogicSubsystem:
(subsystem as DialogicSubsystem).resume()
dialogic_resumed.emit()
## Emitted when [member paused] changes to `true`.
signal dialogic_paused
## Emitted when [member paused] changes to `false`.
signal dialogic_resumed
## Emitted when the timeline ends.
## This can be a timeline ending or [method end_timeline] being called.
signal timeline_ended
## Emitted when a timeline starts by calling either [method start]
## or [method start_timeline].
signal timeline_started
## Emitted when an event starts being executed.
## The event may not have finished executing yet.
signal event_handled(resource: DialogicEvent)
## Emitted when a [class SignalEvent] event was reached.
signal signal_event(argument: Variant)
## Emitted when a signal event gets fired from a [class TextEvent] event.
signal text_signal(argument: String)
# Careful, this section is repopulated automatically at certain moments.
#region SUBSYSTEMS
var Audio := preload("res://addons/dialogic/Modules/Audio/subsystem_audio.gd").new():
get: return get_subsystem("Audio")
var Backgrounds := preload("res://addons/dialogic/Modules/Background/subsystem_backgrounds.gd").new():
get: return get_subsystem("Backgrounds")
var Portraits := preload("res://addons/dialogic/Modules/Character/subsystem_portraits.gd").new():
get: return get_subsystem("Portraits")
var PortraitContainers := preload("res://addons/dialogic/Modules/Character/subsystem_containers.gd").new():
get: return get_subsystem("PortraitContainers")
var Choices := preload("res://addons/dialogic/Modules/Choice/subsystem_choices.gd").new():
get: return get_subsystem("Choices")
var Expressions := preload("res://addons/dialogic/Modules/Core/subsystem_expression.gd").new():
get: return get_subsystem("Expressions")
var Animations := preload("res://addons/dialogic/Modules/Core/subsystem_animation.gd").new():
get: return get_subsystem("Animations")
var Inputs := preload("res://addons/dialogic/Modules/Core/subsystem_input.gd").new():
get: return get_subsystem("Inputs")
var Glossary := preload("res://addons/dialogic/Modules/Glossary/subsystem_glossary.gd").new():
get: return get_subsystem("Glossary")
var History := preload("res://addons/dialogic/Modules/History/subsystem_history.gd").new():
get: return get_subsystem("History")
var Jump := preload("res://addons/dialogic/Modules/Jump/subsystem_jump.gd").new():
get: return get_subsystem("Jump")
var Save := preload("res://addons/dialogic/Modules/Save/subsystem_save.gd").new():
get: return get_subsystem("Save")
var Settings := preload("res://addons/dialogic/Modules/Settings/subsystem_settings.gd").new():
get: return get_subsystem("Settings")
var Styles := preload("res://addons/dialogic/Modules/Style/subsystem_styles.gd").new():
get: return get_subsystem("Styles")
var Text := preload("res://addons/dialogic/Modules/Text/subsystem_text.gd").new():
get: return get_subsystem("Text")
var TextInput := preload("res://addons/dialogic/Modules/TextInput/subsystem_text_input.gd").new():
get: return get_subsystem("TextInput")
var VAR := preload("res://addons/dialogic/Modules/Variable/subsystem_variables.gd").new():
get: return get_subsystem("VAR")
var Voice := preload("res://addons/dialogic/Modules/Voice/subsystem_voice.gd").new():
get: return get_subsystem("Voice")
#endregion
## Autoloads are added first, so this happens REALLY early on game startup.
func _ready() -> void:
_collect_subsystems()
clear()
#region TIMELINE & EVENT HANDLING
################################################################################
## Method to start a timeline AND ensure that a layout scene is present.
## For argument info, checkout [method start_timeline].
## -> returns the layout node
func start(timeline:Variant, label:Variant="") -> Node:
# If we don't have a style subsystem, default to just start_timeline()
if not has_subsystem('Styles'):
printerr("[Dialogic] You called Dialogic.start() but the Styles subsystem is missing!")
clear(ClearFlags.KEEP_VARIABLES)
start_timeline(timeline, label)
return null
# Otherwise make sure there is a style active.
var scene: Node = null
if !self.Styles.has_active_layout_node():
scene = self.Styles.load_style()
else:
scene = self.Styles.get_layout_node()
scene.show()
if not scene.is_node_ready():
scene.ready.connect(clear.bind(ClearFlags.KEEP_VARIABLES))
scene.ready.connect(start_timeline.bind(timeline, label))
else:
start_timeline(timeline, label)
return scene
## Method to start a timeline without adding a layout scene.
## @timeline can be either a loaded timeline resource or a path to a timeline file.
## @label_or_idx can be a label (string) or index (int) to skip to immediatly.
func start_timeline(timeline:Variant, label_or_idx:Variant = "") -> void:
# load the resource if only the path is given
if typeof(timeline) == TYPE_STRING:
#check the lookup table if it's not a full file name
if (timeline as String).contains("res://"):
timeline = load((timeline as String))
else:
timeline = DialogicResourceUtil.get_timeline_resource((timeline as String))
if timeline == null:
printerr("[Dialogic] There was an error loading this timeline. Check the filename, and the timeline for errors")
return
(timeline as DialogicTimeline).process()
current_timeline = timeline
current_timeline_events = current_timeline.events
for event in current_timeline_events:
event.dialogic = self
current_event_idx = -1
if typeof(label_or_idx) == TYPE_STRING:
if label_or_idx:
if has_subsystem('Jump'):
Jump.jump_to_label((label_or_idx as String))
elif typeof(label_or_idx) == TYPE_INT:
if label_or_idx >-1:
current_event_idx = label_or_idx -1
timeline_started.emit()
handle_next_event()
## Preloader function, prepares a timeline and returns an object to hold for later
## [param timeline_resource] can be either a path (string) or a loaded timeline (resource)
func preload_timeline(timeline_resource:Variant) -> Variant:
# I think ideally this should be on a new thread, will test
if typeof(timeline_resource) == TYPE_STRING:
timeline_resource = load((timeline_resource as String))
if timeline_resource == null:
printerr("[Dialogic] There was an error preloading this timeline. Check the filename, and the timeline for errors")
return null
(timeline_resource as DialogicTimeline).process()
return timeline_resource
## Clears and stops the current timeline.
func end_timeline() -> void:
await clear(ClearFlags.TIMELINE_INFO_ONLY)
_on_timeline_ended()
timeline_ended.emit()
## Handles the next event.
func handle_next_event(_ignore_argument: Variant = "") -> void:
handle_event(current_event_idx+1)
## Handles the event at the given index [param event_index].
## You can call this manually, but if another event is still executing, it might have unexpected results.
func handle_event(event_index:int) -> void:
if not current_timeline:
return
_cleanup_previous_event()
if paused:
await dialogic_resumed
if event_index >= len(current_timeline_events):
end_timeline()
return
#actually process the event now, since we didnt earlier at runtime
#this needs to happen before we create the copy DialogicEvent variable, so it doesn't throw an error if not ready
if current_timeline_events[event_index].event_node_ready == false:
current_timeline_events[event_index]._load_from_string(current_timeline_events[event_index].event_node_as_text)
current_event_idx = event_index
if not current_timeline_events[event_index].event_finished.is_connected(handle_next_event):
current_timeline_events[event_index].event_finished.connect(handle_next_event)
set_meta('previous_event', current_timeline_events[event_index])
current_timeline_events[event_index].execute(self)
event_handled.emit(current_timeline_events[event_index])
## Resets Dialogic's state fully or partially.
## By using the clear flags from the [member ClearFlags] enum you can specify
## what info should be kept.
## For example, at timeline end usually it doesn't clear node or subsystem info.
func clear(clear_flags := ClearFlags.FULL_CLEAR) -> void:
_cleanup_previous_event()
if !clear_flags & ClearFlags.TIMELINE_INFO_ONLY:
for subsystem in get_children():
if subsystem is DialogicSubsystem:
(subsystem as DialogicSubsystem).clear_game_state(clear_flags)
var timeline := current_timeline
current_timeline = null
current_event_idx = -1
current_timeline_events = []
current_state = States.IDLE
# Resetting variables
if timeline:
await timeline.clean()
## Cleanup after previous event (if any).
func _cleanup_previous_event():
if has_meta('previous_event') and get_meta('previous_event') is DialogicEvent:
var event := get_meta('previous_event') as DialogicEvent
if event.event_finished.is_connected(handle_next_event):
event.event_finished.disconnect(handle_next_event)
event._clear_state()
remove_meta("previous_event")
#endregion
#region SAVING & LOADING
################################################################################
## Returns a dictionary containing all necessary information to later recreate the same state with load_full_state.
## The [subsystem Save] subsystem might be more useful for you.
## However, this can be used to integrate the info into your own save system.
func get_full_state() -> Dictionary:
if current_timeline:
current_state_info['current_event_idx'] = current_event_idx
current_state_info['current_timeline'] = current_timeline.resource_path
else:
current_state_info['current_event_idx'] = -1
current_state_info['current_timeline'] = null
for subsystem in get_children():
(subsystem as DialogicSubsystem).save_game_state()
return current_state_info.duplicate(true)
## This method tries to load the state from the given [param state_info].
## Will automatically start a timeline and add a layout if a timeline was running when
## the dictionary was retrieved with [method get_full_state].
func load_full_state(state_info:Dictionary) -> void:
clear()
current_state_info = state_info
## The Style subsystem needs to run first for others to load correctly.
var scene: Node = null
if has_subsystem('Styles'):
get_subsystem('Styles').load_game_state()
scene = self.Styles.get_layout_node()
var load_subsystems := func() -> void:
for subsystem in get_children():
if subsystem.name == 'Styles':
continue
(subsystem as DialogicSubsystem).load_game_state()
if null != scene and not scene.is_node_ready():
scene.ready.connect(load_subsystems)
else:
await get_tree().process_frame
load_subsystems.call()
if current_state_info.get('current_timeline', null):
start_timeline(current_state_info.current_timeline, current_state_info.get('current_event_idx', 0))
else:
end_timeline.call_deferred()
#endregion
#region SUB-SYTSEMS
################################################################################
func _collect_subsystems() -> void:
var subsystem_nodes := [] as Array[DialogicSubsystem]
for indexer in DialogicUtil.get_indexers():
for subsystem in indexer._get_subsystems():
var subsystem_node := add_subsystem(str(subsystem.name), str(subsystem.script))
subsystem_nodes.push_back(subsystem_node)
for subsystem in subsystem_nodes:
subsystem.post_install()
## Returns `true` if a subystem with the given [param subsystem_name] exists.
func has_subsystem(subsystem_name:String) -> bool:
return has_node(subsystem_name)
## Returns the subsystem node of the given [param subsystem_name] or null if it doesn't exist.
func get_subsystem(subsystem_name:String) -> DialogicSubsystem:
return get_node(subsystem_name)
## Adds a subsystem node with the given [param subsystem_name] and [param script_path].
func add_subsystem(subsystem_name:String, script_path:String) -> DialogicSubsystem:
var node: Node = Node.new()
node.name = subsystem_name
node.set_script(load(script_path))
node = node as DialogicSubsystem
node.dialogic = self
add_child(node)
return node
#endregion
#region HELPERS
################################################################################
## This handles the `Layout End Behaviour` setting that can be changed in the Dialogic settings.
func _on_timeline_ended() -> void:
if self.Styles.has_active_layout_node() and self.Styles.get_layout_node().is_inside_tree():
match ProjectSettings.get_setting('dialogic/layout/end_behaviour', 0):
0:
self.Styles.get_layout_node().get_parent().remove_child(self.Styles.get_layout_node())
self.Styles.get_layout_node().queue_free()
1:
@warning_ignore("unsafe_method_access")
self.Styles.get_layout_node().hide()
func print_debug_moment() -> void:
if not current_timeline:
return
printerr("\tAt event ", current_event_idx+1, " (",current_timeline_events[current_event_idx].event_name, ' Event) in timeline "', DialogicResourceUtil.get_unique_identifier(current_timeline.resource_path), '" (',current_timeline.resource_path,').')
print("\n")
#endregion

View File

@@ -0,0 +1 @@
uid://jowd827y5pxt

View File

@@ -0,0 +1,291 @@
@tool
class_name DialogicResourceUtil
static var label_cache := {}
static var event_cache: Array[DialogicEvent] = []
static var special_resources := {}
static func update() -> void:
update_directory('.dch')
update_directory('.dtl')
update_label_cache()
#region RESOURCE DIRECTORIES
################################################################################
static func get_directory(extension:String) -> Dictionary:
extension = extension.trim_prefix('.')
if Engine.has_meta(extension+'_directory'):
return Engine.get_meta(extension+'_directory', {})
var directory: Dictionary = ProjectSettings.get_setting("dialogic/directories/"+extension+'_directory', {})
Engine.set_meta(extension+'_directory', directory)
return directory
static func set_directory(extension:String, directory:Dictionary) -> void:
extension = extension.trim_prefix('.')
if Engine.is_editor_hint():
ProjectSettings.set_setting("dialogic/directories/"+extension+'_directory', directory)
ProjectSettings.save()
Engine.set_meta(extension+'_directory', directory)
static func update_directory(extension:String) -> void:
var directory := get_directory(extension)
for resource in list_resources_of_type(extension):
if not resource in directory.values():
directory = add_resource_to_directory(resource, directory)
var keys_to_remove := []
for key in directory:
if not ResourceLoader.exists(directory[key]):
keys_to_remove.append(key)
for key in keys_to_remove:
directory.erase(key)
set_directory(extension, directory)
static func add_resource_to_directory(file_path:String, directory:Dictionary) -> Dictionary:
var suggested_name := file_path.get_file().trim_suffix("."+file_path.get_extension())
while suggested_name in directory:
suggested_name = file_path.trim_suffix("/"+suggested_name+"."+file_path.get_extension()).get_file().path_join(suggested_name)
directory[suggested_name] = file_path
return directory
## Returns the unique identifier for the given resource path.
## Returns an empty string if no identifier was found.
static func get_unique_identifier(file_path:String) -> String:
var identifier: String = get_directory(file_path.get_extension()).find_key(file_path)
if typeof(identifier) == TYPE_STRING:
return identifier
return ""
## Returns the resource associated with the given unique identifier.
## The expected extension is needed to use the right directory.
static func get_resource_from_identifier(identifier:String, extension:String) -> Resource:
var path: String = get_directory(extension).get(identifier, '')
if ResourceLoader.exists(path):
return load(path)
return null
static func change_unique_identifier(file_path:String, new_identifier:String) -> void:
var directory := get_directory(file_path.get_extension())
var key: String = directory.find_key(file_path)
while key != null:
if key == new_identifier:
break
directory.erase(key)
directory[new_identifier] = file_path
key = directory.find_key(file_path)
set_directory(file_path.get_extension(), directory)
static func change_resource_path(old_path:String, new_path:String) -> void:
var directory := get_directory(new_path.get_extension())
var key: String = directory.find_key(old_path)
while key != null:
directory[key] = new_path
key = directory.find_key(old_path)
set_directory(new_path.get_extension(), directory)
static func remove_resource(file_path:String) -> void:
var directory := get_directory(file_path.get_extension())
var key: String = directory.find_key(file_path)
while key != null:
directory.erase(key)
key = directory.find_key(file_path)
set_directory(file_path.get_extension(), directory)
static func is_identifier_unused(extension:String, identifier:String) -> bool:
return not identifier in get_directory(extension)
#endregion
#region LABEL CACHE
################################################################################
# The label cache is only for the editor so we don't have to scan all timelines
# whenever we want to suggest labels. This has no use in game and is not always perfect.
static func get_label_cache() -> Dictionary:
if not label_cache.is_empty():
return label_cache
label_cache = DialogicUtil.get_editor_setting('label_ref', {})
return label_cache
static func set_label_cache(cache:Dictionary) -> void:
label_cache = cache
static func update_label_cache() -> void:
var cache := get_label_cache()
var timelines := get_timeline_directory().values()
for timeline in cache:
if !timeline in timelines:
cache.erase(timeline)
set_label_cache(cache)
#endregion
#region EVENT CACHE
################################################################################
## Dialogic keeps a list that has each event once. This allows retrieval of that list.
static func get_event_cache() -> Array:
if not event_cache.is_empty():
return event_cache
event_cache = update_event_cache()
return event_cache
static func update_event_cache() -> Array:
event_cache = []
for indexer in DialogicUtil.get_indexers():
# build event cache
for event in indexer._get_events():
if not ResourceLoader.exists(event):
continue
if not 'event_end_branch.gd' in event and not 'event_text.gd' in event:
event_cache.append(load(event).new())
# Events are checked in order while testing them. EndBranch needs to be first, Text needs to be last
event_cache.push_front(DialogicEndBranchEvent.new())
event_cache.push_back(DialogicTextEvent.new())
return event_cache
#endregion
#region SPECIAL RESOURCES
################################################################################
static func update_special_resources() -> void:
special_resources.clear()
for indexer in DialogicUtil.get_indexers():
var additions := indexer._get_special_resources()
for resource_type in additions:
if not resource_type in special_resources:
special_resources[resource_type] = {}
special_resources[resource_type].merge(additions[resource_type])
static func list_special_resources(type:String, filter := {}) -> Dictionary:
if special_resources.is_empty():
update_special_resources()
if type in special_resources:
if filter.is_empty():
return special_resources[type]
else:
var results := {}
for i in special_resources[type]:
if match_resource_filter(special_resources[type][i], filter):
results[i] = special_resources[type][i]
return results
return {}
static func match_resource_filter(dict:Dictionary, filter:Dictionary) -> bool:
for i in filter:
if not i in dict:
return false
if typeof(filter[i]) == TYPE_ARRAY:
if not dict[i] in filter[i]:
return false
else:
if not dict[i] == filter[i]:
return false
return true
static func guess_special_resource(type: String, string: String, default := {}, filter := {}, ignores:PackedStringArray=[]) -> Dictionary:
if string.is_empty():
return default
if special_resources.is_empty():
update_special_resources()
var resources := list_special_resources(type, filter)
if resources.is_empty():
printerr("[Dialogic] No ", type, "s found, but attempted to use one.")
return default
if string.begins_with('res://'):
for i in resources.values():
if i.path == string:
return i
printerr("[Dialogic] Unable to find ", type, " at path '", string, "'.")
return default
string = string.to_lower()
if string in resources:
return resources[string]
if not ignores.is_empty():
var regex := RegEx.create_from_string(r" ?\b(" + "|".join(ignores) + r")\b")
for name in resources:
if regex.sub(name, "") == regex.sub(string, ""):
return resources[name]
## As a last effort check against the unfiltered list
if string in special_resources[type]:
push_warning("[Dialogic] Using ", type, " '", string,"' when not supposed to.")
return special_resources[type][string]
printerr("[Dialogic] Unable to identify ", type, " based on string '", string, "'.")
return default
#endregion
#region HELPERS
################################################################################
static func get_character_directory() -> Dictionary:
return get_directory('dch')
static func get_timeline_directory() -> Dictionary:
return get_directory('dtl')
static func get_timeline_resource(timeline_identifier:String) -> DialogicTimeline:
return get_resource_from_identifier(timeline_identifier, 'dtl')
static func get_character_resource(character_identifier:String) -> DialogicCharacter:
return get_resource_from_identifier(character_identifier, 'dch')
static func list_resources_of_type(extension:String) -> Array:
var all_resources := scan_folder('res://', extension)
return all_resources
static func scan_folder(path:String, extension:String) -> Array:
var list: Array = []
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 dir.current_is_dir() and not file_name.begins_with("."):
list += scan_folder(path.path_join(file_name), extension)
else:
if file_name.ends_with(extension):
list.append(path.path_join(file_name))
file_name = dir.get_next()
return list
#endregion

View File

@@ -0,0 +1 @@
uid://6fs1shmk8ip4

View File

@@ -0,0 +1,676 @@
@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:
return get_dialogic_plugin().get_editor_interface().get_editor_scale()
## 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"
for indexer: DialogicIndexer in get_indexers(true, true):
for subsystem: Dictionary in indexer._get_subsystems().duplicate(true):
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 !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 STYLES
################################################################################
static func get_default_layout_base() -> PackedScene:
return load(DialogicUtil.get_module_path('DefaultLayoutParts').path_join("Base_Default/default_layout_base.tscn"))
static func get_fallback_style() -> DialogicStyle:
return load(DialogicUtil.get_module_path('DefaultLayoutParts').path_join("Style_VN_Default/default_vn_style.tres"))
static func get_default_style() -> DialogicStyle:
var default: String = ProjectSettings.get_setting('dialogic/layout/default_style', '')
if !ResourceLoader.exists(default):
return get_fallback_style()
return load(default)
static func get_style_by_name(name:String) -> DialogicStyle:
if name.is_empty():
return get_default_style()
var styles: Array = ProjectSettings.get_setting('dialogic/layout/style_list', [])
for style in styles:
if not ResourceLoader.exists(style):
continue
if load(style).name == name:
return load(style)
return get_default_style()
#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 = SpinBox.new()
input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_info.name, property_changed))
if property_info.hint_string == 'int':
input.step = 1
input.allow_greater = true
input.allow_lesser = true
elif ',' 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))
if value != null:
input.value = value
TYPE_FLOAT:
input = SpinBox.new()
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))
input.value_changed.connect(DialogicUtil._on_export_number_submitted.bind(property_info.name, property_changed))
if value != null:
input.value = value
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_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.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.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(value:float, property_name:String, 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_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

View File

@@ -0,0 +1 @@
uid://dpthgwtjtxtms

View File

@@ -0,0 +1,41 @@
class_name DialogicSubsystem
extends Node
var dialogic: DialogicGameHandler = null
enum LoadFlags {FULL_LOAD, ONLY_DNODES}
# To be overriden by sub-classes
# Called once after every subsystem has been added to the tree
func post_install() -> void:
pass
# To be overriden by sub-classes
# Fill in everything that should be cleared (for example before loading a different state)
func clear_game_state(_clear_flag:=DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
pass
# To be overriden by sub-classes
# Fill in everything that should be loaded using the dialogic_game_handler.current_state_info
# This is called when a save is loaded
func load_game_state(_load_flag:=LoadFlags.FULL_LOAD) -> void:
pass
# To be overriden by sub-classes
# Fill in everything that should be saved into the dialogic_game_handler.current_state_info
# This is called when a save is saved
func save_game_state() -> void:
pass
# To be overriden by sub-classes
func pause() -> void:
pass
# To be overriden by sub-classes
func resume() -> void:
pass

View File

@@ -0,0 +1 @@
uid://dqjswfyxdk3f3

View File

@@ -0,0 +1,152 @@
@tool
class_name DialogicIndexer
extends RefCounted
## Script that indexes events, subsystems, settings pages and more. [br]
## Place a script of this type in every folder in "addons/Events". [br]
## Overwrite the methods to return the contents of that folder.
var this_folder: String = get_script().resource_path.get_base_dir()
## Overwrite if this module contains any events. [br]
## Return an array with all the paths to the event scripts.[br]
## You can use the [property this_folder].path_join('my_event.gd')
func _get_events() -> Array:
if ResourceLoader.exists(this_folder.path_join('event.gd')):
return [this_folder.path_join('event.gd')]
return []
## Overwrite if this module contains any subsystems.
## Should return an array of dictionaries each with the following keys: [br]
## "name" -> name for this subsystem[br]
## "script" -> array of preview images[br]
func _get_subsystems() -> Array[Dictionary]:
return []
func _get_editors() -> Array[String]:
return []
func _get_settings_pages() -> Array:
return []
func _get_character_editor_sections() -> Array:
return []
#region TEXT EFFECTS & MODIFIERS
## Should return array of dictionaries with the following keys:[br]
## "command" -> the text e.g. "speed"[br]
## "node_path" or "subsystem" -> whichever contains your effect method[br]
## "method" -> name of the effect method[br]
func _get_text_effects() -> Array[Dictionary]:
return []
## Should return array of dictionaries with the same arguments as _get_text_effects()
func _get_text_modifiers() -> Array[Dictionary]:
return []
#endregion
## Return a list of resources, scripts, etc.
## These can later be retrieved with DialogicResourceUtil.
## Each dictionary should contain (at least "type" and "path").
## E.g. {"type":"Animation", "path": "res://..."}
func _get_special_resources() -> Dictionary:
return {}
## Return a list of dictionaries, each
func _get_portrait_scene_presets() -> Array[Dictionary]:
return []
#region HELPERS
################################################################################
func list_dir(subdir:='') -> Array:
return Array(DirAccess.get_files_at(this_folder.path_join(subdir))).map(func(file):return this_folder.path_join(subdir).path_join(file))
func list_special_resources(subdir:='', extension:="") -> Dictionary:
var dict := {}
for i in list_dir(subdir):
if extension.is_empty() or i.ends_with(extension):
dict[DialogicUtil.pretty_name(i).to_lower()] = {"path":i}
return dict
func list_animations(subdir := "") -> Dictionary:
var full_animation_list := {}
for path in list_dir(subdir):
if not path.ends_with(".gd") and not path.ends_with(".gdc"):
continue
var anim_object: DialogicAnimation = load(path).new()
var versions := anim_object._get_named_variations()
for version_name in versions:
full_animation_list[version_name] = versions[version_name]
full_animation_list[version_name]["path"] = path
anim_object.queue_free()
return full_animation_list
#endregion
#region STYLES & LAYOUTS
################################################################################
func _get_style_presets() -> Array[Dictionary]:
return []
## Should return an array of dictionaries with the following keys:[br]
## "path" -> the path to the scene[br]
## "name" -> name for this layout[br]
## "description"-> description of this layout. list what features/events are supported[br]
## "preview_image"-> array of preview images[br]
func _get_layout_parts() -> Array[Dictionary]:
return []
## Helper that allows scanning sub directories that might be layout parts or styles
func scan_for_layout_parts() -> Array[Dictionary]:
var dir := DirAccess.open(this_folder)
var style_list: Array[Dictionary] = []
if !dir:
return style_list
dir.list_dir_begin()
var dir_name := dir.get_next()
while dir_name != "":
if !dir.current_is_dir() or !dir.file_exists(dir_name.path_join('part_config.cfg')):
dir_name = dir.get_next()
continue
var config := ConfigFile.new()
config.load(this_folder.path_join(dir_name).path_join('part_config.cfg'))
var default_image_path: String = this_folder.path_join(dir_name).path_join('preview.png')
style_list.append(
{
'type': config.get_value('style', 'type', 'Unknown type'),
'name': config.get_value('style', 'name', 'Unnamed Layout'),
'path': this_folder.path_join(dir_name).path_join(config.get_value('style', 'scene', '')),
'author': config.get_value('style', 'author', 'Anonymous'),
'description': config.get_value('style', 'description', 'No description'),
'preview_image': [config.get_value('style', 'image', default_image_path)],
'style_path':config.get_value('style', 'style_path', ''),
'icon':this_folder.path_join(dir_name).path_join(config.get_value('style', 'icon', '')),
})
if not style_list[-1].style_path.begins_with('res://'):
style_list[-1].style_path = this_folder.path_join(dir_name).path_join(style_list[-1].style_path)
dir_name = dir.get_next()
return style_list
#endregion

View File

@@ -0,0 +1 @@
uid://dv6nv4ytjqq42