ソースを参照

Add LevelEditor

Daniele Bartolini 9 年 前
コミット
42213db526

+ 17 - 0
tools/level_editor/action_type.vala

@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+namespace Crown
+{
+	public enum ActionType
+	{
+		SPAWN_UNIT,
+		DESTROY_UNIT,
+		SPAWN_SOUND,
+		DESTROY_SOUND,
+		MOVE_OBJECTS,
+		DUPLICATE_OBJECTS
+	}
+}

+ 664 - 0
tools/level_editor/level.vala

@@ -0,0 +1,664 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using GLib;
+using Gee;
+
+namespace Crown
+{
+	public class Level
+	{
+		// Project paths
+		private string _source_dir;
+
+		// Engine connections
+		private ConsoleClient _client;
+
+		// Data
+		private Database _db;
+		private Database _prefabs;
+		private Gee.HashSet<string> _loaded_prefabs;
+		private Gee.ArrayList<Guid?> _selection;
+
+		// Signals
+		public signal void selection_changed(Gee.ArrayList<Guid?> selection);
+
+		public Level(Database db, ConsoleClient client, string source_dir)
+		{
+			// Project paths
+			_source_dir = source_dir;
+
+			// Engine connections
+			_client = client;
+
+			// Data
+			_db = db;
+			_prefabs = new Database();
+			_loaded_prefabs = new Gee.HashSet<string>();
+			_selection = new Gee.ArrayList<Guid?>();
+		}
+
+		public void reset()
+		{
+			_db.reset();
+			_prefabs.reset();
+			_loaded_prefabs.clear();
+
+			_selection.clear();
+			selection_changed(_selection);
+
+			_client.send_script(LevelEditorAPI.reset());
+		}
+
+		public void selection(Guid[] ids)
+		{
+			_selection.clear();
+			foreach (Guid id in ids)
+				_selection.add(id);
+
+			selection_changed(_selection);
+		}
+
+		public void new_level()
+		{
+			load(_source_dir + "core/levels/empty.level");
+		}
+
+		public void load(string path)
+		{
+			reset();
+			_db.load(path);
+		}
+
+		public void save(string path)
+		{
+			_db.save(path);
+		}
+
+		/// <summary>
+		/// Loads the prefab name into the database of prefabs.
+		/// </summary>
+		public void load_prefab(string name)
+		{
+			if (_loaded_prefabs.contains(name))
+				return;
+
+			Database prefab_db = new Database();
+			prefab_db.load(_source_dir + "/" + name + ".unit");
+
+			Value? prefab = prefab_db.get_property(GUID_ZERO, "prefab");
+			if (prefab != null)
+				load_prefab((string)prefab);
+
+			prefab_db.copy_to(_prefabs, name);
+			_loaded_prefabs.add(name);
+		}
+
+		public void generate_spawn_unit_commands(Guid?[] unit_ids, StringBuilder sb)
+		{
+			foreach (Guid unit_id in unit_ids)
+			{
+				if (has_prefab(unit_id))
+					load_prefab((string)_db.get_property(unit_id, "prefab"));
+
+				sb.append(LevelEditorAPI.spawn_empty_unit(unit_id));
+
+				Guid component_id = GUID_ZERO;
+
+				if (has_component(unit_id, "transform", ref component_id))
+				{
+					string s = LevelEditorAPI.add_tranform_component(unit_id
+						, component_id
+						, (Vector3)   get_component_property(unit_id, component_id, "data.position")
+						, (Quaternion)get_component_property(unit_id, component_id, "data.rotation")
+						, (Vector3)   get_component_property(unit_id, component_id, "data.scale")
+						);
+					sb.append(s);
+				}
+				if (has_component(unit_id, "mesh_renderer", ref component_id))
+				{
+					string s = LevelEditorAPI.add_mesh_component(unit_id
+						, component_id
+						, (string)get_component_property(unit_id, component_id, "data.mesh_resource")
+						, (string)get_component_property(unit_id, component_id, "data.geometry_name")
+						, (string)get_component_property(unit_id, component_id, "data.material")
+						, (bool)  get_component_property(unit_id, component_id, "data.visible")
+						);
+					sb.append(s);
+				}
+				if (has_component(unit_id, "light", ref component_id))
+				{
+					string s = LevelEditorAPI.add_light_component(unit_id
+						, component_id
+						, (string) get_component_property(unit_id, component_id, "data.type")
+						, (double) get_component_property(unit_id, component_id, "data.range")
+						, (double) get_component_property(unit_id, component_id, "data.intensity")
+						, (double) get_component_property(unit_id, component_id, "data.spot_angle")
+						, (Vector3)get_component_property(unit_id, component_id, "data.color")
+						);
+					sb.append(s);
+				}
+			}
+		}
+
+		public void generate_spawn_sound_commands(Guid?[] sound_ids, StringBuilder sb)
+		{
+			foreach (Guid sound_id in sound_ids)
+			{
+				string s = LevelEditorAPI.spawn_sound(sound_id
+					, (Vector3)   _db.get_property(sound_id, "position")
+					, (Quaternion)_db.get_property(sound_id, "rotation")
+					, (double)    _db.get_property(sound_id, "range")
+					, (double)    _db.get_property(sound_id, "volume")
+					, (bool)      _db.get_property(sound_id, "loop")
+					);
+				sb.append(s);
+			}
+		}
+
+		public void send()
+		{
+			HashSet<Guid?> units = _db.get_property(GUID_ZERO, "units") as HashSet<Guid?>;
+			HashSet<Guid?> sounds = _db.get_property(GUID_ZERO, "sounds") as HashSet<Guid?>;
+
+			StringBuilder sb = new StringBuilder();
+			generate_spawn_unit_commands(units.to_array(), sb);
+			generate_spawn_sound_commands(sounds.to_array(), sb);
+			_client.send_script(sb.str);
+		}
+
+		private void undo_redo_action(bool undo, int id, Value? data)
+		{
+			switch (id)
+			{
+				case (int)ActionType.SPAWN_UNIT:
+				{
+					Guid unit_id = (Guid)data;
+					if (undo)
+						do_destroy_objects(new Guid?[] { unit_id });
+					else
+						do_spawn_units(new Guid?[] { unit_id });
+					break;
+				}
+				case (int)ActionType.DESTROY_UNIT:
+				{
+					Guid unit_id = (Guid)data;
+					if (undo)
+						do_spawn_units(new Guid?[] { unit_id });
+					else
+						do_destroy_objects(new Guid?[] { unit_id });
+					break;
+				}
+				case (int)ActionType.SPAWN_SOUND:
+				{
+					Guid sound_id = (Guid)data;
+					if (undo)
+						do_destroy_objects(new Guid?[] { sound_id });
+					else
+						do_spawn_sounds(new Guid?[] { sound_id });
+					break;
+				}
+				case (int)ActionType.DESTROY_SOUND:
+				{
+					Guid sound_id = (Guid)data;
+					if (undo)
+						do_spawn_sounds(new Guid?[] { sound_id });
+					else
+						do_destroy_objects(new Guid?[] { sound_id });
+					break;
+				}
+				case (int)ActionType.MOVE_OBJECTS:
+				{
+					// Guid?[] ids = (Guid?[])data;
+
+					// Vector3[] positions = new Vector3[ids.length];
+					// Quaternion[] rotations = new Quaternion[ids.length];
+					// Vector3[] scales = new Vector3[ids.length];
+
+					// for (int i = 0; i < ids.length; ++i)
+					// {
+					// 	if (is_unit(ids[i]))
+					// 	{
+					// 		Guid unit_id = ids[i];
+					// 		Guid transform_id = GUID_ZERO;
+
+					// 		if (has_component(unit_id, "transform", ref transform_id))
+					// 		{
+					// 			positions[i] = (Vector3)   get_component_property(unit_id, transform_id, "data.position");
+					// 			rotations[i] = (Quaternion)get_component_property(unit_id, transform_id, "data.rotation");
+					// 			scales[i]    = (Vector3)   get_component_property(unit_id, transform_id, "data.scale");
+					// 		}
+					// 		else
+					// 		{
+					// 			positions[i] = (Vector3)   _db.get_property(unit_id, "position");
+					// 			rotations[i] = (Quaternion)_db.get_property(unit_id, "rotation");
+					// 			scales[i]    = (Vector3)   _db.get_property(unit_id, "scale");
+					// 		}
+					// 	}
+					// 	else if (is_sound(ids[i]))
+					// 	{
+					// 		Guid sound_id = ids[i];
+					// 		positions[i] = (Vector3)   _db.get_property(sound_id, "position");
+					// 		rotations[i] = (Quaternion)_db.get_property(sound_id, "rotation");
+					// 		scales[i]    = Vector3(1.0, 1.0, 1.0);
+					// 	}
+					// 	else
+					// 	{
+					// 		assert(false);
+					// 	}
+					// }
+
+					// do_move_objects(ids, positions, rotations, scales);
+					// // FIXME: Hack to force update the component view
+					// selection_changed(_selection);
+					break;
+				}
+				case (int)ActionType.DUPLICATE_OBJECTS:
+				{
+					// DuplicateObjectsArgs args = (DuplicateObjectsArgs)data;
+					// if (undo)
+					// 	do_destroy_objects(args._new_ids);
+					// else
+					// 	do_spawn_objects(args._new_ids);
+					break;
+				}
+				default:
+				{
+					assert(false);
+					break;
+				}
+			}
+		}
+
+		private void do_spawn_objects(Guid?[] ids)
+		{
+			StringBuilder sb = new StringBuilder();
+			for (int i = 0; i < ids.length; ++i)
+			{
+				if (is_unit(ids[i]))
+				{
+					generate_spawn_unit_commands(new Guid?[] { ids[i] }, sb);
+				}
+				else if (is_sound(ids[i]))
+				{
+					generate_spawn_sound_commands(new Guid?[] { ids[i] }, sb);
+				}
+			}
+			_client.send_script(sb.str);
+		}
+
+		private void do_spawn_units(Guid?[] ids)
+		{
+			StringBuilder sb = new StringBuilder();
+			generate_spawn_unit_commands(ids, sb);
+			_client.send_script(sb.str);
+		}
+
+		private void do_spawn_sounds(Guid?[] ids)
+		{
+			StringBuilder sb = new StringBuilder();
+			generate_spawn_sound_commands(ids, sb);
+			_client.send_script(sb.str);
+		}
+
+		private void do_destroy_objects(Guid?[] ids)
+		{
+			StringBuilder sb = new StringBuilder();
+			foreach (Guid id in ids)
+				sb.append(LevelEditorAPI.destroy(id));
+
+			_client.send_script(sb.str);
+		}
+
+		private void do_move_objects(Guid?[] ids, Vector3[] positions, Quaternion[] rotations, Vector3[] scales)
+		{
+			StringBuilder sb = new StringBuilder();
+			for (int i = 0; i < ids.length; ++i)
+				sb.append(LevelEditorAPI.move_object(ids[i], positions[i], rotations[i], scales[i]));
+
+			_client.send_script(sb.str);
+		}
+
+		public void move_selected_objects(Vector3 pos, Quaternion rot, Vector3 scl)
+		{
+			if (_selection.size == 0)
+				return;
+
+			Guid id = _selection.last();
+			move_objects(new Guid?[] { id }, new Vector3[] { pos }, new Quaternion[] { rot }, new Vector3[] { scl });
+			do_move_objects(new Guid?[] { id }, new Vector3[] { pos }, new Quaternion[] { rot }, new Vector3[] { scl });
+		}
+
+		public void duplicate_selected_objects()
+		{
+			if (_selection.size > 0)
+			{
+				Guid?[] ids = _selection.to_array();
+				Guid?[] new_ids = new Guid?[ids.length];
+
+				for (int i = 0; i < new_ids.length; ++i)
+					new_ids[i] = Guid.new_guid();
+
+				duplicate_objects(ids, new_ids);
+			}
+		}
+
+		public void destroy_selected_objects()
+		{
+			Guid?[] ids = _selection.to_array();
+			_selection.clear();
+
+			destroy_objects(ids);
+		}
+
+		private struct DuplicateObjectsArgs
+		{
+			public Guid?[] _ids;
+			public Guid?[] _new_ids;
+
+			public DuplicateObjectsArgs(Guid?[] ids, Guid?[] new_ids)
+			{
+				_ids = ids;
+				_new_ids = new_ids;
+			}
+		}
+
+		public void duplicate_objects(Guid?[] ids, Guid?[] new_ids)
+		{
+			for (int i = 0; i < ids.length; ++i)
+			{
+				_db.duplicate(ids[i], new_ids[i]);
+
+				if (is_unit(ids[i]))
+				{
+					_db.add_to_set(GUID_ZERO, "units", new_ids[i]);
+				}
+				else if (is_sound(ids[i]))
+				{
+					_db.add_to_set(GUID_ZERO, "sounds", new_ids[i]);
+				}
+			}
+			// FIXME
+			// _db.add_restore_point(undo_redo_action, (int)ActionType.DUPLICATE_OBJECTS, new DuplicateObjectsArgs(ids, new_ids));
+
+			do_spawn_objects(new_ids);
+		}
+
+		public void spawn_unit(Guid id, string name, Vector3 pos, Quaternion rot, Vector3 scl)
+		{
+			on_unit_spawned(id, name, pos, rot, scl);
+			do_spawn_units(new Guid?[] { id });
+		}
+
+		public void on_unit_spawned(Guid id, string name, Vector3 pos, Quaternion rot, Vector3 scl)
+		{
+			load_prefab(name);
+
+			_db.create(id);
+			_db.set_property(id, "prefab", name);
+
+			Guid transform_id = GUID_ZERO;
+			if (has_component(id, "transform", ref transform_id))
+			{
+				set_component_property(id, transform_id, "data.position", pos);
+				set_component_property(id, transform_id, "data.rotation", rot);
+				set_component_property(id, transform_id, "data.scale", scl);
+				set_component_property(id, transform_id, "type", "transform");
+			}
+			else
+			{
+				_db.set_property(id, "position", pos);
+				_db.set_property(id, "rotation", rot);
+				_db.set_property(id, "scale", scl);
+			}
+			_db.add_to_set(GUID_ZERO, "units", id);
+			_db.add_restore_point(undo_redo_action, (int)ActionType.SPAWN_UNIT, id);
+		}
+
+		public void on_sound_spawned(Guid id, string name, Vector3 pos, Quaternion rot, Vector3 scl, double range, double volume, bool loop)
+		{
+			_db.create(id);
+			_db.set_property(id, "position", pos);
+			_db.set_property(id, "rotation", rot);
+			_db.set_property(id, "name", name);
+			_db.set_property(id, "range", range);
+			_db.set_property(id, "volume", volume);
+			_db.set_property(id, "loop", loop);
+			_db.add_to_set(GUID_ZERO, "sounds", id);
+			_db.add_restore_point(undo_redo_action, (int)ActionType.SPAWN_SOUND, id);
+		}
+
+		public void destroy_objects(Guid?[] ids)
+		{
+			foreach (Guid id in ids)
+			{
+				if (is_unit(id))
+				{
+					_db.remove_from_set(GUID_ZERO, "units", id);
+					_db.destroy(id);
+					_db.add_restore_point(undo_redo_action, (int)ActionType.DESTROY_UNIT, id);
+				}
+				else if (is_sound(id))
+				{
+					_db.remove_from_set(GUID_ZERO, "sounds", id);
+					_db.destroy(id);
+					_db.add_restore_point(undo_redo_action, (int)ActionType.DESTROY_SOUND, id);
+				}
+			}
+
+			do_destroy_objects(ids);
+		}
+
+		public void set_selected_unit(Guid id)
+		{
+			_client.send_script(LevelEditorAPI.set_selected_unit(id));
+			_selection.clear();
+			_selection.add(id);
+
+			selection_changed(_selection);
+		}
+
+		public void move_objects(Guid?[] ids, Vector3[] positions, Quaternion[] rotations, Vector3[] scales)
+		{
+			for (int i = 0; i < ids.length; ++i)
+			{
+				Guid id = ids[i];
+				Vector3 pos = positions[i];
+				Quaternion rot = rotations[i];
+				Vector3 scl = scales[i];
+
+				if (is_unit(id))
+				{
+					Guid transform_id = GUID_ZERO;
+
+					if (has_component(id, "transform", ref transform_id))
+					{
+						set_component_property(id, transform_id, "data.position", pos);
+						set_component_property(id, transform_id, "data.rotation", rot);
+						set_component_property(id, transform_id, "data.scale", scl);
+					}
+					else
+					{
+						_db.set_property(id, "position", pos);
+						_db.set_property(id, "rotation", rot);
+						_db.set_property(id, "scale", scl);
+					}
+				}
+				else if (is_sound(id))
+				{
+					_db.set_property(id, "position", pos);
+					_db.set_property(id, "rotation", rot);
+				}
+			}
+			// FIXME
+			// _db.add_restore_point(undo_redo_action, (int)ActionType.MOVE_OBJECTS, (Value?)ids);
+
+			// FIXME: Hack to force update the component view
+			selection_changed(_selection);
+		}
+
+		public Value? get_property(Guid id, string key)
+		{
+			return _db.get_property(id, key);
+		}
+
+		public void set_property(Guid id, string key, Value? value)
+		{
+			_db.set_property(id, key, value);
+		}
+
+		public Value? get_component_property(Guid unit_id, Guid component_id, string key)
+		{
+			// Search in components
+			{
+				Value? components = _db.get_property(unit_id, "components");
+				if (components != null && ((HashSet<Guid?>)components).contains(component_id))
+					return _db.get_property(component_id, key);
+			}
+
+			// Search in modified components
+			{
+				Value? value = _db.get_property(unit_id, "modified_components.#" + component_id.to_string() + "." + key);
+				if (value != null)
+					return value;
+			}
+
+			// Search in prefab's components
+			{
+				Value? value = _db.get_property(unit_id, "prefab");
+				if (value != null)
+				{
+					string prefab = (string)value;
+					Value? pcvalue = _prefabs.get_property(GUID_ZERO, prefab + ".components");
+					if (pcvalue != null)
+					{
+						HashSet<Guid?> prefab_components = (HashSet<Guid?>)pcvalue;
+						if (prefab_components.contains(component_id))
+							return _prefabs.get_property(component_id, key);
+					}
+				}
+			}
+
+			assert(false);
+			return null;
+		}
+
+		public void set_component_property(Guid unit_id, Guid component_id, string key, Value? value)
+		{
+			// Search in components
+			{
+				Value? components = _db.get_property(unit_id, "components");
+				if (components != null && ((HashSet<Guid?>)components).contains(component_id))
+				{
+					_db.set_property(component_id, key, value);
+					return;
+				}
+			}
+
+			// Search in modified components
+			{
+				Value? val = _db.get_property(unit_id, "modified_components.#" + component_id.to_string() + "." + key);
+				if (val != null)
+				{
+					_db.set_property(unit_id, "modified_components.#" + component_id.to_string() + "." + key, value);
+					return;
+				}
+			}
+
+			// Create new entry
+			{
+				_db.set_property(unit_id, "modified_components.#" + component_id.to_string() + "." + key, value);
+				return;
+			}
+		}
+
+		private static bool has_component_static(Database db, Database prefabs_db, Guid unit_id, string component_type, ref Guid ref_component_id)
+		{
+			// Search in components
+			{
+				Value? value = db.get_property(unit_id, "components");
+				if (value != null)
+				{
+					HashSet<Guid?> components = (HashSet<Guid?>)value;
+					foreach (Guid component_id in components)
+					{
+						if((string)db.get_property(component_id, "type") == component_type)
+						{
+							ref_component_id = component_id;
+							return true;
+						}
+					}
+				}
+			}
+
+			{
+				string[] keys = db.get_keys(unit_id);
+				foreach (string m in keys)
+				{
+					if (!m.has_prefix("modified_components.#"))
+						continue;
+
+					// 0                   21                                   58  62
+					// |                    |                                    |   |
+					// modified_components.#f56420ad-7f9c-4cca-aca5-350f366e0dc0.type
+					string id = m[21:57];
+					string type_or_name = m[58:62];
+
+					if (!type_or_name.has_prefix("type"))
+						continue;
+
+					if ((string)db.get_property(unit_id, m) == component_type)
+					{
+						ref_component_id = Guid.parse(id);
+						return true;
+					}
+				}
+			}
+
+			{
+				Value? value = db.get_property(unit_id, "prefab");
+				if (value != null)
+				{
+					string prefab = (string)value;
+					Value? pcvalue = prefabs_db.get_property(GUID_ZERO, prefab + ".components");
+					if (pcvalue != null)
+					{
+						HashSet<Guid?> prefab_components = (HashSet<Guid?>)pcvalue;
+						foreach (Guid component_id in prefab_components)
+						{
+							if((string)prefabs_db.get_property(component_id, "type") == component_type)
+							{
+								ref_component_id = component_id;
+								return true;
+							}
+						}
+					}
+				}
+			}
+
+			return false;
+		}
+
+		public bool has_component(Guid unit_id, string component_type, ref Guid ref_component_id)
+		{
+			return Level.has_component_static(_db, _prefabs, unit_id, component_type, ref ref_component_id);
+		}
+
+		public bool has_prefab(Guid unit_id)
+		{
+			return _db.get_property(unit_id, "prefab") != null;
+		}
+
+		public bool is_unit(Guid id)
+		{
+			return (_db.get_property(GUID_ZERO, "units") as HashSet<Guid?>).contains(id);
+		}
+
+		public bool is_sound(Guid id)
+		{
+			return (_db.get_property(GUID_ZERO, "sounds") as HashSet<Guid?>).contains(id);
+		}
+	}
+}

