Last active
February 24, 2018 20:17
-
-
Save PLyczkowski/c372b2464e6c8a511f105bd8cdf58a0d to your computer and use it in GitHub Desktop.
GDScript Database
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment