miggor-StreamGraph/classes/deck/node_db.gd
Lera Elvoé f720efcc72 lib group storage rework (#162)
saving decks that use lib groups will no longer save the whole file path to that library (at the expense of the structure needing to be the same)

also some ui/ux improvements:
- more menus in sidebar remember their collapsed state between deck/node switches
- adding a lib group will name the group node appropriately
- save dialog properly remembers the most recent path when invoked via ctrl+s and the deck hasn't been saved before

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/162
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-05-26 14:13:42 +00:00

286 lines
8.7 KiB
GDScript

# (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