extends Node # Can store and save node trees, and lists of objects # Each object added receives a unique id, stored here and using set_meta("id", id) # When saving a node tree, the tree's hierarchy is saved as a nested dict of node id's onready var tools = load("res://lib/tools.gd").new(self) const discarded_properties = ["_import_path", "pause_mode", "editor/display_folded", "script", "Node", "Pause", "Script Variables", "object"] # Settings var store_undo_history = true var max_undo = 100 var store_mode # @TODO You can choose from: enum store_mode{MODE_JSON, MODE_TSCN} var parent_mode # Will display dbslots in scene tree var dir var title # Internal var dbslots # {id = dbslot} var objects_to_ids # {object = slot_id} var objects_by_filename # {filename = [object, object]} var objects_by_class_name # {class_name = [object, object]} var undo_history = [[]] # [[json_line, json_line], [json_line, json_line]] json_line is a dict with obj properties stored. First empty json_lines is for empty initial state var undo_amount = 0 # Amount of times the db was undo'ed # Signals signal db_changed signal db_saved func setup(title, dir = null): self.store_mode = MODE_JSON self.parent_mode = true self.title = title self.name = title # This is the name of the node in the tree self.dir = dir dbslots = {} objects_to_ids = {} func _ready(): if !title: breakpoint # Please run setup() on this func add_object(object, built_in_properties_to_save = null, id = null): # This takes care of the object, including freeing! var dbslot = DbSlot.new() dbslot.class_name = object.get_class() # Id if id != null: object.set_meta("id", id) dbslot.id = id elif Array(object.get_meta_list()).has("id") and object.get_meta("id") != null: dbslot.id = object.get_meta("id") else: dbslot.id = _generate_id() object.set_meta("id", dbslot.id) # Script/scene path if object.get_filename() != null and object.get_filename() != "": dbslot.path = object.get_filename() else: var object_script if object.get_script() != null: dbslot.path = object.get_script().get_path() object_script = object.get_script() if object_script != null: dbslot.path = object_script.resource_path # Script path @TODO duplicates with ^ var object_script if object.get_script() != null: dbslot.script_path = object.get_script().get_path() object_script = object.get_script() if object_script != null: dbslot.script_path = object_script.resource_path # Properties dbslot.object = object var properties_names_to_save = [] var properties_array = object.get_property_list() for property in properties_array: if property.usage == 8199: # means the property is exported properties_names_to_save.append(property.name) if built_in_properties_to_save: var all_property_names = tools.get_property_names(object) for property_name in built_in_properties_to_save: if all_property_names.has(property_name): properties_names_to_save.append(property_name) else: prints(title, "error: Tried to save a non existing built in property:", property_name) var properties = {} # name:{t:TYPE_*, v:value} for property_name in properties_names_to_save: var property_value = object.get(property_name) var property_type = typeof(property_value) var property_class # if it's an object # Ignore metadata if property_name == "__meta__": continue # Type conversion if property_type in [TYPE_STRING, TYPE_BOOL, TYPE_NIL, TYPE_INT, TYPE_REAL]: pass elif property_type in [TYPE_DICTIONARY, TYPE_ARRAY]: pass # todo recursive property conversion elif property_type in [TYPE_OBJECT]: if property_value is GDScript: property_class = property_value.get_class() property_value = property_value.get_source_code() elif property_type in [TYPE_VECTOR2]: property_value = var2str(property_value) else: prints("Database", title, "error: Property type not recognized:", property_type) var property = { v = property_value, t = property_type, } if property_class != null: property["c"] = property_class properties[property_name] = property dbslot.props = properties # Register dbslots[dbslot.id] = dbslot objects_to_ids[object] = dbslot.id if dbslot.path: if !objects_by_filename.has(dbslot.get_filename()): objects_by_filename[dbslot.get_filename()] = [object] else: objects_by_filename[dbslot.get_filename()].append(object) if dbslot.class_name: if !objects_by_class_name.has(dbslot.class_name): objects_by_class_name[dbslot.class_name] = [object] else: objects_by_class_name[dbslot.class_name].append(object) # Add slot as child if parent_mode: add_child(dbslot) dbslot.set_name(dbslot.id + " [" + dbslot.get_node_name() + "]") emit_signal("db_changed") func set_dir(dir): self.dir = dir func _update_all_dbslots(): for dbslot in dbslots.values(): _update_dbslot(dbslot) func _update_dbslot(dbslot): # transfer properties to save from object to dbslot if dbslot.get("props") != null and dbslot.props != null: var object = get_object_by_slot(dbslot) if object == null: prints("Database", title, "error: dbslot holds an empty object") prints("Dbslot:") tools.analyze(dbslot) return var updated_properties = {} for property_name in dbslot.props.keys(): var property_value = object.get(property_name) var property_type = typeof(property_value) var property_class # if it's an object # @TODO code duplication with add_object # Type conversion if property_type in [TYPE_STRING, TYPE_BOOL, TYPE_NIL, TYPE_INT, TYPE_REAL]: pass elif property_type in [TYPE_DICTIONARY, TYPE_ARRAY]: pass # todo recursive property conversion elif property_type in [TYPE_OBJECT]: if property_value is GDScript: property_class = property_value.get_class() property_value = property_value.get_source_code() elif property_type in [TYPE_VECTOR2]: property_value = var2str(property_value) else: prints("Database", title, "error: Property type not recognized:", property_type) var updated_property = { v = property_value, t = property_type, } if property_class != null: updated_property["c"] = property_class updated_properties[property_name] = updated_property dbslot.props = updated_properties func clear_and_store_undo(): # Stores an empty undo state store_undo([]) clear() func clear(): for dbslot in dbslots.values(): if parent_mode: remove_child(dbslot) dbslot.queue_free() dbslots = {} objects_to_ids = {} objects_by_filename = {} objects_by_class_name = {} func update(): restore() func restore(from_history = false): # From history is for undo redo if !dir and !from_history: prints("Database", title, "was ordered to be restored without a dir specified") return clear() var json_lines = [] if from_history: # Get from history json_lines = undo_history[undo_amount] else: # Read from dir var file = File.new() if !file.file_exists(dir): return null else: file.open(dir, File.READ) while !file.eof_reached(): var json_line = file.get_line() if json_line != null and json_line.length() > 0: json_lines.append(json_line) file.close() for json_line in json_lines: var dict_from_line = parse_json(json_line) # Create object var new_object if dict_from_line.has("path") and dict_from_line["path"] != null and dict_from_line["path"].length() > 0: var path = dict_from_line["path"] if path.ends_with(".gd"): new_object = load(path).new() elif path.ends_with(".tscn"): new_object = load(path).instance() else: prints("Database", title, "error: Path not recognized:", path) elif dict_from_line.has("class_name") and dict_from_line["class_name"] != null and ClassDB.can_instance(dict_from_line["class_name"]): new_object = ClassDB.instance(dict_from_line["class_name"]) if new_object == null: prints("Database", title, "error: Couldn't instance object from line:", dict_from_line) else: # Id new_object.set_meta("id", dict_from_line["id"]) # Properties var properties = {} if dict_from_line["props"] != null: properties = dict_from_line["props"] for property_name in properties.keys(): var property = properties[property_name] var property_value = property.v var property_type = property.t # Type conversion if property_type in [TYPE_STRING, TYPE_BOOL, TYPE_NIL, TYPE_INT, TYPE_REAL]: pass elif property_type in [TYPE_DICTIONARY, TYPE_ARRAY]: pass # todo recursive property conversion elif property_type in [TYPE_OBJECT]: if property.has("c") and property.c != null: if property.c == "GDScript": var gdscript = GDScript.new() gdscript.set_source_code(property_value) property_value = gdscript else: prints("Database", title, "error: Property class unknown") else: prints("Database", title, "error: Object property doesn't have class defined") elif property_type in [TYPE_VECTOR2]: property_value = str2var(property_value) else: prints("Database", title, "error: Property type not recognized: ", property_type) new_object.set(property_name, property_value) # Register add_object(new_object, properties.keys(), dict_from_line["id"]) #todo this does things twice prints("Database", self.title, "restored", objects_to_ids.keys().size(), "objects") func delete_object(object): var dbslot = get_slot_by_object(object) if dbslot != null: _delete_slot(dbslot) else: prints("Database", title, "error: can't find object to remove") objects_to_ids.erase(object) object.queue_free() #update all id_trees #@TODO only update affected hierarchies for id_tree in _get_slots("id_tree.gd"): var node_tree_root = get_object_by_slot(dbslots[id_tree.get_root_id()]) id_tree.id_tree_dict = _node_tree_to_id_tree(node_tree_root) func get_object(id): return get_object_by_id(id) func get_object_by_id(id): var dbslot = _get_slot(id) if dbslot != null: if dbslot.get("object") != null and dbslot.object != null: return dbslot.object else: prints("Database", title, "error: dbslot doesn't have an object") else: prints("Database", title, "error: dbslot by id not found - ", id) func get_size(): return dbslots.size() func _delete_slot(dbslot): dbslots.erase(dbslot.id) if dbslot.object != null: objects_to_ids.erase(dbslot.object) dbslot.queue_free() func delete_slot_by_id(slot_id): delete_object(_get_slot(slot_id)) func _get_slot(id): if dbslots.keys().has(id): return dbslots[id] else: prints("Database", title, "error: dbslot not found - ", id) func _get_slots(inc_filename): var found_slots = [] for id in dbslots.keys(): if dbslots[id].get_filename() == inc_filename: found_slots.append(dbslots[id]) return found_slots func get_objects_by_filename(inc_filename): if objects_by_filename.has(inc_filename): return objects_by_filename[inc_filename] else: return [] func get_objects_by_class_name(inc_class_name): if objects_by_class_name.has(inc_class_name): return objects_by_class_name[inc_class_name] else: return [] func get_all_objects(): return objects_to_ids.keys() func get_node_tree(title): # returns root node var target_root_slot var target_id_tree var id_trees_slots = _get_slots("id_tree.gd") for id_tree in id_trees_slots: if id_tree.object.title == title: target_id_tree = id_tree.object break if target_id_tree != null: var id_tree_dict = target_id_tree.id_tree_dict if id_tree_dict != null: var target_root_id = target_id_tree.get_root_id() target_root_slot = dbslots[target_root_id] _restore_hierarchy(target_root_slot.object, id_tree_dict[target_root_id]) var target_root_node = get_object_by_slot(target_root_slot) if target_root_node != null: return target_root_node else: prints("Database", title, "error: Retrieving dbslot tree failed") func get_object_by_slot(dbslot): if dbslot != null and dbslot.get("id") != null: for object in objects_to_ids.keys(): if objects_to_ids[object] == dbslot.id: return object else: prints("Database", title, "Not a dbslot! -> ", dbslot) func add_node_tree(node_tree_root, title, properties_to_save = null): add_object(node_tree_root, properties_to_save) _add_nodes_from_tree(node_tree_root) var id_tree = load("res://addons/endless_editor/data_classes/id_tree.gd").new() id_tree.title = title id_tree.id_tree_dict = {objects_to_ids[node_tree_root] : _node_tree_to_id_tree(node_tree_root)} add_object(id_tree) func _node_tree_to_id_tree(root_node): var children = {} for child in root_node.get_children(): if child.get_child_count() > 0: children[objects_to_ids[child]] = _node_tree_to_id_tree(child) else: children[objects_to_ids[child]] = null return children func get_slot_by_object(object): var dbslot if objects_to_ids.has(object): var id = objects_to_ids[object] dbslot = dbslots[id] return dbslot func undo_available(): if store_undo_history and undo_amount < undo_history.size() -1 and undo_amount + 1 <= max_undo: return true else: return false func undo(): if undo_available(): undo_amount += 1 restore(true) else: print("No more undo") return false func redo_available(): if store_undo_history and undo_amount > 0: return true else: return false func redo(): if redo_available(): undo_amount -= 1 restore(true) else: print("No more redo") return false func clear_undo_history(): undo_amount = 0 undo_history = [] func save(): if !dir and !store_undo_history: prints("Database", title, "was ordered to be saved without a dir specified or without storing undo") return # Bake undo for n in range(undo_amount): undo_history.pop_front() undo_amount = 0 # JSON lines (one line per object, saving object properties in json) _update_all_dbslots() var json_lines = [] for dbslot in dbslots.values(): var json_line = dbslot.get_json_line() json_lines.append(json_line) # Save JSON to file if dir: var file = File.new() var status = 0 status = file.open(dir, File.WRITE_READ) if status == OK: for json_line in json_lines: if json_line.length() > 0: file.store_line(json_line) else: prints("Database", title, "error: File write failed:", status) file.close() else: prints("Database", title, "was ordered to be saved without a dir specified") # Store undo if store_undo_history: store_undo(json_lines) # TSCN # Works, but the custom variables have to be exported to be saved with the scene, and must use at least Nodes # var tscn_dir = dir.replace(".bin", ".tscn") # var temp_scene_root = Node.new() # for object in objects_to_ids.keys(): # if object.get("_import_path") != null: #to check if it's at least a node # var object_duplicate = object.duplicate(DUPLICATE_SCRIPTS) # temp_scene_root.add_child(object_duplicate) # object_duplicate.set_owner(temp_scene_root) # var packed_scene = PackedScene.new() # packed_scene.pack(temp_scene_root) # ResourceSaver.save(tscn_dir, packed_scene) # temp_scene_root.queue_free() emit_signal("db_saved") func store_undo(json_lines): # json_lines is an array of json lines with object properties undo_history.push_front(json_lines) if undo_history.size() > max_undo: undo_history.pop_back() func print_db(): prints("Printing db", title, ":") for dbslot in dbslots.values(): prints(dbslot.id, dbslot.class_name, dbslot.get_filename()) # PRIVATE func _generate_id(): randomize() var id = str(randi()) return id # @TODO better method func _restore_hierarchy(node, dict): for id in dict.keys(): var child_node if _get_slot(id).get("object") != null and _get_slot(id).object != null: child_node = _get_slot(id).object if child_node != null: if child_node.get_parent(): child_node.get_parent().remove_child(child_node) node.add_child(child_node) else: prints("Database", title, "error: Restoring hierarchy - node not found") if dict[id] != null: _restore_hierarchy(child_node, dict[id]) func _add_nodes_from_tree(node_tree_root): for child in node_tree_root.get_children(): add_object(child) if child.get_child_count() > 0: _add_nodes_from_tree(child) class DbSlot extends Node: # A dbslot holding an object the database stores var id var path var script_path var class_name # for instance "GraphNode" var props # {"property_name":property_value} var object # if it stores an object func get_filename(): if path != null: return path.right(path.find_last("/") + 1) func get_node_name(): # For debugging pursposes/displaying in scene tree if path != null: return path.right(path.find_last("/") + 1) else: return class_name func get_json_line(): var savedict = {} var props = get_property_list() for property in props: var value = self.get(property.name) if not property.name in discarded_properties: savedict[property.name] = value return to_json(savedict)