+ 1092 - 0
tools/level_editor/level_editor.vala

@@ -0,0 +1,1092 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using Gdk; // Pixbuf
+using Gee;
+using Gtk;
+
+namespace Crown
+{
+	public enum ToolType
+	{
+		PLACE,
+		MOVE,
+		ROTATE,
+		SCALE
+	}
+
+	public enum SnapMode
+	{
+		RELATIVE,
+		ABSOLUTE
+	}
+
+	public enum ReferenceSystem
+	{
+		LOCAL,
+		WORLD
+	}
+
+	public enum PlaceableType
+	{
+		UNIT,
+		SOUND
+	}
+
+	public class LevelEditor : Gtk.Window
+	{
+		// Project paths
+		private string _source_dir;
+		private string _bundle_dir;
+		private string _platform;
+
+		// Editor state
+		private double _grid_size;
+		private double _rotation_snap;
+		private bool _show_grid;
+		private bool _snap_to_grid;
+		private bool _debug_render_world;
+		private bool _debug_physics_world;
+		private ToolType _tool_type;
+		private SnapMode _snap_mode;
+		private ReferenceSystem _reference_system;
+
+		// Engine connections
+		private GLib.Subprocess _compiler_process;
+		private GLib.Subprocess _engine_process;
+		private GLib.Subprocess _game_process;
+		private ConsoleClient _compiler;
+		private ConsoleClient _engine;
+
+		// Level data
+		private Database _db;
+		private Level _level;
+		private string _level_filename;
+
+		// Widgets
+		private StartingCompiler _starting_compiler;
+		private ConsoleView _console_view;
+		private EngineView _engine_view;
+		private LevelTreeView _level_treeview;
+		private PropertiesView _properties_view;
+/*
+		private GraphStore _graph_store;
+		private GraphView _graph_view;
+*/
+		private Gtk.Alignment _alignment_engine;
+		private Gtk.Alignment _alignment_level_tree_view;
+		private Gtk.Alignment _alignment_properties_view;
+
+		private Gtk.ActionGroup _action_group;
+		private Gtk.UIManager _ui_manager;
+		private Gtk.Paned _pane_left;
+		private Gtk.Paned _pane_right;
+		private Gtk.Notebook _notebook;
+		private Gtk.Box _vbox;
+		private Gtk.FileFilter _file_filter;
+
+		private ResourceBrowser _resource_browser;
+
+		const Gtk.ActionEntry[] action_entries =
+		{
+			{ "menu-file",            null,  "_File",            null,             null, null                },
+			{ "new",                  null,  "New",              "<ctrl>N",        null, on_new              },
+			{ "open",                 null,  "Open",             "<ctrl>O",        null, on_open             },
+			{ "save",                 null,  "Save",             "<ctrl>S",        null, on_save             },
+			{ "save-as",              null,  "Save As...",       null,             null, on_save_as          },
+			{ "quit",                 null,  "Quit",             "<ctrl>Q",        null, on_quit             },
+			{ "menu-edit",            null,  "_Edit",            null,             null, null                },
+			{ "undo",                 null,  "Undo",             "<ctrl>Z",        null, on_undo             },
+			{ "redo",                 null,  "Redo",             "<shift><ctrl>Z", null, on_redo             },
+			{ "duplicate",            null,  "Duplicate",        "<ctrl>D",        null, on_duplicate        },
+			{ "delete",               null,  "Delete",           "<ctrl>K",        null, on_delete           },
+			{ "menu-grid",            null,  "Grid",             null,             null, null                },
+			{ "grid-custom",          null,  "Custom",           "G",              null, on_custom_grid      },
+			{ "menu-rotation-snap",   null,  "Rotation Snap",    null,             null, null                },
+			{ "rotation-snap-custom", null,  "Custom",           "H",              null, on_rotation_snap    },
+			{ "menu-create",          null,  "Create",           null,             null, null                },
+			{ "menu-primitives",      null,  "Primitives",       null,             null, null                },
+			{ "primitive-cube",       null,  "Cube",             null,             null, on_create_cube      },
+			{ "primitive-sphere",     null,  "Sphere",           null,             null, on_create_sphere    },
+			{ "primitive-cone",       null,  "Cone",             null,             null, on_create_cone      },
+			{ "primitive-cylinder",   null,  "Cylinder",         null,             null, on_create_cylinder  },
+			{ "primitive-plane",      null,  "Plane",            null,             null, on_create_plane     },
+			{ "camera",               null,  "Camera",           null,             null, on_create_camera    },
+			{ "light",                null,  "Light",            null,             null, on_create_light     },
+			{ "menu-engine",          null,  "En_gine",          null,             null, null                },
+			{ "menu-view",            null,  "View",             null,             null, null                },
+			{ "resource-browser",     null,  "Resource Browser", null,             null, on_resource_browser },
+			{ "restart",              null,  "_Restart",         null,             null, on_engine_restart   },
+			{ "reload-lua",           null,  "Reload Lua",       "F7",             null, on_reload_lua       },
+			{ "menu-run",             null,  "_Run",             null,             null, null                },
+			{ "game-run",             "run", "Run Game",         "F5",             null, on_run_game         },
+			{ "menu-help",            null,  "Help",             null,             null, null                },
+			{ "manual",               null,  "Manual",           "F1",             null, on_manual           },
+			{ "report-issue",         null,  "Report an Issue",  null,             null, on_report_issue     },
+			{ "open-last-log",        null,  "Open last.log",    null,             null, on_open_last_log    },
+			{ "about",                null,  "About",            null,             null, on_about            }
+		};
+
+		const Gtk.RadioActionEntry[] grid_entries =
+		{
+			{ "grid-0.1", null, "0.1m", null, null, 10  },
+			{ "grid-0.2", null, "0.2m", null, null, 20  },
+			{ "grid-0.5", null, "0.5m", null, null, 50  },
+			{ "grid-1",   null, "1m",   null, null, 100 },
+			{ "grid-2",   null, "2m",   null, null, 200 },
+			{ "grid-5",   null, "5m",   null, null, 500 }
+		};
+
+		const RadioActionEntry[] rotation_snap_entries =
+		{
+			{ "rotation-snap-1",   null, "1°",   null, null,   1 },
+			{ "rotation-snap-15",  null, "15°",  null, null,  15 },
+			{ "rotation-snap-30",  null, "30°",  null, null,  30 },
+			{ "rotation-snap-45",  null, "45°",  null, null,  45 },
+			{ "rotation-snap-90",  null, "90°",  null, null,  90 },
+			{ "rotation-snap-180", null, "180°", null, null, 180 }
+		};
+
+		const RadioActionEntry[] tool_entries =
+		{
+			{ "place",  "tool-place",  "Place",  "Q", "Place",  (int)ToolType.PLACE  },
+			{ "move",   "tool-move",   "Move",   "W", "Move",   (int)ToolType.MOVE   },
+			{ "rotate", "tool-rotate", "Rotate", "E", "Rotate", (int)ToolType.ROTATE },
+			{ "scale",  "tool-scale",  "Scale",  "R", "Scale",  (int)ToolType.SCALE  }
+		};
+
+		const RadioActionEntry[] snap_mode_entries =
+		{
+			{ "snap-relative", "reference-local", "Relative Snap", null, "Relative Snap", (int)SnapMode.RELATIVE },
+			{ "snap-absolute", "reference-world", "Absolute Snap", null, "Absolute Snap", (int)SnapMode.ABSOLUTE }
+		};
+
+		const RadioActionEntry[] reference_system_entries =
+		{
+			{ "reference-system-local", "axis-local", "Local Axis", null, "Local Axis", (int)ReferenceSystem.LOCAL },
+			{ "reference-system-world", "axis-world", "World Axis", null, "World Axis", (int)ReferenceSystem.WORLD }
+		};
+
+		const ToggleActionEntry[] snap_to_entries =
+		{
+			{ "snap-to-grid", "snap-to-grid", "Snap To Grid", "<ctrl>U", "Snap To Grid", on_snap_to_grid, true },
+			{ "grid-show",     null,          "Show Grid",    null,      "Show Grid",    on_show_grid,    true }
+		};
+
+		const ToggleActionEntry[] view_entries =
+		{
+			{ "debug-render-world",  null, "Debug Render World",  null, null, on_debug_render_world,  false },
+			{ "debug-physics-world", null, "Debug Physics World", null, null, on_debug_physics_world, false }
+		};
+
+		public LevelEditor(string source_dir, string bundle_dir)
+		{
+			this.title = "Level Editor";
+
+			// Project paths
+			_source_dir = source_dir;
+			_bundle_dir = bundle_dir;
+			_platform   = "linux";
+
+			// Editor state
+			_grid_size = 1.0;
+			_rotation_snap = 45.0;
+			_show_grid = true;
+			_snap_to_grid = true;
+			_debug_render_world = false;
+			_debug_physics_world = false;
+			_tool_type = ToolType.MOVE;
+			_snap_mode = SnapMode.RELATIVE;
+			_reference_system = ReferenceSystem.LOCAL;
+
+			// Engine connections
+			_compiler_process = null;
+			_engine_process = null;
+			_game_process = null;
+			_compiler = new ConsoleClient();
+			_compiler.connected.connect(on_compiler_connected);
+			_compiler.disconnected.connect(on_compiler_disconnected);
+			_compiler.message_received.connect(on_message_received);
+			_engine = new ConsoleClient();
+			_engine.connected.connect(on_engine_connected);
+			_engine.disconnected.connect(on_engine_disconnected);
+			_engine.message_received.connect(on_message_received);
+
+			// Level data
+			_db = new Database();
+			_level = new Level(_db, _engine, _source_dir);
+			_level_filename = null;
+
+			// Widgets
+			_console_view = new ConsoleView(_engine);
+			_level_treeview = new LevelTreeView(_db, _level);
+			_properties_view = new PropertiesView(_level);
+/*
+			_graph_store = new GraphStore();
+			_graph_view = new GraphView(_graph_store);
+*/
+			_starting_compiler = new StartingCompiler();
+			_alignment_engine = new Gtk.Alignment(0, 0, 1, 1);
+			_alignment_level_tree_view = new Gtk.Alignment(0, 0, 1, 1);
+			_alignment_properties_view = new Gtk.Alignment(0, 0, 1, 1);
+			_alignment_engine.add(_starting_compiler);
+			_alignment_level_tree_view.add(_starting_compiler);
+			_alignment_properties_view.add(_starting_compiler);
+
+			start_compiler();
+			new_level();
+
+			try
+			{
+				Gtk.IconTheme.add_builtin_icon("tool-place",      16, new Pixbuf.from_file("icons/tool-place.png"));
+				Gtk.IconTheme.add_builtin_icon("tool-move",       16, new Pixbuf.from_file("icons/tool-move.png"));
+				Gtk.IconTheme.add_builtin_icon("tool-rotate",     16, new Pixbuf.from_file("icons/tool-rotate.png"));
+				Gtk.IconTheme.add_builtin_icon("tool-scale",      16, new Pixbuf.from_file("icons/tool-scale.png"));
+				Gtk.IconTheme.add_builtin_icon("axis-local",      16, new Pixbuf.from_file("icons/axis-local.png"));
+				Gtk.IconTheme.add_builtin_icon("axis-world",      16, new Pixbuf.from_file("icons/axis-world.png"));
+				Gtk.IconTheme.add_builtin_icon("snap-to-grid",    16, new Pixbuf.from_file("icons/snap-to-grid.png"));
+				Gtk.IconTheme.add_builtin_icon("reference-local", 16, new Pixbuf.from_file("icons/reference-local.png"));
+				Gtk.IconTheme.add_builtin_icon("reference-world", 16, new Pixbuf.from_file("icons/reference-world.png"));
+				Gtk.IconTheme.add_builtin_icon("run",             16, new Pixbuf.from_file("icons/run.png"));
+			}
+			catch (Error e)
+			{
+				stderr.printf(e.message);
+			}
+
+			_action_group = new Gtk.ActionGroup("group");
+			_action_group.add_actions(action_entries, this);
+			_action_group.add_radio_actions(grid_entries, (int)(_grid_size*100.0), this.on_grid_changed);
+			_action_group.add_radio_actions(rotation_snap_entries, (int)_rotation_snap, this.on_rotation_snap_changed);
+			_action_group.add_radio_actions(tool_entries, (int)_tool_type, on_tool_changed);
+			_action_group.add_radio_actions(snap_mode_entries, (int)_snap_mode, on_snap_mode_changed);
+			_action_group.add_radio_actions(reference_system_entries, (int)_reference_system, on_reference_system_changed);
+			_action_group.add_toggle_actions(snap_to_entries, this);
+			_action_group.add_toggle_actions(view_entries, this);
+
+			_ui_manager = new UIManager();
+			try
+			{
+				_ui_manager.add_ui_from_file("ui/level_editor_menu.xml");
+				_ui_manager.insert_action_group(_action_group, 0);
+				add_accel_group(_ui_manager.get_accel_group());
+			}
+			catch (Error e)
+			{
+				error(e.message);
+			}
+
+			_pane_left = new Gtk.Paned(Gtk.Orientation.VERTICAL);
+			_pane_left.pack1(_alignment_engine, true, true);
+			_pane_left.pack2(_console_view, true, true);
+
+			Toolbar toolbar = _ui_manager.get_widget("/toolbar") as Toolbar;
+			toolbar.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR);
+			toolbar.set_style(Gtk.ToolbarStyle.ICONS);
+			Gtk.Box vb = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+			vb.pack_start(toolbar, false, false, 0);
+			vb.pack_start(_pane_left, true, true, 0);
+
+			Gtk.Paned rb = new Gtk.Paned(Gtk.Orientation.VERTICAL);
+			rb.pack1(_alignment_level_tree_view, true, true);
+			rb.pack2(_alignment_properties_view, true, true);
+
+			_pane_right = new Gtk.Paned(Gtk.Orientation.HORIZONTAL);
+			_pane_right.pack1(vb, true, false);
+			_pane_right.pack2(rb, true, true);
+
+			_notebook = new Notebook();
+			_notebook.append_page(_pane_right, new Gtk.Label("Level"));
+/*
+			_notebook.append_page(_graph_view, new Gtk.Label("Pepper"));
+*/
+
+			MenuBar menu = (MenuBar)_ui_manager.get_widget("/menubar");
+			_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+			_vbox.pack_start(menu, false, false, 0);
+			_vbox.pack_start(_notebook, true, true, 0);
+
+			_file_filter = new FileFilter();
+			_file_filter.set_filter_name("Level (*.level)");
+			_file_filter.add_pattern("*.level");
+
+			_resource_browser = new ResourceBrowser(_source_dir, _bundle_dir);
+			_resource_browser.resource_selected.connect(on_resource_browser_resource_selected);
+			_resource_browser.transient_for = this;
+			_resource_browser.delete_event.connect(() => { _resource_browser.hide(); return true; });
+
+			this.destroy.connect(this.on_destroy);
+			this.delete_event.connect(this.on_delete_event);
+
+			this.add(_vbox);
+			this.set_size_request(1280, 720);
+			this.show_all();
+		}
+
+		private bool on_focus_in(EventFocus ev)
+		{
+			add_accel_group(_ui_manager.get_accel_group());
+			return true;
+		}
+
+		private bool on_focus_out(EventFocus ev)
+		{
+			remove_accel_group(_ui_manager.get_accel_group());
+			return true;
+		}
+
+		private void on_resource_browser_resource_selected(PlaceableType placeable_type, string name)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(placeable_type, name));
+		}
+
+		private void on_compiler_connected()
+		{
+			_console_view.log("Compiler connected.", "info");
+			_compiler.receive_async();
+		}
+
+		private void on_compiler_disconnected()
+		{
+			_console_view.log("Compiler disconnected.", "info");
+		}
+
+		private void on_engine_connected()
+		{
+			_console_view.log("Engine connected.", "info");
+			_engine.receive_async();
+		}
+
+		private void on_engine_disconnected()
+		{
+			_console_view.log("Engine disconnected.", "info");
+		}
+
+		private static int stringcmp(ref string a, ref string b)
+		{
+			return Posix.strcmp(a, b);
+		}
+
+		private void on_message_received(ConsoleClient client, uint8[] json)
+		{
+			try
+			{
+				Hashtable msg = JSON.decode(json) as Hashtable;
+				string msg_type = msg["type"] as string;
+
+				if (msg_type == "message")
+				{
+					_console_view.log((string)msg["message"], (string)msg["severity"]);
+				}
+				else if (msg_type == "compile")
+				{
+					Guid id = Guid.parse((string)msg["id"]);
+
+					if (msg.has_key("start"))
+					{
+					}
+					else if (msg.has_key("success"))
+					{
+						if (_engine_view == null)
+						{
+							_engine_view = new EngineView(_engine);
+							_engine_view.realized.connect(on_engine_view_realized);
+							_engine_view.button_press_event.connect(on_button_press);
+							_engine_view.button_release_event.connect(on_button_release);
+							_engine_view._event_box.focus_in_event.connect(on_focus_in);
+							_engine_view._event_box.focus_out_event.connect(on_focus_out);
+
+							_alignment_engine.remove(_alignment_engine.get_child());
+							_alignment_level_tree_view.remove(_alignment_level_tree_view.get_child());
+							_alignment_properties_view.remove(_alignment_properties_view.get_child());
+
+							_alignment_engine.add(_engine_view);
+							_alignment_level_tree_view.add(_level_treeview);
+							_alignment_properties_view.add(_properties_view);
+
+							_alignment_engine.show_all();
+							_alignment_level_tree_view.show_all();
+							_alignment_properties_view.show_all();
+						}
+					}
+				}
+				else if (msg_type == "unit_spawned")
+				{
+					string id             = (string)           msg["id"];
+					string name           = (string)           msg["name"];
+					ArrayList<Value?> pos = (ArrayList<Value?>)msg["position"];
+					ArrayList<Value?> rot = (ArrayList<Value?>)msg["rotation"];
+					ArrayList<Value?> scl = (ArrayList<Value?>)msg["scale"];
+
+					_level.on_unit_spawned(Guid.parse(id)
+						, name
+						, Vector3.from_array(pos)
+						, Quaternion.from_array(rot)
+						, Vector3.from_array(scl)
+						);
+				}
+				else if (msg_type == "sound_spawned")
+				{
+					string id             = (string)           msg["id"];
+					string name           = (string)           msg["name"];
+					ArrayList<Value?> pos = (ArrayList<Value?>)msg["position"];
+					ArrayList<Value?> rot = (ArrayList<Value?>)msg["rotation"];
+					ArrayList<Value?> scl = (ArrayList<Value?>)msg["scale"];
+					double range          = (double)           msg["range"];
+					double volume         = (double)           msg["volume"];
+					bool loop             = (bool)             msg["loop"];
+
+					_level.on_sound_spawned(Guid.parse(id)
+						, name
+						, Vector3.from_array(pos)
+						, Quaternion.from_array(rot)
+						, Vector3.from_array(scl)
+						, range
+						, volume
+						, loop
+						);
+				}
+				else if (msg_type == "move_objects")
+				{
+					Hashtable ids           = (Hashtable)msg["ids"];
+					Hashtable new_positions = (Hashtable)msg["new_positions"];
+					Hashtable new_rotations = (Hashtable)msg["new_rotations"];
+					Hashtable new_scales    = (Hashtable)msg["new_scales"];
+
+					string[] keys = ids.keys.to_array();
+					Posix.qsort(keys, keys.length, sizeof(string), (Posix.compar_fn_t)stringcmp);
+
+					Guid?[] n_ids            = new Guid?[keys.length];
+					Vector3[] n_positions    = new Vector3[keys.length];
+					Quaternion[] n_rotations = new Quaternion[keys.length];
+					Vector3[] n_scales       = new Vector3[keys.length];
+
+					for (int i = 0; i < keys.length; ++i)
+					{
+						string k = keys[i];
+
+						n_ids[i]       = Guid.parse((string)ids[k]);
+						n_positions[i] = Vector3.from_array((ArrayList<Value?>)(new_positions[k]));
+						n_rotations[i] = Quaternion.from_array((ArrayList<Value?>)new_rotations[k]);
+						n_scales[i]    = Vector3.from_array((ArrayList<Value?>)new_scales[k]);
+					}
+
+					_level.move_objects(n_ids, n_positions, n_rotations, n_scales);
+				}
+				else if (msg_type == "selection")
+				{
+					Hashtable objects = (Hashtable)msg["objects"];
+
+					string[] keys = objects.keys.to_array();
+					Posix.qsort(keys, keys.length, sizeof(string), (Posix.compar_fn_t)stringcmp);
+
+					Guid[] ids = new Guid[keys.length];
+
+					for (int i = 0; i < keys.length; ++i)
+					{
+						string k = keys[i];
+						ids[i] = Guid.parse((string)objects[k]);
+					}
+
+					_level.selection(ids);
+				}
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+
+			// Receive next message
+			client.receive_async();
+		}
+
+		private void send_state()
+		{
+			StringBuilder sb = new StringBuilder();
+			sb.append(LevelEditorAPI.set_grid_size(_grid_size));
+			sb.append(LevelEditorAPI.set_rotation_snap(_rotation_snap));
+			sb.append(LevelEditorAPI.enable_show_grid(_show_grid));
+			sb.append(LevelEditorAPI.enable_snap_to_grid(_snap_to_grid));
+			sb.append(LevelEditorAPI.enable_debug_render_world(_debug_render_world));
+			sb.append(LevelEditorAPI.enable_debug_physics_world(_debug_physics_world));
+			sb.append(LevelEditorAPI.set_tool_type(_tool_type));
+			sb.append(LevelEditorAPI.set_snap_mode(_snap_mode));
+			sb.append(LevelEditorAPI.set_reference_system(_reference_system));
+			_engine.send_script(sb.str);
+		}
+
+		private bool on_button_press(EventButton ev)
+		{
+			// Prevent accelerators to step on camera's toes
+			remove_accel_group(_ui_manager.get_accel_group());
+			return true;
+		}
+
+		private bool on_button_release(EventButton ev)
+		{
+			add_accel_group(_ui_manager.get_accel_group());
+			return true;
+		}
+
+		private void start_compiler()
+		{
+			string args[] =
+			{
+				ENGINE_EXE,
+				"--source-dir", _source_dir,
+				"--server",
+				null
+			};
+
+			GLib.SubprocessLauncher sl = new GLib.SubprocessLauncher(SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE);
+			sl.set_cwd(ENGINE_DIR);
+			try
+			{
+				_compiler_process = sl.spawnv(args);
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+
+			while (!_compiler.is_connected())
+				_compiler.connect("127.0.0.1", CROWN_DEFAULT_SERVER_PORT);
+
+			_compiler.send(EngineAPI.compile(Guid.new_guid(), _bundle_dir, _platform));
+		}
+
+		private void stop_compiler()
+		{
+			_compiler.close();
+
+			if (_compiler_process != null)
+				_compiler_process.force_exit();
+		}
+
+		private void start_engine(uint window_xid)
+		{
+			string args[] =
+			{
+				ENGINE_EXE,
+				"--bundle-dir", _bundle_dir,
+				"--boot-dir", "core/level_editor",
+				"--parent-window", window_xid.to_string(),
+				"--wait-console",
+				null
+			};
+
+			GLib.SubprocessLauncher sl = new GLib.SubprocessLauncher(SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE);
+			sl.set_cwd(ENGINE_DIR);
+			try
+			{
+				_engine_process = sl.spawnv(args);
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+
+			while (!_engine.is_connected())
+				_engine.connect("127.0.0.1", 10001);
+
+			_level.send();
+			send_state();
+		}
+
+		private void stop_engine()
+		{
+			_engine.close();
+
+			if (_engine_process != null)
+				_engine_process.force_exit();
+		}
+
+		private void restart_engine()
+		{
+			stop_engine();
+			start_engine(_engine_view.window_id);
+		}
+
+		private void start_game()
+		{
+			string args[] =
+			{
+				ENGINE_EXE,
+				"--bundle-dir", _bundle_dir,
+				"--console-port", "12345",
+				null
+			};
+
+			GLib.SubprocessLauncher sl = new GLib.SubprocessLauncher(SubprocessFlags.NONE);
+			sl.set_cwd(ENGINE_DIR);
+
+			try
+			{
+				_game_process = sl.spawnv(args);
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+		}
+
+		private void stop_game()
+		{
+			if (_game_process != null)
+				_game_process.force_exit();
+		}
+
+		private void on_engine_view_realized()
+		{
+			start_engine(_engine_view.window_id);
+		}
+
+		private void on_tool_changed(Gtk.Action action)
+		{
+			RadioAction ra = (RadioAction)action;
+			_tool_type = (ToolType)ra.current_value;
+			send_state();
+		}
+
+		private void on_snap_mode_changed(Gtk.Action action)
+		{
+			RadioAction ra = (RadioAction)action;
+			_snap_mode = (SnapMode)ra.current_value;
+			send_state();
+		}
+
+		private void on_reference_system_changed(Gtk.Action action)
+		{
+			RadioAction ra = (RadioAction)action;
+			_reference_system = (ReferenceSystem)ra.current_value;
+			send_state();
+		}
+
+		private void on_grid_changed(Gtk.Action action)
+		{
+			RadioAction ra = (RadioAction)action;
+			_grid_size = (float)ra.current_value/100.0;
+			send_state();
+		}
+
+		private void on_rotation_snap_changed(Gtk.Action action)
+		{
+			RadioAction ra = (RadioAction)action;
+			_rotation_snap = (float)ra.current_value;
+			send_state();
+		}
+
+		private void new_level()
+		{
+			_level_filename = null;
+			_level.new_level();
+		}
+
+		private void load()
+		{
+			FileChooserDialog fcd = new FileChooserDialog("Open..."
+				, this
+				, FileChooserAction.OPEN
+				, "Cancel"
+				, ResponseType.CANCEL
+				, "Open"
+				, ResponseType.ACCEPT
+				);
+			fcd.add_filter(_file_filter);
+			fcd.set_current_folder(_source_dir);
+
+			if (fcd.run() == (int)ResponseType.ACCEPT)
+			{
+				_level_filename = fcd.get_filename();
+				_level.load(_level_filename);
+				_level.send();
+				send_state();
+			}
+
+			fcd.destroy();
+		}
+
+		private bool save_as()
+		{
+			bool saved = false;
+
+			FileChooserDialog fcd = new FileChooserDialog("Save As..."
+				, this
+				, FileChooserAction.SAVE
+				, "Cancel"
+				, ResponseType.CANCEL
+				, "Save"
+				, ResponseType.ACCEPT
+				);
+			fcd.add_filter(_file_filter);
+			fcd.set_current_folder(_source_dir);
+
+			if (fcd.run() == (int)ResponseType.ACCEPT)
+			{
+				_level_filename = fcd.get_filename();
+				_level.save(_level_filename);
+				saved = true;
+			}
+
+			fcd.destroy();
+			return saved;
+		}
+
+		private bool save()
+		{
+			bool saved = false;
+
+			if (_level_filename == null)
+			{
+				saved = save_as();
+			}
+			else
+			{
+				_level.save(_level_filename);
+				saved = true;
+			}
+
+			return saved;
+		}
+
+		private void shutdown()
+		{
+			_resource_browser.destroy();
+			stop_game();
+			stop_engine();
+			stop_compiler();
+			Gtk.main_quit();
+		}
+
+		private void quit()
+		{
+			if (!_db.changed())
+			{
+				shutdown();
+				return;
+			}
+
+			Dialog dlg = new Dialog.with_buttons("File changed, save?", this, DialogFlags.MODAL);
+			dlg.add_button("Quit without saving", ResponseType.NO);
+			dlg.add_button("Cancel", ResponseType.CANCEL);
+			dlg.add_button("Save", ResponseType.YES);
+			int rt = dlg.run();
+			dlg.destroy();
+
+			if (rt == (int)ResponseType.YES && save() || rt == (int)ResponseType.NO)
+				shutdown();
+		}
+
+		private void on_new()
+		{
+			if (!_db.changed())
+			{
+				new_level();
+				_level.send();
+				send_state();
+				return;
+			}
+
+			Dialog dlg = new Dialog.with_buttons("File changed, save?", this, DialogFlags.MODAL);
+			dlg.add_button("New without saving", ResponseType.NO);
+			dlg.add_button("Cancel", ResponseType.CANCEL);
+			dlg.add_button("Save", ResponseType.YES);
+			int rt = dlg.run();
+			dlg.destroy();
+
+			if (rt == (int)ResponseType.YES && save() || rt == (int)ResponseType.NO)
+			{
+				new_level();
+				_level.send();
+				send_state();
+			}
+		}
+
+		private void on_open(Gtk.Action action)
+		{
+			if (!_db.changed())
+			{
+				load();
+				return;
+			}
+
+			Dialog dlg = new Dialog.with_buttons("File changed, save?", this, DialogFlags.MODAL);
+			dlg.add_button("Open without saving", ResponseType.NO);
+			dlg.add_button("Cancel", ResponseType.CANCEL);
+			dlg.add_button("Save", ResponseType.YES);
+			int rt = dlg.run();
+			dlg.destroy();
+
+			if (rt == (int)ResponseType.YES && save() || rt == (int)ResponseType.NO)
+				load();
+		}
+
+		private void on_save(Gtk.Action action)
+		{
+			save();
+		}
+
+		private void on_save_as(Gtk.Action action)
+		{
+			save_as();
+		}
+
+		private void on_quit(Gtk.Action action)
+		{
+			quit();
+		}
+
+		private void on_show_grid(Gtk.Action action)
+		{
+			ToggleAction ta = (ToggleAction)action;
+			_show_grid = ta.active;
+			send_state();
+		}
+
+		private void on_custom_grid()
+		{
+			MessageDialog dg = new MessageDialog(this
+				, DialogFlags.MODAL
+				, MessageType.OTHER
+				, ButtonsType.OK_CANCEL
+				, "Grid size in meters:"
+				);
+
+			SpinButtonDouble sb = new SpinButtonDouble(_grid_size, 0.1, 1000);
+			(dg.message_area as Gtk.Box).add(sb);
+			dg.show_all();
+
+			if (dg.run() == (int)ResponseType.OK)
+			{
+				_grid_size = sb.value;
+				send_state();
+			}
+
+			dg.destroy();
+		}
+
+		private void on_rotation_snap(Gtk.Action action)
+		{
+			MessageDialog dg = new MessageDialog(this
+				, DialogFlags.MODAL
+				, MessageType.OTHER
+				, ButtonsType.OK_CANCEL
+				, "Rotation snap in degrees:"
+				);
+
+			SpinButtonDouble sb = new SpinButtonDouble(_rotation_snap, 1.0, 180.0);
+			(dg.message_area as Gtk.Box).add(sb);
+			dg.show_all();
+
+			if (dg.run() == (int)ResponseType.OK)
+			{
+				_rotation_snap = sb.value;
+				send_state();
+			}
+
+			dg.destroy();
+		}
+
+		private void on_create_cube(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/primitives/cube"));
+		}
+
+		private void on_create_sphere(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/primitives/sphere"));
+		}
+
+		private void on_create_cone(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/primitives/cone"));
+		}
+
+		private void on_create_cylinder(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/primitives/cylinder"));
+		}
+
+		private void on_create_plane(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/primitives/plane"));
+		}
+
+		private void on_create_camera(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/camera"));
+		}
+
+		private void on_create_light(Gtk.Action action)
+		{
+			_engine.send_script(LevelEditorAPI.set_placeable(PlaceableType.UNIT, "core/units/light"));
+		}
+
+		private void on_resource_browser(Gtk.Action action)
+		{
+			_resource_browser.show_all();
+		}
+
+		private void on_engine_restart(Gtk.Action action)
+		{
+			restart_engine();
+		}
+
+		private void on_reload_lua(Gtk.Action action)
+		{
+			_compiler.send(EngineAPI.compile(Guid.new_guid(), _bundle_dir, _platform));
+			// _engine.send(EngineAPI.pause());
+			// _engine.send(EngineAPI.reload("lua", "core/level_editor/level_editor"));
+			// _engine.send(EngineAPI.unpause());
+		}
+
+		public void on_snap_to_grid(Gtk.Action action)
+		{
+			ToggleAction ta = (ToggleAction)action;
+			_snap_to_grid = ta.active;
+			send_state();
+		}
+
+		private void on_debug_render_world(Gtk.Action action)
+		{
+			ToggleAction ta = (ToggleAction)action;
+			_debug_render_world = ta.active;
+			send_state();
+		}
+
+		private void on_debug_physics_world(Gtk.Action action)
+		{
+			ToggleAction ta = (ToggleAction)action;
+			_debug_physics_world = ta.active;
+			send_state();
+		}
+
+		private void on_run_game(Gtk.Action action)
+		{
+			start_game();
+		}
+
+		private void on_undo(Gtk.Action action)
+		{
+			_db.undo_single_action();
+		}
+
+		private void on_redo(Gtk.Action action)
+		{
+			_db.redo_single_action();
+		}
+
+		private void on_duplicate(Gtk.Action action)
+		{
+			_level.duplicate_selected_objects();
+		}
+
+		private void on_delete(Gtk.Action action)
+		{
+			_level.destroy_selected_objects();
+		}
+
+		private void on_manual(Gtk.Action action)
+		{
+			try
+			{
+				AppInfo.launch_default_for_uri("https://taylor001.github.io/crown/manual.html", null);
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+		}
+
+		private void on_report_issue(Gtk.Action action)
+		{
+			try
+			{
+				AppInfo.launch_default_for_uri("https://github.com/taylor001/crown/issues", null);
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+		}
+
+		private void on_open_last_log(Gtk.Action action)
+		{
+			File file = File.new_for_path(_bundle_dir + "/last.log");
+			try
+			{
+				AppInfo.launch_default_for_uri(file.get_uri(), null);
+			}
+			catch (Error e)
+			{
+				_console_view.log(e.message, "error");
+			}
+		}
+
+		private void on_about(Gtk.Action action)
+		{
+			Gtk.AboutDialog dlg = new Gtk.AboutDialog();
+			dlg.set_destroy_with_parent(true);
+			dlg.set_transient_for(this);
+			dlg.set_modal(true);
+
+			try
+			{
+				dlg.set_logo(new Pixbuf.from_file("icons/pepper-128x128.png"));
+			}
+			catch (Error e)
+			{
+				stderr.printf("%s\n", e.message);
+			}
+
+			dlg.program_name = "Crown Game Engine";
+			dlg.version = "0.0.16";
+			dlg.website = "https://github.com/taylor001/crown";
+			dlg.copyright = "Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.";
+			dlg.license = "Crown Game Engine.\n"
+				+ "Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.\n"
+				+ "\n"
+				+ "This program is free software; you can redistribute it and/or\n"
+				+ "modify it under the terms of the GNU General Public License\n"
+				+ "as published by the Free Software Foundation; either version 2\n"
+				+ "of the License, or (at your option) any later version.\n"
+				+ "\n"
+				+ "This program is distributed in the hope that it will be useful,\n"
+				+ "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
+				+ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
+				+ "GNU General Public License for more details.\n"
+				+ "\n"
+				+ "You should have received a copy of the GNU General Public License\n"
+				+ "along with this program; if not, write to the Free Software\n"
+				+ "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n"
+				;
+			dlg.run();
+			dlg.destroy();
+		}
+
+		private void on_destroy()
+		{
+			Gtk.main_quit();
+		}
+
+		private bool on_delete_event()
+		{
+			quit();
+			return true;
+		}
+	}
+
+	public static int main (string[] args)
+	{
+		Gtk.init(ref args);
+		Intl.setlocale(LocaleCategory.ALL, "C");
+
+		var window = new LevelEditor(args[1], args[2]);
+		window.show_all();
+
+		Gtk.main();
+		return 0;
+	 }
+}

+ 499 - 0
tools/level_editor/properties_view.vala

@@ -0,0 +1,499 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using Gtk;
+using Gee;
+
+namespace Crown
+{
+	public class PropertyRow : Gtk.Bin
+	{
+		public PropertyRow(Gtk.Widget child)
+		{
+			this.hexpand = true;
+			add(child);
+		}
+	}
+
+	public class ComponentView : Gtk.Grid
+	{
+		// Data
+		public Guid _unit_id;
+		public Guid _component_id;
+
+		public ComponentView()
+		{
+			// Data
+			_unit_id = GUID_ZERO;
+			_component_id = GUID_ZERO;
+		}
+
+		public void attach_row(uint row, string label, Gtk.Widget w)
+		{
+			Gtk.Label l = new Label(label);
+			l.width_chars = 10;
+			l.set_alignment(1.0f, 0.5f);
+			l.set_padding(4, 0);
+
+			PropertyRow r = new PropertyRow(w);
+
+			this.attach(l, 0, (int)row);
+			this.attach(r, 1, (int)row);
+		}
+
+		public virtual void update()
+		{
+		}
+	}
+
+	public class TransformComponentView : ComponentView
+	{
+		// Data
+		Level _level;
+
+		// Widgets
+		private SpinButtonVector3 _position;
+		private SpinButtonRotation _rotation;
+		private SpinButtonVector3 _scale;
+
+		public TransformComponentView(Level level)
+		{
+			// Data
+			_level = level;
+			_unit_id = GUID_ZERO;
+			_component_id = GUID_ZERO;
+
+			// Widgets
+			_position = new SpinButtonVector3(Vector3(0, 0, 0), Vector3(-9999.9, -9999.9, -9999.9), Vector3(9999.9, 9999.9, 9999.9));
+			_rotation = new SpinButtonRotation(Vector3(0, 0, 0));
+			_scale    = new SpinButtonVector3(Vector3(0, 0, 0), Vector3(0.1, 0.1, 0.1), Vector3(10.0, 10.0, 10.0));
+
+			_position.value_changed.connect(on_value_changed);
+			_rotation.value_changed.connect(on_value_changed);
+			_scale.value_changed.connect(on_value_changed);
+
+			uint row = 0;
+			attach_row(row++, "Position", _position);
+			attach_row(row++, "Rotation", _rotation);
+			attach_row(row++, "Scale", _scale);
+		}
+
+		private void on_value_changed()
+		{
+			Vector3 pos    = _position.value;
+			Quaternion rot = _rotation.value;
+			Vector3 scl    = _scale.value;
+
+			_level.move_selected_objects(pos, rot, scl);
+		}
+
+		public override void update()
+		{
+			Vector3 pos    = (Vector3)   _level.get_component_property(_unit_id, _component_id, "data.position");
+			Quaternion rot = (Quaternion)_level.get_component_property(_unit_id, _component_id, "data.rotation");
+			Vector3 scl    = (Vector3)   _level.get_component_property(_unit_id, _component_id, "data.scale");
+
+			_position.value = pos;
+			_rotation.value = rot;
+			_scale.value    = scl;
+		}
+	}
+
+	public class LightComponentView : ComponentView
+	{
+		// Data
+		Level _level;
+
+		// Widgets
+		private ComboBoxMap _type;
+		private SpinButtonDouble _range;
+		private SpinButtonDouble _intensity;
+		private SpinButtonDouble _spot_angle;
+		private Gtk.ColorButton _color;
+
+		public LightComponentView(Level level)
+		{
+			// Data
+			_level = level;
+
+			// Widgets
+			_type       = new ComboBoxMap();
+			_type.append("directional", "Directional");
+			_type.append("omni", "Omni");
+			_type.append("spot", "Spot");
+			_range      = new SpinButtonDouble(0.0, 0.0, 999.0);
+			_intensity  = new SpinButtonDouble(0.0, 0.0,  10.0);
+			_spot_angle = new SpinButtonDouble(0.0, 0.0,  90.0);
+			_color      = new ColorButton();
+
+			_range.value_changed.connect(on_value_changed);
+			_intensity.value_changed.connect(on_value_changed);
+			_spot_angle.value_changed.connect(on_value_changed);
+			_color.color_set.connect(on_value_changed);
+
+			uint row = 0;
+			attach_row(row++, "Type", _type);
+			attach_row(row++, "Range", _range);
+			attach_row(row++, "Intensity", _intensity);
+			attach_row(row++, "Spot Angle", _spot_angle);
+			attach_row(row++, "Color", _color);
+		}
+
+		private void on_value_changed()
+		{
+			// Vector3 color = Vector3((double)_color.Color.Red/(double)ushort.MaxValue
+			// 	, (double)_color.Color.Green/(double)ushort.MaxValue
+			// 	, (double)_color.Color.Blue/(double)ushort.MaxValue
+			// 	);
+
+			_level.set_component_property(_unit_id, _component_id, "data.type",       "directional"); // FIXME
+			_level.set_component_property(_unit_id, _component_id, "data.range",      _range.value);
+			_level.set_component_property(_unit_id, _component_id, "data.intensity",  _intensity.value);
+			_level.set_component_property(_unit_id, _component_id, "data.spot_angle", _spot_angle.value);
+			// _level.set_component_property(_unit_id, _component_id, "data.color",      color);
+			_level.set_component_property(_unit_id, _component_id, "type", "light");
+		}
+
+		public override void update()
+		{
+			string type       = (string) _level.get_component_property(_unit_id, _component_id, "data.type");
+			double range      = (double) _level.get_component_property(_unit_id, _component_id, "data.range");
+			double intensity  = (double) _level.get_component_property(_unit_id, _component_id, "data.intensity");
+			double spot_angle = (double) _level.get_component_property(_unit_id, _component_id, "data.spot_angle");
+			Vector3 color     = (Vector3)_level.get_component_property(_unit_id, _component_id, "data.color");
+
+			_range.value      = range;
+			_intensity.value  = intensity;
+			_spot_angle.value = spot_angle;
+			// _color.color      = new Gdk.Color((byte)(255.0*color.x), (byte)(255.0*color.y), (byte)(255.0*color.z));
+		}
+	}
+
+	public class CameraComponentView : ComponentView
+	{
+		// Data
+		Level _level;
+
+		// Widgets
+		private ComboBoxMap _projection;
+		private SpinButtonDouble _fov;
+		private SpinButtonDouble _near_range;
+		private SpinButtonDouble _far_range;
+
+		public CameraComponentView(Level level)
+		{
+			// Data
+			_level = level;
+
+			// Widgets
+			_projection = new ComboBoxMap();
+			_projection.append("orthographic", "Orthographic");
+			_projection.append("perspective", "Perspective");
+			_fov        = new SpinButtonDouble(0.0, 1.0,   90.0);
+			_near_range = new SpinButtonDouble(0.0, 0.001, 9999.0);
+			_far_range  = new SpinButtonDouble(0.0, 0.001, 9999.0);
+
+			uint row = 0;
+			attach_row(row++, "Projection", _projection);
+			attach_row(row++, "FOV", _fov);
+			attach_row(row++, "Near Range", _near_range);
+			attach_row(row++, "Far Range", _far_range);
+		}
+
+		public override void update()
+		{
+			string type       = (string)_level.get_component_property(_unit_id, _component_id, "data.projection");
+			double fov        = (double)_level.get_component_property(_unit_id, _component_id, "data.fov");
+			double near_range = (double)_level.get_component_property(_unit_id, _component_id, "data.near_range");
+			double far_range  = (double)_level.get_component_property(_unit_id, _component_id, "data.far_range");
+
+			_fov.value        = MathUtils.deg(fov);
+			_near_range.value = near_range;
+			_far_range.value  = far_range;
+		}
+	}
+
+	public class MeshRendererComponentView : ComponentView
+	{
+		// Data
+		Level _level;
+
+		// Widgets
+		private Gtk.Entry _mesh_resource;
+		private Gtk.Entry _geometry;
+		private Gtk.Entry _material;
+		private Gtk.CheckButton _visible;
+
+		public MeshRendererComponentView(Level level)
+		{
+			// Data
+			_level = level;
+
+			// Widgets
+			_mesh_resource = new Gtk.Entry();
+			_geometry = new Gtk.Entry();
+			_material = new Gtk.Entry();
+			_visible = new Gtk.CheckButton();
+			_mesh_resource.sensitive = false;
+			_geometry.sensitive = false;
+			_material.sensitive = false;
+
+			uint row = 0;
+			attach_row(row++, "Mesh", _mesh_resource);
+			attach_row(row++, "Geometry", _geometry);
+			attach_row(row++, "Material", _material);
+			attach_row(row++, "Visible", _visible);
+		}
+
+		public override void update()
+		{
+			_mesh_resource.text = (string)_level.get_component_property(_unit_id, _component_id, "data.mesh_resource");
+			_geometry.text      = (string)_level.get_component_property(_unit_id, _component_id, "data.geometry_name");
+			_material.text      = (string)_level.get_component_property(_unit_id, _component_id, "data.material");
+			_visible.active     = (bool)  _level.get_component_property(_unit_id, _component_id, "data.visible");
+		}
+	}
+
+	public class SoundTransformView : ComponentView
+	{
+		// Data
+		Level _level;
+
+		// Widgets
+		private SpinButtonVector3 _position;
+		private SpinButtonRotation _rotation;
+
+		public SoundTransformView(Level level)
+		{
+			// Data
+			_level = level;
+			_unit_id = GUID_ZERO;
+			_component_id = GUID_ZERO;
+
+			// Widgets
+			_position = new SpinButtonVector3(Vector3(0, 0, 0), Vector3(-9999.9, -9999.9, -9999.9), Vector3(9999.9, 9999.9, 9999.9));
+			_rotation = new SpinButtonRotation(Vector3(0, 0, 0));
+
+			_position.value_changed.connect(on_value_changed);
+			_rotation.value_changed.connect(on_value_changed);
+
+			uint row = 0;
+			attach_row(row++, "Position", _position);
+			attach_row(row++, "Rotation", _rotation);
+		}
+
+		private void on_value_changed()
+		{
+			Vector3 pos    = _position.value;
+			Quaternion rot = _rotation.value;
+
+			_level.move_selected_objects(pos, rot, Vector3(1.0, 1.0, 1.0));
+		}
+
+		public override void update()
+		{
+			Vector3 pos    = (Vector3)   _level.get_property(_component_id, "position");
+			Quaternion rot = (Quaternion)_level.get_property(_component_id, "rotation");
+
+			_position.value = pos;
+			_rotation.value = rot;
+		}
+	}
+	public class SoundPropertiesView : ComponentView
+	{
+		// Data
+		Level _level;
+
+		// Widgets
+		private Gtk.Entry _name;
+		private SpinButtonDouble _range;
+		private SpinButtonDouble _volume;
+		private Gtk.CheckButton _loop;
+
+		public SoundPropertiesView(Level level)
+		{
+			// Data
+			_level = level;
+
+			// Widgets
+			_name   = new Gtk.Entry();
+			_range  = new SpinButtonDouble(0.0, 0.001, 9999.0);
+			_volume = new SpinButtonDouble(0.0, 0.0,   1.0);
+			_loop   = new Gtk.CheckButton();
+			_name.sensitive = false;
+
+			_range.value_changed.connect(on_range_changed);
+			_volume.value_changed.connect(on_volume_changed);
+			_loop.toggled.connect(on_loop_toggled);
+
+			uint row = 0;
+			attach_row(row++, "Name", _name);
+			attach_row(row++, "Range", _range);
+			attach_row(row++, "Volume", _volume);
+			attach_row(row++, "Loop", _loop);
+		}
+
+		private void on_range_changed()
+		{
+			_level.set_property(_component_id, "range", _range.value);
+		}
+
+		private void on_volume_changed()
+		{
+			_level.set_property(_component_id, "volume", _range.value);
+		}
+
+		private void on_loop_toggled()
+		{
+			_level.set_property(_component_id, "loop", _loop.active);
+		}
+
+		public override void update()
+		{
+			_name.text    = (string)_level.get_property(_component_id, "name");
+			_range.value  = (double)_level.get_property(_component_id, "range");
+			_volume.value = (double)_level.get_property(_component_id, "volume");
+			_loop.active  = (bool)  _level.get_property(_component_id, "loop");
+		}
+	}
+
+	public class PropertiesView : Gtk.Bin
+	{
+		// Data
+		private Level _level;
+		private HashMap<string, ComponentView> _components;
+
+		// Widgets
+		private Gtk.Label _nothing_to_show;
+		private Gtk.Viewport _viewport;
+		private Gtk.ScrolledWindow _scrolled_window;
+		private Gtk.Box _components_vbox;
+		private Gtk.Widget _current_widget;
+
+		public PropertiesView(Level level)
+		{
+			// Data
+			_level = level;
+			_level.selection_changed.connect(on_selection_changed);
+
+			_components = new HashMap<string, ComponentView>();
+
+			// Widgets
+			_components_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+			_components_vbox.margin_right = 18;
+
+			// Unit
+			add_component_view("Transform", "transform", new TransformComponentView(_level));
+			add_component_view("Light", "light", new LightComponentView(_level));
+			add_component_view("Camera", "camera", new CameraComponentView(_level));
+			add_component_view("Mesh Renderer", "mesh_renderer", new MeshRendererComponentView(_level));
+
+			// Sound
+			add_component_view("Transform", "sound_transform", new SoundTransformView(_level));
+			add_component_view("Sound Properties", "sound_properties", new SoundPropertiesView(_level));
+
+			_nothing_to_show = new Gtk.Label("Nothing to show");
+
+			_viewport = new Gtk.Viewport(null, null);
+			_viewport.add(_components_vbox);
+
+			_scrolled_window = new Gtk.ScrolledWindow(null, null);
+			_scrolled_window.add(_viewport);
+
+			_current_widget = null;
+
+			set_current_widget(_nothing_to_show);
+
+			set_size_request(470, 200);
+		}
+
+		private void add_component_view(string label, string component_type, ComponentView cv)
+		{
+			Gtk.Label lb = new Gtk.Label(null);
+			lb.set_markup("<b>%s</b>".printf(label));
+			lb.set_alignment(0.0f, 0.5f);
+
+			Gtk.Expander expander = new Gtk.Expander("");
+			expander.label_widget = lb;
+			expander.child = cv;
+			expander.expanded = true;
+
+			_components[component_type] = cv;
+			_components_vbox.pack_start(expander, false, true, 0);
+		}
+
+		private void set_current_widget(Gtk.Widget w)
+		{
+			if (_current_widget != null)
+			{
+				_current_widget.hide();
+				remove(_current_widget);
+			}
+
+			_current_widget = w;
+			_current_widget.show_all();
+			add(_current_widget);
+		}
+
+		private void on_selection_changed(Gee.ArrayList<Guid?> selection)
+		{
+			if (selection.size == 0)
+			{
+				set_current_widget(_nothing_to_show);
+				return;
+			}
+
+			Guid id = selection[selection.size - 1];
+
+			if (_level.is_unit(id))
+			{
+				set_current_widget(_scrolled_window);
+
+				Guid unit_id = id;
+
+				foreach (var entry in _components.entries)
+				{
+					ComponentView cv = entry.value;
+					cv.hide();
+				}
+				foreach (var entry in _components.entries)
+				{
+					ComponentView cv = entry.value;
+					Guid component_id = GUID_ZERO;
+					if (_level.has_component(unit_id, entry.key, ref component_id))
+					{
+						cv._unit_id = unit_id;
+						cv._component_id = component_id;
+						cv.update();
+						cv.show_all();
+					}
+				}
+			}
+			else if (_level.is_sound(id))
+			{
+				set_current_widget(_scrolled_window);
+
+				foreach (var entry in _components.entries)
+				{
+					ComponentView cv = entry.value;
+					cv.hide();
+				}
+
+				ComponentView st = _components["sound_transform"];
+				ComponentView sp = _components["sound_properties"];
+				st._component_id = id;
+				st.update();
+				st.show_all();
+				sp._component_id = id;
+				sp.update();
+				sp.show_all();
+			}
+			else
+			{
+				set_current_widget(_nothing_to_show);
+			}
+		}
+	}
+}

+ 230 - 0
tools/level_editor/resource_browser.vala

@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using Gtk;
+
+namespace Crown
+{
+	public class ResourceBrowser : Gtk.Window
+	{
+		// Project paths
+		private string _source_dir;
+		private string _bundle_dir;
+
+		// Data
+		private GLib.Subprocess _engine_process;
+		private ConsoleClient _console_client;
+
+		// Widgets
+		private Gtk.Entry _filter_entry;
+		private Gtk.TreeStore _tree_store;
+		private Gtk.TreeModelFilter _tree_filter;
+		private Gtk.TreeView _tree_view;
+		private Gtk.ScrolledWindow _scrolled_window;
+		private Gtk.Box _box;
+
+		private EngineView _engine_view;
+
+		// Signals
+		public signal void resource_selected(PlaceableType placeable_type, string name);
+
+		public ResourceBrowser(string source_dir, string bundle_dir)
+		{
+			this.title = "Resource Browser";
+
+			// Project paths
+			_source_dir = source_dir;
+			_bundle_dir = bundle_dir;
+
+			// Data
+			_console_client = new ConsoleClient();
+
+			// Widgets
+			this.destroy.connect(on_destroy);
+
+			_filter_entry = new Gtk.SearchEntry();
+			_filter_entry.set_placeholder_text("Search...");
+			_filter_entry.changed.connect(on_filter_entry_text_changed);
+
+			_tree_store = new Gtk.TreeStore(2, typeof(string), typeof(PlaceableType));
+			_tree_filter = new Gtk.TreeModelFilter(_tree_store, null);
+			_tree_filter.set_visible_func(filter_tree);
+
+			_tree_view = new Gtk.TreeView();
+			_tree_view.insert_column_with_attributes(-1, "Name", new Gtk.CellRendererText(), "text", 0, null);
+			_tree_view.model = _tree_filter;
+			_tree_view.headers_visible = false;
+			_tree_view.row_activated.connect(on_row_activated);
+			_tree_view.button_press_event.connect(on_button_pressed);
+
+			_scrolled_window = new Gtk.ScrolledWindow(null, null);
+			_scrolled_window.add(_tree_view);
+			_scrolled_window.set_size_request(300, 400);
+
+			walk_directory_tree();
+
+			_engine_view = new EngineView(_console_client, false);
+			_engine_view.realized.connect(on_engine_view_realized);
+
+			_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+			_box.pack_start(_filter_entry, false, false, 0);
+			_box.pack_start(_scrolled_window, false, false, 0);
+			_box.pack_start(_engine_view, true, true, 0);
+
+			add(_box);
+			set_size_request(300, 400);
+		}
+
+		private void on_row_activated(Gtk.TreePath path2, TreeViewColumn column)
+		{
+			Gtk.TreePath path = _tree_filter.convert_path_to_child_path(path2);
+			Gtk.TreeIter iter;
+			if (_tree_store.get_iter(out iter, path))
+			{
+				Value name;
+				Value type;
+				_tree_store.get_value(iter, 0, out name);
+				_tree_store.get_value(iter, 1, out type);
+				resource_selected((PlaceableType)type, (string)name);
+
+				string s = UnitPreviewAPI.set_preview_unit((PlaceableType)type, (string)name);
+				_console_client.send_script(s);
+			}
+		}
+
+		private bool on_button_pressed(Gdk.EventButton ev)
+		{
+			int button = (int)ev.button;
+
+			if (button == 1)
+			{
+				Gtk.TreePath path;
+				int cell_x;
+				int cell_y;
+				if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, out cell_x, out cell_y))
+				{
+					Gtk.TreePath child_path = _tree_filter.convert_path_to_child_path(path);
+					Gtk.TreeIter iter;
+					if (_tree_store.get_iter(out iter, child_path))
+					{
+						Value name;
+						Value type;
+						_tree_store.get_value(iter, 0, out name);
+						_tree_store.get_value(iter, 1, out type);
+						resource_selected((PlaceableType)type, (string)name);
+
+						string s = UnitPreviewAPI.set_preview_unit((PlaceableType)type, (string)name);
+						_console_client.send_script(s);
+					}
+				}
+			}
+
+			return false;
+		}
+
+		private void on_destroy()
+		{
+			if (_console_client.is_connected())
+				_console_client.close();
+
+			if (_engine_process != null)
+				_engine_process.force_exit();
+		}
+
+		private void start_engine(uint window_xid)
+		{
+			string args[] =
+			{
+				ENGINE_EXE,
+				"--bundle-dir", _bundle_dir,
+				"--boot-dir", "core/unit_preview",
+				"--parent-window", window_xid.to_string(),
+				"--console-port", "10002",
+				"--wait-console",
+				null
+			};
+
+			GLib.SubprocessLauncher sl = new GLib.SubprocessLauncher(SubprocessFlags.NONE);
+			sl.set_cwd(ENGINE_DIR);
+			try
+			{
+				_engine_process = sl.spawnv(args);
+			}
+			catch (Error e)
+			{
+				GLib.stderr.printf("%s\n", e.message);
+			}
+
+			while (!_console_client.is_connected())
+				_console_client.connect("127.0.0.1", 10002);
+		}
+
+		private void on_engine_view_realized()
+		{
+			start_engine(_engine_view.window_id);
+		}
+
+		private void on_filter_entry_text_changed()
+		{
+			_tree_filter.refilter();
+		}
+
+		private bool filter_tree(Gtk.TreeModel model, Gtk.TreeIter iter)
+		{
+			Value name;
+			model.get_value(iter, 0, out name);
+
+			if (_filter_entry.text == "")
+					 return true;
+
+			string text = (string)name;
+			if (text.index_of(_filter_entry.text) > -1)
+				return true;
+			else
+				return false;
+		}
+
+		private void walk_directory_tree()
+		{
+			_tree_view.model = null;
+			File file = File.new_for_path(_source_dir);
+			list_children(file);
+			_tree_view.model = _tree_filter;
+		}
+
+		private void list_children(File file, string space = "", Cancellable? cancellable = null) throws Error
+		{
+			FileEnumerator enumerator = file.enumerate_children ("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+
+			FileInfo info = null;
+			while (cancellable.is_cancelled () == false && ((info = enumerator.next_file (cancellable)) != null)) {
+				if (info.get_file_type () == FileType.DIRECTORY)
+				{
+					File subdir = file.resolve_relative_path (info.get_name ());
+					list_children (subdir, space + " ", cancellable);
+				}
+				else
+				{
+					string path = file.get_path() + "/" + info.get_name();
+					string path_rel = File.new_for_path(_source_dir).get_relative_path(File.new_for_path(path));
+					string type = path_rel.substring(path_rel.last_index_of("."), path_rel.length - path_rel.last_index_of("."));
+					string name = path_rel.substring(0, path_rel.last_index_of("."));
+
+					if (type == ".unit" || type == ".sound")
+					{
+						Gtk.TreeIter resource_iter;
+						_tree_store.append(out resource_iter, null);
+						_tree_store.set(resource_iter, 0, name, 1, type == ".unit" ? PlaceableType.UNIT : PlaceableType.SOUND, -1);
+					}
+				}
+			}
+
+			if (cancellable.is_cancelled ())
+			{
+				throw new IOError.CANCELLED("Operation was cancelled");
+			}
+		}
+	}
+}

+ 19 - 0
tools/level_editor/starting_compiler.vala

@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2012-2016 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using Gtk;
+
+namespace Crown
+{
+	public class StartingCompiler : Gtk.Bin
+	{
+		public StartingCompiler()
+		{
+			add(new Gtk.Label("Compiling resources, please wait..."));
+			set_size_request(300, 300);
+			show_all();
+		}
+	}
+}