Skip to content

Instantly share code, notes, and snippets.

@PLyczkowski
Last active February 24, 2018 20:17
Show Gist options
  • Save PLyczkowski/c372b2464e6c8a511f105bd8cdf58a0d to your computer and use it in GitHub Desktop.
Save PLyczkowski/c372b2464e6c8a511f105bd8cdf58a0d to your computer and use it in GitHub Desktop.
GDScript Database
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