Ver Fonte

tools: Add Database

Daniele Bartolini há 9 anos atrás
pai
commit
d3971467db
1 ficheiros alterados com 880 adições e 0 exclusões
  1. 880 0
      tools/core/database.vala

+ 880 - 0
tools/core/database.vala

@@ -0,0 +1,880 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using Gee;
+
+namespace Crown
+{
+	public class Stack
+	{
+		private ArrayList<Value?> _data;
+
+		public Stack()
+		{
+			_data = new ArrayList<Value?>();
+		}
+
+		public void push(Value? value)
+		{
+			_data.add(value);
+		}
+
+		public Value? pop()
+		{
+			return _data.remove_at(_data.size - 1);
+		}
+
+		public Value? peek()
+		{
+			return _data.last();
+		}
+
+		public int size()
+		{
+			return _data.size;
+		}
+
+		public void clear()
+		{
+			_data.clear();
+		}
+	}
+
+	public class Database
+	{
+		private abstract class ChangeAction
+		{
+		}
+
+		private class CreateAction : ChangeAction
+		{
+			public Guid _id;
+
+			public CreateAction(Guid id)
+			{
+				_id = id;
+			}
+		}
+
+		private class DestroyAction : ChangeAction
+		{
+			public Guid _id;
+
+			public DestroyAction(Guid id)
+			{
+				_id = id;
+			}
+		}
+
+		private class SetPropertyAction : ChangeAction
+		{
+			public Guid _id;
+			public string _key;
+			public Value? _value;
+
+			public SetPropertyAction(Guid id, string key, Value? value)
+			{
+				_id = id;
+				_key = key;
+				_value = value;
+			}
+		}
+
+		private class AddToSetAction : ChangeAction
+		{
+			public Guid _id;
+			public string _key;
+			public Guid _item_id;
+
+			public AddToSetAction(Guid id, string key, Guid item_id)
+			{
+				_id = id;
+				_key = key;
+				_item_id = item_id;
+			}
+		}
+
+		private class RemoveFromSetAction : ChangeAction
+		{
+			public Guid _id;
+			public string _key;
+			public Guid _item_id;
+
+			public RemoveFromSetAction(Guid id, string key, Guid item_id)
+			{
+				_id = id;
+				_key = key;
+				_item_id = item_id;
+			}
+		}
+
+		public delegate void ActionCallback(bool undo, int id, Value? data);
+
+		private class RestorePoint : ChangeAction
+		{
+			public ActionCallback _undo_redo;
+			public int _id;
+			public Value? _data;
+
+			public RestorePoint(ActionCallback undo_redo, int id, Value? data)
+			{
+				_undo_redo = undo_redo;
+				_id = id;
+				_data = data;
+			}
+
+			public void Redo()
+			{
+				if (_undo_redo != null)
+					_undo_redo(false, _id, _data);
+			}
+
+			public void Undo()
+			{
+				if (_undo_redo != null)
+					_undo_redo(true, _id, _data);
+			}
+		}
+
+		// Data
+		private HashMap<string, Value?> _data;
+		private Stack _undo;
+		private Stack _redo;
+		private bool _changed;
+
+		// Signals
+		public signal void key_changed(Guid id, string key);
+
+		public Database()
+		{
+			_data = new HashMap<string, Value?>();
+			_undo = new Stack();
+			_redo = new Stack();
+
+			reset();
+		}
+
+		/// <summary>
+		/// Resets database to clean state.
+		/// </summary>
+		public void reset()
+		{
+			_data.clear();
+			_undo.clear();
+			_redo.clear();
+
+			_changed = false;
+
+			// This is a special field which stores all objects
+			_data.set("_objects", new HashMap<string, Value?>());
+		}
+
+		/// <summary>
+		/// Returns whether the database has been changed since last call to Save().
+		/// </summary>
+		public bool changed()
+		{
+			return _changed;
+		}
+
+		/// <summary>
+		/// Saves database to path.
+		/// </summary>
+		public void save(string path)
+		{
+			Hashtable json = encode();
+			SJSON.save(json, path);
+			_changed = false;
+		}
+
+		/// <summary>
+		/// Loads database from path.
+		/// </summary>
+		public void load(string path)
+		{
+			Hashtable json = SJSON.load(path);
+			decode(json);
+			_changed = false;
+		}
+
+		private Hashtable encode()
+		{
+			return encode_object(_data);
+		}
+
+		private static bool is_valid_value(Value? value)
+		{
+			return value == null
+				|| value.holds(typeof(bool))
+				|| value.holds(typeof(double))
+				|| value.holds(typeof(string))
+				|| value.holds(typeof(Guid))
+				|| value.holds(typeof(Vector3))
+				|| value.holds(typeof(Quaternion))
+				;
+		}
+
+		private static bool is_valid_key(string key)
+		{
+			return key.length > 0
+				&& key != "_objects"
+				&& !key.has_prefix(".")
+				&& !key.has_suffix(".")
+				;
+		}
+
+		private static string value_to_string(Value? value)
+		{
+			if (value == null)
+				return "null";
+			if (value.holds(typeof(bool)))
+				return ((bool)value).to_string();
+			if (value.holds(typeof(double)))
+				return ((double)value).to_string();
+			if (value.holds(typeof(string)))
+				return ((string)value).to_string();
+			if (value.holds(typeof(Guid)))
+				return ((Guid)value).to_string();
+			if (value.holds(typeof(Vector3)))
+				return ((Vector3)value).to_string();
+			if (value.holds(typeof(Quaternion)))
+				return ((Quaternion)value).to_string();
+			if (value.holds(typeof(HashSet<Guid?>)))
+				return "Set<Guid>";
+
+			return "<invalid>";
+		}
+
+		private void decode(Hashtable json)
+		{
+			reset();
+			decode_root_object(json);
+		}
+
+		private void decode_root_object(Hashtable json)
+		{
+			decode_object(GUID_ZERO, "", json);
+		}
+
+		private void decode_object(Guid id, string db_key, Hashtable json)
+		{
+			string old_db = db_key;
+			string k = db_key;
+
+			string[] keys = json.keys.to_array();
+			foreach (string key in keys)
+			{
+				assert(key != "_objects");
+
+				Value? val = json[key];
+
+				k += k == "" ? key : ("." + key);
+
+				if (val.holds(typeof(Hashtable)))
+				{
+					Hashtable ht = (Hashtable)val;
+					if (is_set(ht))
+						decode_set(id, key, ht);
+					else
+						decode_object(id, k, ht);
+				}
+				else
+				{
+					set_property_internal(id, k, decode_value(val));
+				}
+
+				k = old_db;
+			}
+		}
+
+		private bool is_set(Hashtable json)
+		{
+			string[] keys = json.keys.to_array();
+			foreach (string k in keys)
+			{
+				Guid guid;
+				if (!Guid.try_parse(k, out guid))
+					return false;
+			}
+
+			return true;
+		}
+
+		private void decode_set(Guid id, string key, Hashtable json)
+		{
+			create_empty_set(id, key);
+
+			string[] keys = json.keys.to_array();
+			foreach (string k in keys)
+			{
+				Guid item_id = Guid.parse(k);
+				create_internal(item_id);
+				decode_object(item_id, "", (Hashtable)json[k]);
+				add_to_set_internal(id, key, item_id);
+			}
+		}
+
+		private Value? decode_value(Value? value)
+		{
+			if (value.holds(typeof(ArrayList<Value?>)))
+			{
+				ArrayList<Value?> al = (ArrayList<Value?>)value;
+				if (al.size == 3)
+					return Vector3((double)al[0], (double)al[1], (double)al[2]);
+				else if (al.size == 4)
+					return Quaternion((double)al[0], (double)al[1], (double)al[2], (double)al[3]);
+				else
+					assert(false);
+			}
+			else if (value.holds(typeof(string)))
+			{
+				Guid id;
+				if (Guid.try_parse((string)value, out id))
+					return id;
+				return value;
+			}
+			else if (value == null || value.holds(typeof(bool)) || value.holds(typeof(double)))
+			{
+				return value;
+			}
+			else
+			{
+				assert(false);
+			}
+
+			return null;
+		}
+
+		private Hashtable encode_object(HashMap<string, Value?> db)
+		{
+			Hashtable obj = new Hashtable();
+
+			string[] keys = db.keys.to_array();
+			foreach (string key in keys)
+			{
+				if (key == "_objects")
+					continue;
+
+				string[] foo = key.split(".");
+				Hashtable x = obj;
+				if (foo.length > 1)
+				{
+					for (int i = 0; i < foo.length - 1; ++i)
+					{
+						string f = foo[i];
+
+						if (x.has_key(f))
+						{
+							x = (Hashtable)x[f];
+							continue;
+						}
+
+						Hashtable y = new Hashtable();
+						x.set(f, y);
+						x = y;
+					}
+				}
+				x.set(foo[foo.length-1], encode_value(db[key]));
+			}
+
+			return obj;
+		}
+
+		private Value? encode_value(Value? value)
+		{
+			assert(is_valid_value(value) || value.holds(typeof(HashSet<Guid?>)));
+
+			if (value.holds(typeof(Vector3)))
+			{
+				Vector3 v = (Vector3)value;
+				ArrayList<Value?> arr = new Gee.ArrayList<Value?>();
+				arr.add(v.x);
+				arr.add(v.y);
+				arr.add(v.z);
+				return arr;
+			}
+			else if (value.holds(typeof(Quaternion)))
+			{
+				Quaternion q = (Quaternion)value;
+				ArrayList<Value?> arr = new Gee.ArrayList<Value?>();
+				arr.add(q.x);
+				arr.add(q.y);
+				arr.add(q.z);
+				arr.add(q.w);
+				return arr;
+			}
+			else if (value.holds(typeof(Guid)))
+			{
+				Guid id = (Guid)value;
+				return "\"%s\"".printf(id.to_string());
+			}
+			else if (value.holds(typeof(HashSet<Guid?>)))
+			{
+				HashSet<Guid?> hs = (HashSet<Guid?>)value;
+				Hashtable ht = new Hashtable();
+				foreach (Guid id in hs)
+				{
+					HashMap<string, Value?> objs = (HashMap<string, Value?>)_data["_objects"];
+					ht.set(id.to_string(), encode_object((HashMap<string, Value?>)objs[id.to_string()]));
+				}
+				return ht;
+			}
+			else
+			{
+				return value;
+			}
+		}
+
+		private HashMap<string, Value?> get_data(Guid id)
+		{
+			assert(has_object(id));
+
+			return (HashMap<string, Value?>)(id == GUID_ZERO ? _data : (_data["_objects"] as HashMap<string, Value?>)[id.to_string()]);
+		}
+
+		private void create_internal(Guid id)
+		{
+			assert(id != GUID_ZERO);
+#if CROWN_DEBUG
+			stdout.printf("create %s\n", id.to_string());
+#endif // CROWN_DEBUG
+			(_data["_objects"] as HashMap<string, Value?>).set(id.to_string(), new HashMap<string, Value?>());
+
+			_changed = true;
+			key_changed(id, "_objects");
+		}
+
+		private void destroy_internal(Guid id)
+		{
+			assert(id != GUID_ZERO);
+			assert(has_object(id));
+#if CROWN_DEBUG
+			stdout.printf("destroy %s\n", id.to_string());
+#endif // CROWN_DEBUG
+			(_data["_objects"] as HashMap<string, Value?>).unset(id.to_string());
+
+			_changed = true;
+			key_changed(id, "_objects");
+		}
+
+		private void set_property_internal(Guid id, string key, Value? value)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+			assert(is_valid_value(value));
+#if CROWN_DEBUG
+			stdout.printf("set_property %s %s %s\n"
+				, id.to_string()
+				, key
+				, (value == null) ? "null" : value_to_string(value)
+				);
+#endif // CROWN_DEBUG
+			HashMap<string, Value?> ob = get_data(id);
+			ob[key] = value;
+
+			_changed = true;
+			key_changed(id, key);
+		}
+
+		private void create_empty_set(Guid id, string key)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+
+			HashMap<string, Value?> ob = get_data(id);
+			assert(!ob.has_key(key));
+
+			ob[key] = new HashSet<Guid?>(Guid.hash_func, Guid.equal_func);
+		}
+
+		private void add_to_set_internal(Guid id, string key, Guid item_id)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+			assert(item_id != GUID_ZERO);
+			assert(has_object(item_id));
+#if CROWN_DEBUG
+			stdout.printf("add_to_set %s %s %s\n"
+				, id.to_string()
+				, key
+				, item_id.to_string()
+				);
+#endif // CROWN_DEBUG
+			HashMap<string, Value?> ob = get_data(id);
+
+			if (!ob.has_key(key))
+			{
+				HashSet<Guid?> hs = new HashSet<Guid?>(Guid.hash_func, Guid.equal_func);
+				hs.add(item_id);
+				ob[key] = hs;
+			}
+			else
+			{
+				(ob[key] as HashSet<Guid?>).add(item_id);
+			}
+
+			_changed = true;
+			key_changed(id, key);
+		}
+
+		private void remove_from_set_internal(Guid id, string key, Guid item_id)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+			assert(item_id != GUID_ZERO);
+#if CROWN_DEBUG
+			stdout.printf("remove_from_set %s %s %s\n"
+				, id.to_string()
+				, key
+				, item_id.to_string()
+				);
+#endif // CROWN_DEBUG
+			HashMap<string, Value?> ob = get_data(id);
+			(ob[key] as HashSet<Guid?>).remove(item_id);
+
+			_changed = true;
+			key_changed(id, key);
+		}
+
+		public void create(Guid id)
+		{
+			assert(id != GUID_ZERO);
+			assert(!has_object(id));
+
+			_undo.push(new DestroyAction(id));
+			_redo.clear();
+
+			create_internal(id);
+		}
+
+		public void destroy(Guid id)
+		{
+			assert(id != GUID_ZERO);
+			assert(has_object(id));
+
+
+			HashMap<string, Value?> o = get_data(id);
+			string[] keys = o.keys.to_array();
+
+			foreach (string key in keys)
+			{
+				Value? value = o[key];
+				if (value.holds(typeof(HashSet<Guid?>)))
+				{
+					HashSet<Guid?> hs = (HashSet<Guid?>)value;
+					Guid?[] ids = hs.to_array();
+					foreach (Guid item_id in ids)
+					{
+						remove_from_set(id, key, item_id);
+						destroy(item_id);
+					}
+				}
+				else
+				{
+					set_property(id, key, null);
+				}
+			}
+
+			_undo.push(new CreateAction(id));
+			_redo.clear();
+
+			destroy_internal(id);
+		}
+
+		public void set_property(Guid id, string key, Value? value)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+			assert(is_valid_value(value));
+
+			HashMap<string, Value?> ob = get_data(id);
+			_undo.push(new SetPropertyAction(id, key, ob.has_key(key) ? ob[key] : null));
+			_redo.clear();
+
+			set_property_internal(id, key, value);
+		}
+
+		public void add_to_set(Guid id, string key, Guid item_id)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+			assert(item_id != GUID_ZERO);
+			assert(has_object(item_id));
+
+			_undo.push(new RemoveFromSetAction(id, key, item_id));
+			_redo.clear();
+
+			add_to_set_internal(id, key, item_id);
+		}
+
+		public void remove_from_set(Guid id, string key, Guid item_id)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+			assert(item_id != GUID_ZERO);
+
+			_undo.push(new AddToSetAction(id, key, item_id));
+			_redo.clear();
+
+			remove_from_set_internal(id, key, item_id);
+		}
+
+		public bool has_object(Guid id)
+		{
+			bool contains = (_data["_objects"] as HashMap<string, Value?>).has_key(id.to_string());
+			return id == GUID_ZERO || contains;
+		}
+
+		public bool has_property(Guid id, string key)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+
+			return get_data(id).has_key(key);
+		}
+
+		public Value? get_property(Guid id, string key)
+		{
+			assert(has_object(id));
+			assert(is_valid_key(key));
+
+			HashMap<string, Value?> ob = get_data(id);
+			Value? value = (ob.has_key(key) ? ob[key] : null);
+#if CROWN_DEBUG
+			stdout.printf("get_property %s %s %s\n"
+				, id.to_string()
+				, key
+				, (value == null) ? "null" : value_to_string(value)
+				);
+#endif // CROWN_DEBUG
+			return value;
+		}
+
+		public HashMap<string, Value?> get_object(Guid id)
+		{
+			return (HashMap<string, Value?>)get_data(GUID_ZERO)[id.to_string()];
+		}
+
+		public string[] get_keys(Guid id)
+		{
+			HashMap<string, Value?> data = get_data(id);
+			return data.keys.to_array();
+		}
+
+		public void add_restore_point(ActionCallback? undo_redo = null, int id = -1, Value? data = null)
+		{
+#if CROWN_DEBUG
+			stdout.printf("add_restore_point %d\n", id);
+#endif // CROWN_DEBUG
+			_undo.push(new RestorePoint(undo_redo, id, data));
+		}
+
+		/// <summary>
+		/// Duplicates the object specified by id and assign new_id to the duplicated object.
+		/// </summary>
+		public void duplicate(Guid id, Guid new_id)
+		{
+			assert(id != GUID_ZERO);
+			assert(new_id != GUID_ZERO);
+			assert(id != new_id);
+			assert(has_object(id));
+
+			create(new_id);
+
+			HashMap<string, Value?> o = get_data(id);
+			string[] keys = o.keys.to_array();
+			foreach (string key in keys)
+			{
+				Value? val = o[key];
+				if (val.holds(typeof(HashSet<Guid?>)))
+				{
+					HashSet<Guid?> hs = (HashSet<Guid?>)val;
+					foreach (Guid j in hs)
+					{
+						Guid x = Guid.new_guid();
+						duplicate(j, x);
+						add_to_set(new_id, key, x);
+					}
+				}
+				else
+				{
+					set_property(new_id, key, o[key]);
+				}
+			}
+		}
+
+		/// <summary>
+		/// Copies the database to db under the given new_key.
+		/// </summary>
+		public void copy_to(Database db, string new_key)
+		{
+			assert(db != null);
+			assert(is_valid_key(new_key));
+
+			copy_deep(db, GUID_ZERO, new_key);
+		}
+
+		public void copy_deep(Database db, Guid id, string new_key)
+		{
+			HashMap<string, Value?> o = get_data(id);
+			string[] keys = o.keys.to_array();
+			foreach (string key in keys)
+			{
+				if (key == "_objects")
+					continue;
+
+				Value? value = o[key];
+				if (value.holds(typeof(HashSet<Guid?>)))
+				{
+					HashSet<Guid?> hs = (HashSet<Guid?>)value;
+					foreach (Guid j in hs)
+					{
+						db.create(j);
+						copy_deep(db, j, "");
+						db.add_to_set(id, new_key + (new_key == "" ? "" : ".") + key, j);
+					}
+				}
+				else
+				{
+					db.set_property(id, new_key + (new_key == "" ? "" : ".") + key, o[key]);
+				}
+			}
+		}
+
+		public void undo_single_action()
+		{
+			RestorePoint begin_action = null;
+
+			while (true)
+			{
+				if (_undo.size() == 0)
+				{
+					if (begin_action != null)
+					{
+						_redo.push(begin_action);
+						begin_action.Undo();
+					}
+					return;
+				}
+
+				Value? action = _undo.peek();
+				if (action.holds(typeof(RestorePoint)))
+				{
+					if (begin_action == null)
+					{
+						begin_action = (RestorePoint)action;
+						_undo.pop();
+						continue;
+					}
+					else
+					{
+						_redo.push(begin_action);
+						begin_action.Undo();
+						return;
+					}
+				}
+				else if (action.holds(typeof(CreateAction)))
+				{
+					CreateAction a = (action as CreateAction);
+					_redo.push(new DestroyAction(a._id));
+					create_internal(a._id);
+				}
+				else if (action.holds(typeof(DestroyAction)))
+				{
+					DestroyAction a = (action as DestroyAction);
+					_redo.push(new CreateAction(a._id));
+					destroy_internal(a._id);
+				}
+				else if (action.holds(typeof(SetPropertyAction)))
+				{
+					SetPropertyAction a = (action as SetPropertyAction);
+					_redo.push(new SetPropertyAction(a._id, a._key, get_data(a._id).has_key(a._key) ? get_data(a._id)[a._key] : null));
+					set_property_internal(a._id, a._key, a._value);
+				}
+				else if (action.holds(typeof(AddToSetAction)))
+				{
+					AddToSetAction a = (action as AddToSetAction);
+					_redo.push(new RemoveFromSetAction(a._id, a._key, a._item_id));
+					add_to_set_internal(a._id, a._key, a._item_id);
+				}
+				else if (action.holds(typeof(RemoveFromSetAction)))
+				{
+					RemoveFromSetAction a = (action as RemoveFromSetAction);
+					_redo.push(new AddToSetAction(a._id, a._key, a._item_id));
+					remove_from_set_internal(a._id, a._key, a._item_id);
+				}
+
+				_undo.pop();
+			}
+		}
+
+		public void redo_single_action()
+		{
+			RestorePoint begin_action = null;
+
+			while (true)
+			{
+				if (_redo.size() == 0)
+				{
+					if (begin_action != null)
+					{
+						_undo.push(begin_action);
+						begin_action.Redo();
+					}
+					return;
+				}
+
+				Value? action = _redo.peek();
+				if (action.holds(typeof(RestorePoint)))
+				{
+					if (begin_action == null)
+					{
+						begin_action = (RestorePoint)action;
+						_redo.pop();
+						continue;
+					}
+					else
+					{
+						_undo.push(begin_action);
+						begin_action.Redo();
+						return;
+					}
+				}
+				else if (action.holds(typeof(CreateAction)))
+				{
+					CreateAction a = (action as CreateAction);
+					_undo.push(new DestroyAction(a._id));
+					create_internal(a._id);
+				}
+				else if (action.holds(typeof(DestroyAction)))
+				{
+					DestroyAction a = (action as DestroyAction);
+					_undo.push(new CreateAction(a._id));
+					destroy_internal(a._id);
+				}
+				else if (action.holds(typeof(SetPropertyAction)))
+				{
+					SetPropertyAction a = (action as SetPropertyAction);
+					_undo.push(new SetPropertyAction(a._id, a._key, get_data(a._id).has_key(a._key) ? get_data(a._id)[a._key] : null));
+					set_property_internal(a._id, a._key, a._value);
+				}
+				else if (action.holds(typeof(AddToSetAction)))
+				{
+					AddToSetAction a = (action as AddToSetAction);
+					_undo.push(new RemoveFromSetAction(a._id, a._key, a._item_id));
+					add_to_set_internal(a._id, a._key, a._item_id);
+				}
+				else if (action.holds(typeof(RemoveFromSetAction)))
+				{
+					RemoveFromSetAction a = (action as RemoveFromSetAction);
+					_undo.push(new AddToSetAction(a._id, a._key, a._item_id));
+					remove_from_set_internal(a._id, a._key, a._item_id);
+				}
+
+				_redo.pop();
+			}
+		}
+	}
+}