# (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 DeckHolder ## @experimental ## A static class holding references to all decks opened in the current session. ## List of decks opened this session. #static var decks: Array[Deck] static var decks: Dictionary # Dictionary[String -> id, (Deck|Dictionary[String -> instance_id, Deck])] ## List of library groups open this session. static var lib_groups: Dictionary # Dictionary[String -> path, Dictionary[String -> instance_id, Deck]] static var groups_emitted: Array[String] static var logger := Logger.new() static var signals := Signals.new() enum Compat { CONNECTIONS_IN_DECK = 1 << 0, } static func _static_init() -> void: NodeDB.init() ## Returns a new empty deck and assigns a new random ID to it. static func add_empty_deck() -> Deck: var deck := Deck.new() var uuid := UUID.v4() decks[uuid] = deck deck.id = uuid signals.deck_added.emit(uuid) print_verbose("DeckHolder: added empty deck %s, id %s" % [deck.id, deck.get_instance_id()]) return deck static func get_deck_compat(data: Dictionary) -> int: var res := 0 if not data.deck.has("connections"): res |= Compat.CONNECTIONS_IN_DECK return res static func apply_compat_patches(data: Dictionary, compat: int) -> void: if compat & Compat.CONNECTIONS_IN_DECK: # convert pre-0.0.6 connections to be stored in the deck instead var connections := Deck.NodeConnections.new() for node: String in data.deck.nodes: for from_port in data.deck.nodes[node].outgoing_connections: for to_node: String in data.deck.nodes[node].outgoing_connections[from_port]: for to_port: int in data.deck.nodes[node].outgoing_connections[from_port][to_node]: connections.add_connection(node, to_node, int(from_port), to_port) data.deck.connections = connections.to_dict() ## Opens a deck from the [param path]. static func open_deck_from_file(path: String) -> Deck: var f := FileAccess.open(path, FileAccess.READ) if f.get_error() != OK: return null var deck := open_deck_from_dict(JSON.parse_string(f.get_as_text()), path) return deck static func open_deck_from_dict(data: Dictionary, path := "") -> Deck: if get_deck_compat(data) != 0: apply_compat_patches(data, get_deck_compat(data)) var deck := Deck.from_dict(data, path) decks[deck.id] = deck signals.deck_added.emit(deck.id) print_verbose("DeckHolder: opened deck %s, id %s" % [deck.id, deck.get_instance_id()]) return deck static func add_group_from_dict(data: Dictionary, deck_id: String, instance_id: String, parent: String = "") -> Deck: var group := Deck.from_dict(data) group.instance_id = instance_id group.is_group = true group._belonging_to = parent group.group_input_node = data.deck.group_input_node group.group_output_node = data.deck.group_output_node var instances: Dictionary = decks.get(deck_id, {}) instances[instance_id] = group decks[deck_id] = instances connect_group_signals(group) if deck_id not in groups_emitted: signals.deck_added.emit(deck_id) groups_emitted.append(deck_id) #print(decks) print_verbose("DeckHolder: added group instance %s::%s, id %s" % [group.id, group.instance_id, group.get_instance_id()]) return group static func make_new_group_instance(group_id: String, parent: String = "") -> Deck: var group := get_deck(group_id) var data := group.to_dict() var inst := add_group_from_dict(data, group_id, UUID.v4(), parent) # copy connections inst.connections = group.connections return inst static func make_group_instance_unique(group_id: String, instance_id: String, parent_deck_id: String, group_node_id: String) -> Deck: if not decks.has(group_id): return null var group_node := get_deck(parent_deck_id).get_node(group_node_id) var inst := get_group_instance(group_id, instance_id) var new_id := UUID.v4() var new_inst_id := UUID.v4() inst.id = new_id group_node.group_id = new_id group_node.group_instance_id = new_inst_id var data := inst.to_dict() #data.instance_id = new_inst_id (decks[group_id] as Dictionary).erase(instance_id) var new_instance := add_group_from_dict(data, new_id, new_inst_id, parent_deck_id) group_node.init_io() #TODO: some weird close bug var instances := inst.get_referenced_group_instances() for instance: Dictionary in instances: make_group_instance_unique(instance.group_id, instance.instance_id, instance.parent_id, instance.group_node) return new_instance static func add_empty_group(parent: String = "") -> Deck: var group := Deck.new() group.is_group = true group.id = UUID.v4() group._belonging_to = parent group.instance_id = UUID.v4() decks[group.id] = {group.instance_id: group} connect_group_signals(group) if group.id not in groups_emitted: signals.deck_added.emit(group.id) groups_emitted.append(group.id) print_verbose("DeckHolder: added empty group %s::%s, id %s" % [group.id, group.instance_id, group.get_instance_id()]) return group static func add_lib_instance(type: String, to_deck: String, instance_id: String = UUID.v4()) -> Deck: var nd: NodeDB.NodeDescriptor = NodeDB.libraries[type] var path := nd.script_path var deck_data: Dictionary if lib_groups.has(path): deck_data = (lib_groups[path].values()[0] as Deck).to_dict() else: if not NodeDB.libraries.has(type): return null var f := FileAccess.open(path, FileAccess.READ) if not f: return null deck_data = JSON.parse_string(f.get_as_text()) var instances := lib_groups.get(path, {}) as Dictionary var deck := Deck.from_dict(deck_data) deck.id = path deck.instance_id = instance_id deck.is_group = true deck.is_library = true deck._belonging_to = to_deck instances[instance_id] = deck lib_groups[path] = instances print_verbose("DeckHolder: added lib group %s::%s, id %s" % [deck.id, deck.instance_id, deck.get_instance_id()]) return deck static func connect_group_signals(group: Deck) -> void: group.node_added_to_group.connect(DeckHolder._on_node_added_to_group) group.node_removed_from_group.connect(DeckHolder._on_node_removed_from_group) group.node_port_value_updated.connect(DeckHolder._on_node_port_value_updated) group.node_renamed.connect(DeckHolder._on_node_renamed) group.node_moved.connect(DeckHolder._on_node_moved) static func get_deck(id: String) -> Deck: if not decks.has(id): return get_lib(id) if not decks[id] is Dictionary: return decks[id] else: return (decks[id] as Dictionary).values()[0] static func get_lib(path: String) -> Deck: if not lib_groups.has(path): return null return (lib_groups[path] as Dictionary).values()[0] static func get_group_instance(group_id: String, instance_id: String) -> Deck: if not decks.has(group_id): return get_lib_instance(group_id, instance_id) if decks[group_id] is Dictionary: return (decks[group_id] as Dictionary).get(instance_id) else: return null static func get_lib_instance(path: String, instance_id: String) -> Deck: if not lib_groups.has(path): return null return (lib_groups[path] as Dictionary).get(instance_id, null) static func close_group_instance(group_id: String, instance_id: String) -> void: # this is kinda dumb, but to close groups that may be dangling # when all instances are closed, we have to get that list # *before* we close the instance var dangling_groups := get_deck(group_id).get_referenced_groups() var from := lib_groups if group_id.is_absolute_path() else decks var group_instances: Dictionary = from.get(group_id, {}) as Dictionary var group_inst: Deck = group_instances[instance_id] print_verbose("DeckHolder: freeing group instance %s::%s, id %s" % [group_inst.id, group_inst.instance_id, group_inst.get_instance_id()]) group_inst.pre_exit_cleanup() group_inst.free() group_instances.erase(instance_id) if group_instances.is_empty(): for group in dangling_groups: close_all_group_instances(group) signals.deck_closed.emit(group_id) groups_emitted.erase(group_id) from.erase(group_id) static func close_all_group_instances(group_id: String) -> void: var from := lib_groups if group_id.is_absolute_path() else decks if from.get(group_id) is Dictionary: for instance_id: String in from[group_id]: print_verbose("DeckHolder: freeing group instance %s::%s, id %s" % [group_id, instance_id, from[group_id][instance_id].get_instance_id()]) from[group_id][instance_id].pre_exit_cleanup() from[group_id][instance_id].free() from.erase(group_id) ## Unloads a deck. Returns a list of groups that are closed as a result of ## closing this deck. static func close_deck(deck_id: String) -> Array: if decks.get(deck_id) is Deck: var deck: Deck = decks[deck_id] as Deck var groups := deck.get_referenced_groups() for group in groups: close_all_group_instances(group) signals.deck_closed.emit(deck_id) decks.erase(deck_id) print_verbose("DeckHolder: freeing deck %s, id %s" % [deck.id, deck.get_instance_id()]) deck.pre_exit_cleanup() deck.free() return groups return [] static func pre_exit_cleanup() -> void: for deck_id: String in decks: if decks[deck_id] is Deck: var deck: Deck = decks[deck_id] print_verbose("DeckHolder: freeing deck %s, id %s" % [deck.id, deck.get_instance_id()]) deck.pre_exit_cleanup() deck.free() else: for instance_id: String in decks[deck_id]: var deck: Deck = decks[deck_id][instance_id] print_verbose("DeckHolder: freeing group %s::%s, id %s" % [deck_id, instance_id, deck.get_instance_id()]) deck.pre_exit_cleanup() deck.free() for lib_id: String in lib_groups: for instance_id: String in lib_groups[lib_id]: var deck: Deck = lib_groups[lib_id][instance_id] print_verbose("DeckHolder: freeing lib group %s::%s, id %s" % [lib_id, instance_id, deck.get_instance_id()]) deck.pre_exit_cleanup() deck.free() static func send_event(event_name: StringName, event_data: Dictionary = {}) -> void: for deck_id: String in decks: if decks[deck_id] is Deck: (decks[deck_id] as Deck).send_event(event_name, event_data) else: for deck_instance_id: String in decks[deck_id]: (decks[deck_id][deck_instance_id] as Deck).send_event(event_name, event_data) for lib_group_id: String in lib_groups: for lib_instance_id: String in lib_groups[lib_group_id]: (lib_groups[lib_group_id][lib_instance_id] as Deck).send_event(event_name, event_data) #region group signal callbacks static func _on_node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck) -> void: var group_id := deck.id for instance_id: String in decks[group_id]: if instance_id == deck.instance_id: continue var instance: Deck = get_group_instance(group_id, instance_id) instance.emit_group_signals = false var node_duplicate := DeckNode.from_dict(node.to_dict()) instance.add_node_inst(node_duplicate, assign_id, assign_to_self) instance.emit_group_signals = true static func _on_node_removed_from_group(node_id: String, remove_connections: bool, deck: Deck) -> void: var group_id := deck.id for instance_id: String in decks[group_id]: if instance_id == deck.instance_id: continue var instance: Deck = get_group_instance(group_id, instance_id) instance.emit_group_signals = false instance.remove_node(node_id, remove_connections) instance.emit_group_signals = true static func _on_node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck) -> void: var group_id := deck.id for instance_id: String in decks[group_id]: if instance_id == deck.instance_id: continue var instance: Deck = get_group_instance(group_id, instance_id) instance.emit_group_signals = false instance.get_node(node_id).get_all_ports()[port_idx].set_value_no_signal(new_value) instance.emit_group_signals = true static func _on_node_renamed(node_id: String, new_name: String, deck: Deck) -> void: var group_id := deck.id for instance_id: String in decks[group_id]: if instance_id == deck.instance_id: continue var instance: Deck = get_group_instance(group_id, instance_id) instance.emit_group_signals = false instance.get_node(node_id).name = new_name instance.emit_group_signals = true static func _on_node_moved(node_id: String, new_position: Dictionary, deck: Deck) -> void: var group_id := deck.id for instance_id: String in decks[group_id]: if instance_id == deck.instance_id: continue var instance: Deck = get_group_instance(group_id, instance_id) instance.emit_group_signals = false instance.get_node(node_id).position = new_position.duplicate() instance.emit_group_signals = true #endregion class Signals: signal deck_added(deck_id: String) signal deck_closed(deck_id: String)