# (c) 2023-present Eroax # (c) 2023-present Yagich # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) class_name NodeDB ## Path to search for node files. const BASE_NODE_PATH := "res://classes/deck/nodes/" ## Filepath where the index of [NodeDB.NodeDescriptor]s are saved to avoid reloading ## everything each run. ## @experimental const NODE_INDEX_CACHE_PATH := "user://nodes_index.json" ## Filepath where the the list of favorite nodes is stored. const FAVORITE_NODES_PATH := "user://favorite_nodes.json" ## A map of [code]snake_case[/code] category names for proper capitalization. const CATEGORY_CAPITALIZATION := { "obs": "OBS" } ## A list of nodes that the user marked favorite in the [AddNodeMenu]. static var favorite_nodes: Array[String] # Dictionary[node_type, NodeDescriptor] ## The node index. Maps [member DeckNode.node_type]s to [NodeDB.NodeDescriptor]. static var nodes: Dictionary = {} static var libraries: Dictionary = {} static func init() -> void: load_favorites() #if load_node_index(): #return create_descriptors(BASE_NODE_PATH) reload_libraries() save_node_index() ## Fills the [member nodes] index. static func create_descriptors(path: String) -> void: var dir := DirAccess.open(path) if not dir: return dir.list_dir_begin() var current_file := dir.get_next() while current_file != "": if dir.current_is_dir(): create_descriptors(path.path_join(current_file)) elif current_file.ends_with(".gd"): var script_path := path.path_join(current_file) var node: DeckNode = load(script_path).new() as DeckNode var aliases: String = node.aliases.reduce( func(accum, el): return accum + el , "") var descriptor := NodeDescriptor.new( script_path, node.name, node.node_type, node.description, aliases, path.get_slice("/", path.get_slice_count("/") - 1), node.appears_in_search, false, ) nodes[node.node_type] = descriptor print_verbose("NodeDB: freeing node %s, id %s" % [node.node_type, node.get_instance_id()]) node.free() current_file = dir.get_next() ## Fills the [member libraries] index. static func create_lib_descriptors(path: String) -> void: var dir := DirAccess.open(path) if not dir: return dir.list_dir_begin() var current_file := dir.get_next() while current_file != "": if dir.current_is_dir(): create_lib_descriptors(path.path_join(current_file)) elif current_file.ends_with(".deck"): var load_path := path.path_join(current_file) var f := FileAccess.open(load_path, FileAccess.READ) var deck: Dictionary = JSON.parse_string(f.get_as_text()) if not deck.deck.has("library"): current_file = dir.get_next() continue var type := current_file if nodes.has(type): DeckHolder.logger.toast_error("Library group '%s' collides with a node with the same type." % type) current_file = dir.get_next() continue if libraries.has(type): DeckHolder.logger.toast_error("Library group '%s' collides with a library group with the same type." % type) current_file = dir.get_next() continue var lib = deck.deck.library var aliases: String = lib.lib_aliases.reduce( func(accum, el): return accum + el , "") var descriptor := NodeDescriptor.new( load_path, lib.lib_name, type, lib.lib_description, aliases, path.get_slice("/", path.get_slice_count("/") - 1), true, true, ) descriptor.added_by_library = path libraries[type] = descriptor current_file = dir.get_next() static func reload_libraries() -> void: libraries.clear() for path in StreamGraphConfig.get_library_search_paths(): create_lib_descriptors(path) ## Instantiates a [DeckNode] from a given [param node_type]. See [member DeckNode.node_type]. static func instance_node(node_type: String) -> DeckNode: if not nodes.has(node_type): return null return load(nodes[node_type]["script_path"]).new() ## Saves the index of all loaded nodes to [member NODE_INDEX_CACHE_PATH]. static func save_node_index() -> void: var d := {} for node_type in nodes: var nd: NodeDescriptor = nodes[node_type] as NodeDescriptor d[node_type] = nd.to_dictionary() var json := JSON.stringify(d, "\t") var f := FileAccess.open(NODE_INDEX_CACHE_PATH, FileAccess.WRITE) f.store_string(json) ## Loads the node index from [member NODE_INDEX_CACHE_PATH]. Returns [code]true[/code] ## if the index was found on the file system. static func load_node_index() -> bool: var f := FileAccess.open(NODE_INDEX_CACHE_PATH, FileAccess.READ) if f == null: DeckHolder.logger.log_system("node index file does not exist", Logger.LogType.WARN) return false var data: Dictionary = JSON.parse_string(f.get_as_text()) as Dictionary if data.is_empty(): DeckHolder.logger.log_system("node index file exists, but is empty", Logger.LogType.ERROR) return false for node_type in data: var nd_dict: Dictionary = data[node_type] var nd := NodeDescriptor.from_dictionary(nd_dict) nodes[node_type] = nd DeckHolder.logger.log_system("node index file exists, loaded") return true ## Marks a [member DeckNode.node_type] as "favorite" for use in ## [AddNodeMenu]. static func set_node_favorite(node_type: String, favorite: bool) -> void: if (favorite and node_type in favorite_nodes) or (not favorite and node_type not in favorite_nodes): return if favorite: favorite_nodes.append(node_type) else: favorite_nodes.erase(node_type) var f := FileAccess.open(FAVORITE_NODES_PATH, FileAccess.WRITE) f.store_string(JSON.stringify(favorite_nodes, "\t")) ## Loads the list of favorite [memeber DeckNode.node_type]s from [member FAVORITE_NODES_PATH]. static func load_favorites() -> void: var f := FileAccess.open(FAVORITE_NODES_PATH, FileAccess.READ) if not f: return var data: Array = JSON.parse_string(f.get_as_text()) favorite_nodes.clear() favorite_nodes.assign(data) ## Returns [code]true[/code] if the specified [member DeckNode.node_type] is marked favorite ## by the user. static func is_node_favorite(node_type: String) -> bool: return node_type in favorite_nodes static func is_library(type: String) -> bool: return libraries.has(type) static func get_library_descriptor(type: String) -> NodeDescriptor: return libraries.get(type, null) as NodeDescriptor ## Returns a capitalized category string. static func get_category_capitalization(category: String) -> String: return CATEGORY_CAPITALIZATION.get(category, category.capitalize()) ## Used for storing the shorthand data of a [DeckNode]. class NodeDescriptor: ## Default name of the [DeckNode] type this is storing properties of. var name: String ## The node type of the [DeckNode] reference. See [member DeckNode.node_type]. var type: String ## The description of the [DeckNode] reference. See [member DeckNode.description]. var description: String ## The aliases of the [DeckNode] reference. Stored as a flattened string of ## the [member DeckNode.aliases] array. var aliases: String ## The category of the [DeckNode] reference. See [member DeckNode.category]. var category: String ## Whether this [DeckNode] reference will appear when searching. See [member DeckNode.appears_in_search]. var appears_in_search: bool ## Whether this describes a library group. var is_library: bool ## If [member is_library] is [code]true[/code], this is the index that adds this library. var added_by_library: String ## Stores the path to this node's script for later instantiation. var script_path: String func _init( p_script_path: String, p_name: String, p_type: String, p_description: String, p_aliases: String, p_category: String, p_appears_in_search: bool, p_is_library: bool, ) -> void: script_path = p_script_path name = p_name type = p_type description = p_description aliases = p_aliases category = p_category appears_in_search = p_appears_in_search is_library = p_is_library ## Returns a [Dictionary] representation of this node descriptor. func to_dictionary() -> Dictionary: var d := { "name": name, "type": type, "description": description, "aliases": aliases, "script_path": script_path, "category": category, "appears_in_search": appears_in_search, "is_library": is_library, "added_by_library": added_by_library, } return d ## Creates a new [NodeDB.NodeDescriptor] from a given [Dictionary] of properties. static func from_dictionary(data: Dictionary) -> NodeDescriptor: var nd := NodeDescriptor.new( data.get("script_path", ""), data.get("name", ""), data.get("type", ""), data.get("description", ""), data.get("aliases", ""), data.get("category", ""), data.get("appears_in_search", false), data.get("is_library", false) ) nd.added_by_library = data.get("added_by_library", "") return nd