project.vala 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. /*
  2. * Copyright (c) 2012-2025 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public enum ImportResult
  8. {
  9. SUCCESS, ///< Data imported successfully.
  10. ERROR, ///< Error during import or elsewhere.
  11. CANCEL, ///< User cancelled the import.
  12. CALLBACK ///< The actual importing will happen in the provided callback.
  13. }
  14. public delegate void Import(ImportResult result);
  15. public class Project
  16. {
  17. public const string LEVEL_EDITOR_TEST_NAME = "_level_editor_test";
  18. public delegate ImportResult ImporterDelegate(ProjectStore project_store
  19. , string destination_dir
  20. , SList<string> filenames
  21. , Import import_result
  22. , Gtk.Window? parent_window
  23. );
  24. [Compact]
  25. public struct ImporterData
  26. {
  27. public unowned ImporterDelegate delegate;
  28. public Gee.ArrayList<string> extensions;
  29. public double order;
  30. public Gtk.FileFilter _filter;
  31. public unowned Import import_result;
  32. ImporterData()
  33. {
  34. delegate = null;
  35. extensions = new Gee.ArrayList<string>();
  36. order = 0.0;
  37. _filter = new Gtk.FileFilter();
  38. import_result = null;
  39. }
  40. public bool can_import_extension(string extension)
  41. {
  42. string e = extension.down();
  43. foreach (var ext in extensions) {
  44. if (e == ext)
  45. return true;
  46. }
  47. return false;
  48. }
  49. public bool can_import_filenames(GLib.SList<string> filenames)
  50. {
  51. foreach (var filename in filenames) {
  52. if (!can_import_extension(path_extension(filename)))
  53. return false;
  54. }
  55. return true;
  56. }
  57. }
  58. // Data
  59. public File? _source_dir;
  60. public File _toolchain_dir;
  61. public File _data_dir;
  62. public File _user_dir;
  63. public File _level_editor_test_level;
  64. public File _level_editor_test_package;
  65. public string _platform;
  66. public Database _files;
  67. public Gee.HashMap<string, Guid?> _map;
  68. public ImporterData _all_extensions_importer_data;
  69. public Gee.ArrayList<ImporterData?> _importers;
  70. public bool _data_compiled;
  71. public Hashtable _data_index;
  72. public signal void file_added(string type, string name, uint64 size, uint64 mtime);
  73. public signal void file_changed(string type, string name, uint64 size, uint64 mtime);
  74. public signal void file_removed(string type, string name);
  75. public signal void tree_added(string name);
  76. public signal void tree_removed(string name);
  77. public signal void project_reset();
  78. public signal void project_loaded();
  79. public Project()
  80. {
  81. #if CROWN_PLATFORM_WINDOWS
  82. _platform = "windows";
  83. #else
  84. _platform = "linux";
  85. #endif
  86. _files = new Database(this);
  87. _map = new Gee.HashMap<string, Guid?>();
  88. _all_extensions_importer_data = ImporterData();
  89. _all_extensions_importer_data.delegate = import_all_extensions;
  90. _importers = new Gee.ArrayList<ImporterData?>();
  91. _data_compiled = false;
  92. _data_index = new Hashtable();
  93. }
  94. public void data_compiled()
  95. {
  96. _data_compiled = true;
  97. try {
  98. string index_path = Path.build_filename(_data_dir.get_path(), "data_index.sjson");
  99. _data_index = SJSON.load_from_path(index_path);
  100. } catch (JsonSyntaxError e) {
  101. loge(e.message);
  102. }
  103. }
  104. public uint64 mtime(string type, string name)
  105. {
  106. var path = ResourceId.path(type, name);
  107. Guid id = _map[path];
  108. string mtime = _files.get_property_string(id, "mtime");
  109. return uint64.parse(mtime);
  110. }
  111. public void reset()
  112. {
  113. project_reset();
  114. _source_dir = null;
  115. _files.reset();
  116. _map.clear();
  117. }
  118. public bool is_loaded()
  119. {
  120. return _source_dir != null;
  121. }
  122. public void load(string source_dir)
  123. {
  124. reset();
  125. _source_dir = File.new_for_path(source_dir);
  126. _data_dir = File.new_for_path(_source_dir.get_path() + "_" + _platform);
  127. _level_editor_test_level = File.new_for_path(Path.build_filename(_source_dir.get_path(), LEVEL_EDITOR_TEST_NAME + ".level"));
  128. _level_editor_test_package = File.new_for_path(Path.build_filename(_source_dir.get_path(), LEVEL_EDITOR_TEST_NAME + ".package"));
  129. // Cleanup source directory from previous runs' garbage
  130. delete_garbage();
  131. _user_dir = GLib.File.new_for_path(GLib.Path.build_filename(Crown._data_dir.get_path(), "projects", StringId64(source_dir).to_string()));
  132. try {
  133. _user_dir.make_directory_with_parents();
  134. } catch (Error e) {
  135. /* Nobody cares */
  136. }
  137. project_loaded();
  138. }
  139. public void set_toolchain_dir(string toolchain_dir)
  140. {
  141. _toolchain_dir = File.new_for_path(toolchain_dir);
  142. }
  143. public void create_initial_files(string source_dir)
  144. {
  145. // Write boot.config
  146. {
  147. string text = "// Lua script to launch on boot"
  148. + "\nboot_script = \"core/game/boot\""
  149. + "\n"
  150. + "\n// Package to load on boot"
  151. + "\nboot_package = \"boot\""
  152. + "\n"
  153. + "\nwindow_title = \"New Project\""
  154. + "\n"
  155. + "\n// Linux-only configs"
  156. + "\nlinux = {"
  157. + "\n renderer = {"
  158. + "\n resolution = [ 1280 720 ]"
  159. + "\n }"
  160. + "\n}"
  161. + "\n"
  162. + "\n// Windows-only configs"
  163. + "\nwindows = {"
  164. + "\n renderer = {"
  165. + "\n resolution = [ 1280 720 ]"
  166. + "\n }"
  167. + "\n}"
  168. + "\n"
  169. ;
  170. string path = Path.build_filename(source_dir, "boot.config");
  171. FileStream fs = FileStream.open(path, "wb");
  172. if (fs != null)
  173. fs.write(text.data);
  174. }
  175. // Write boot.package
  176. {
  177. string text = "lua = ["
  178. + "\n \"core/game/boot\""
  179. + "\n]"
  180. + "\nshader = ["
  181. + "\n \"core/shaders/default\""
  182. + "\n]"
  183. + "\nphysics_config = ["
  184. + "\n \"global\""
  185. + "\n]"
  186. + "\nunit = ["
  187. + "\n \"core/units/camera\""
  188. + "\n]"
  189. + "\n"
  190. ;
  191. string path = Path.build_filename(source_dir, "boot.package");
  192. FileStream fs = FileStream.open(path, "wb");
  193. if (fs != null)
  194. fs.write(text.data);
  195. }
  196. // Write global.physics_config
  197. {
  198. string text = "materials = {"
  199. + "\n default = { friction = 0.8 rolling_friction = 0.5 restitution = 0.81 }"
  200. + "\n}"
  201. + "\n"
  202. + "\ncollision_filters = {"
  203. + "\n no_collision = { collides_with = [] }"
  204. + "\n default = { collides_with = [ \"default\" ] }"
  205. + "\n}"
  206. + "\n"
  207. + "\nactors = {"
  208. + "\n static = { dynamic = false }"
  209. + "\n dynamic = { dynamic = true }"
  210. + "\n keyframed = { dynamic = true kinematic = true disable_gravity = true }"
  211. + "\n}"
  212. + "\n"
  213. ;
  214. string path = Path.build_filename(source_dir, "global.physics_config");
  215. FileStream fs = FileStream.open(path, "wb");
  216. if (fs != null)
  217. fs.write(text.data);
  218. }
  219. // Write main.lua
  220. {
  221. string text = "require \"core/game/camera\""
  222. + "\n"
  223. + "\nGame = Game or {"
  224. + "\n sg = nil,"
  225. + "\n pw = nil,"
  226. + "\n rw = nil,"
  227. + "\n camera = nil,"
  228. + "\n}"
  229. + "\n"
  230. + "\nGameBase.game = Game"
  231. + "\nGameBase.game_level = nil"
  232. + "\n"
  233. + "\nfunction Game.level_loaded()"
  234. + "\n Device.enable_resource_autoload(true)"
  235. + "\n"
  236. + "\n Game.sg = World.scene_graph(GameBase.world)"
  237. + "\n Game.pw = World.physics_world(GameBase.world)"
  238. + "\n Game.rw = World.render_world(GameBase.world)"
  239. + "\n Game.camera = FPSCamera(GameBase.world, GameBase.camera_unit)"
  240. + "\nend"
  241. + "\n"
  242. + "\nfunction Game.update(dt)"
  243. + "\n -- Stop the engine when the 'ESC' key is released"
  244. + "\n if Keyboard.released(Keyboard.button_id(\"escape\")) then"
  245. + "\n Device.quit()"
  246. + "\n end"
  247. + "\n"
  248. + "\n -- Update camera"
  249. + "\n local delta = Vector3.zero()"
  250. + "\n if Mouse.pressed(Mouse.button_id(\"right\")) then move = true end"
  251. + "\n if Mouse.released(Mouse.button_id(\"right\")) then move = false end"
  252. + "\n if move then delta = Mouse.axis(Mouse.axis_id(\"cursor_delta\")) end"
  253. + "\n Game.camera:update(dt, delta.x, delta.y)"
  254. + "\nend"
  255. + "\n"
  256. + "\nfunction Game.render(dt)"
  257. + "\nend"
  258. + "\n"
  259. + "\nfunction Game.shutdown()"
  260. + "\nend"
  261. + "\n"
  262. ;
  263. string path = Path.build_filename(source_dir, "main.lua");
  264. FileStream fs = FileStream.open(path, "wb");
  265. if (fs != null)
  266. fs.write(text.data);
  267. }
  268. }
  269. public int create_script(string directory, string name, bool empty)
  270. {
  271. string script_path = Path.build_filename(directory, name + ".lua");
  272. string path = this.absolute_path(script_path);
  273. FileStream fs = FileStream.open(path, "wb");
  274. if (fs != null) {
  275. if (empty) {
  276. return fs.puts("\n");
  277. } else {
  278. string text = "local Behavior = Behavior or {}"
  279. + "\nlocal Data = Data or {}"
  280. + "\n"
  281. + "\nfunction Behavior.spawned(world, units)"
  282. + "\n if Data[world] == nil then"
  283. + "\n Data[world] = {}"
  284. + "\n end"
  285. + "\n"
  286. + "\n for uu = 1, #units do"
  287. + "\n local unit = units[uu]"
  288. + "\n"
  289. + "\n -- Store instance-specific data"
  290. + "\n if Data[world][unit] == nil then"
  291. + "\n -- Data[world][unit] = {}"
  292. + "\n end"
  293. + "\n"
  294. + "\n -- Do something with the unit"
  295. + "\n end"
  296. + "\nend"
  297. + "\n"
  298. + "\nfunction Behavior.update(world, dt)"
  299. + "\n -- Update all units"
  300. + "\nend"
  301. + "\n"
  302. + "\nfunction Behavior.unspawned(world, units)"
  303. + "\n -- Cleanup"
  304. + "\n for uu = 1, #units do"
  305. + "\n if Data[world][units] then"
  306. + "\n Data[world][units] = nil"
  307. + "\n end"
  308. + "\n end"
  309. + "\nend"
  310. + "\n"
  311. + "\nreturn Behavior"
  312. + "\n"
  313. ;
  314. return fs.puts(text);
  315. }
  316. }
  317. return -1;
  318. }
  319. public int create_unit(string directory, string name)
  320. {
  321. string unit_path = Path.build_filename(directory, name + ".unit");
  322. string path = this.absolute_path(unit_path);
  323. Database db = new Database(this, null);
  324. Guid unit_id = Guid.new_guid();
  325. Unit unit = Unit(db, unit_id);
  326. unit.create_empty();
  327. db.dump(path, unit_id);
  328. return 0;
  329. }
  330. // Returns the absolute path to the source directory.
  331. public string source_dir()
  332. {
  333. if (_source_dir == null)
  334. return "";
  335. else
  336. return _source_dir.get_path();
  337. }
  338. // Returns the absolute path to the toolchain directory.
  339. public string toolchain_dir()
  340. {
  341. return _toolchain_dir.get_path();
  342. }
  343. // Returns the absolute path to the data directory.
  344. public string data_dir()
  345. {
  346. return _data_dir.get_path();
  347. }
  348. // Returns the absolute path to the user-specific data for this project.
  349. public string user_dir()
  350. {
  351. return _user_dir.get_path();
  352. }
  353. public string platform()
  354. {
  355. return _platform;
  356. }
  357. public string name()
  358. {
  359. string sd = source_dir();
  360. return sd.substring(sd.last_index_of_char(GLib.Path.DIR_SEPARATOR) + 1);
  361. }
  362. public bool path_is_within_source_dir(string path)
  363. {
  364. GLib.File file = GLib.File.new_for_path(path);
  365. return file.has_prefix(_source_dir);
  366. }
  367. public void delete_garbage()
  368. {
  369. try {
  370. _level_editor_test_level.delete();
  371. _level_editor_test_package.delete();
  372. } catch (GLib.Error e) {
  373. // Ignored
  374. }
  375. }
  376. /// Converts the @a resource_id to its corresponding human-readable @a
  377. /// resource_name. It returns true if the conversion is successful, otherwise
  378. /// it returns false and sets @a resource_name to the value of @a resource_id.
  379. public bool resource_id_to_name(out string resource_name, string resource_id)
  380. {
  381. Value? name = _data_index[resource_id];
  382. if (name != null) {
  383. resource_name = (string)name;
  384. return true;
  385. }
  386. resource_name = resource_id;
  387. return false;
  388. }
  389. public Database files()
  390. {
  391. return _files;
  392. }
  393. public void add_file(string path, uint64 size, uint64 mtime)
  394. {
  395. string type = path_extension(path);
  396. string name = type == "" ? path : path.substring(0, path.last_index_of("."));
  397. Guid id = Guid.new_guid();
  398. _files.create(id, OBJECT_TYPE_FILE);
  399. _files.set_property_string(id, "path", path);
  400. _files.set_property_string(id, "type", type);
  401. _files.set_property_string(id, "name", name);
  402. _files.set_property_string(id, "size", size.to_string());
  403. _files.set_property_string(id, "mtime", mtime.to_string());
  404. _files.add_to_set(GUID_ZERO, "data", id);
  405. _map[path] = id;
  406. file_added(type, name, size, mtime);
  407. }
  408. public void change_file(string path, uint64 size, uint64 mtime)
  409. {
  410. string type = path_extension(path);
  411. string name = type == "" ? path : path.substring(0, path.last_index_of("."));
  412. Guid id = _map[path];
  413. _files.set_property_string(id, "size", size.to_string());
  414. _files.set_property_string(id, "mtime", mtime.to_string());
  415. _data_compiled = false;
  416. file_changed(type, name, size, mtime);
  417. }
  418. public void remove_file(string path)
  419. {
  420. if (!_map.has_key(path)) {
  421. logw("remove_file: map does not contain path: %s".printf(path));
  422. return;
  423. }
  424. Guid id = _map[path];
  425. file_removed(_files.get_property_string(id, "type"), _files.get_property_string(id, "name"));
  426. _files.remove_from_set(GUID_ZERO, "data", id);
  427. _files.destroy(id);
  428. _map.unset(path);
  429. }
  430. public void add_tree(string path)
  431. {
  432. tree_added(path);
  433. }
  434. public void remove_tree(string path)
  435. {
  436. tree_removed(path);
  437. }
  438. public string resource_filename(string absolute_path)
  439. {
  440. string prefix = _source_dir.get_path();
  441. if (absolute_path.has_prefix(_toolchain_dir.get_path() + "/core"))
  442. prefix = _toolchain_dir.get_path();
  443. return File.new_for_path(prefix).get_relative_path(File.new_for_path(absolute_path));
  444. }
  445. public string absolute_path(string resource_path)
  446. {
  447. string prefix = _source_dir.get_path();
  448. if (resource_path.has_prefix("core/") || resource_path == "core")
  449. prefix = _toolchain_dir.get_path();
  450. return Path.build_filename(prefix, resource_path);
  451. }
  452. public static ImportResult import_all_extensions(ProjectStore project_store, string destination_dir, SList<string> filenames, Import import_result, Gtk.Window? parent_window)
  453. {
  454. Project project = project_store._project;
  455. Gee.ArrayList<string> paths = new Gee.ArrayList<string>();
  456. foreach (var item in filenames)
  457. paths.add(item);
  458. paths.sort((a, b) => {
  459. int ext_a = a.last_index_of_char('.');
  460. int ext_b = b.last_index_of_char('.');
  461. return strcmp(a[ext_a : a.length], b[ext_b : b.length]);
  462. });
  463. int result = 0;
  464. while (paths.size != 0 && result == ImportResult.SUCCESS) {
  465. // Find importer for the first file in the list of selected filenames.
  466. ImporterData? importer = project.find_importer_for_path(paths[0]);
  467. if (importer == null)
  468. return ImportResult.ERROR;
  469. // Create the list of all filenames importable by importer.
  470. Gee.ArrayList<string> importables = new Gee.ArrayList<string>();
  471. var cur = paths.list_iterator();
  472. for (var has_next = cur.next(); has_next; has_next = cur.next()) {
  473. string path = paths[cur.index()];
  474. if (importer.can_import_extension(path_extension(path))) {
  475. importables.add(path);
  476. cur.remove();
  477. }
  478. }
  479. // If importables is empty, filenames must have been filled with
  480. // un-importable filenames...
  481. if (importables.size == 0)
  482. return ImportResult.ERROR;
  483. // Convert importables to SList<string> to be used as delegate param.
  484. SList<string> importables_list = new SList<string>();
  485. foreach (var item in importables)
  486. importables_list.append(item);
  487. result = importer.delegate(project_store, destination_dir, importables_list, import_result, parent_window);
  488. }
  489. return result;
  490. }
  491. public class FileFilterFuncData
  492. {
  493. public string extension;
  494. public FileFilterFuncData(string ext)
  495. {
  496. extension = ext;
  497. }
  498. public bool handler(Gtk.FileFilterInfo info)
  499. {
  500. return info.filename.down().has_suffix("." + extension);
  501. }
  502. }
  503. // Returns a Gtk.FileFilter based on file @a extensions list.
  504. public Gtk.FileFilter create_gtk_file_filter(string name, Gee.ArrayList<string> extensions)
  505. {
  506. Gtk.FileFilter filter = new Gtk.FileFilter();
  507. string extensions_comma_separated = "";
  508. foreach (var ext in extensions) {
  509. extensions_comma_separated += "*.%s, ".printf(ext);
  510. FileFilterFuncData data = new FileFilterFuncData(ext);
  511. filter.add_custom(Gtk.FileFilterFlags.FILENAME, data.handler);
  512. }
  513. filter.set_filter_name(name + " (%s)".printf(extensions_comma_separated[0 : -2]));
  514. return filter;
  515. }
  516. public void register_importer_internal(string name, ref ImporterData data)
  517. {
  518. data._filter = create_gtk_file_filter(name, data.extensions);
  519. _importers.add(data);
  520. _importers.sort((a, b) => { return a.order < b.order ? -1 : 1; });
  521. // Skip duplicated extensions.
  522. foreach (string ext in data.extensions) {
  523. if (!_all_extensions_importer_data.extensions.contains(ext))
  524. _all_extensions_importer_data.extensions.add(ext);
  525. }
  526. _all_extensions_importer_data._filter = create_gtk_file_filter("All", _all_extensions_importer_data.extensions);
  527. }
  528. // Registers an @a importer for importing source data with the given @a
  529. // extensions. @a order is used to establish precedence when distinct importers
  530. // support similar extensions; lower values have higher precedence.
  531. public void register_importer(string name, string[] extensions, ImporterDelegate importer, Import import_result, double order)
  532. {
  533. ImporterData data = ImporterData();
  534. data.delegate = importer;
  535. data.extensions.add_all_array(extensions);
  536. data.order = order;
  537. data.import_result = import_result;
  538. register_importer_internal(name, ref data);
  539. }
  540. // Returns the preferable importer (lowest order values) which can import files
  541. // with the given @a extension.
  542. public ImporterData? find_importer_for_extension(string extension)
  543. {
  544. foreach (var imp in _importers) {
  545. if (imp.can_import_extension(extension))
  546. return imp;
  547. }
  548. return null;
  549. }
  550. public ImporterData? find_importer_for_path(string path)
  551. {
  552. return find_importer_for_extension(path_extension(path));
  553. }
  554. public bool is_type_importable(string type)
  555. {
  556. return find_importer_for_extension(type) != null;
  557. }
  558. public ImportResult import(string? destination_dir, Import import_result, ProjectStore project_store, Gtk.Window? parent_window = null)
  559. {
  560. Gtk.FileChooserDialog src = new Gtk.FileChooserDialog("Import..."
  561. , parent_window
  562. , Gtk.FileChooserAction.OPEN
  563. , "Cancel"
  564. , Gtk.ResponseType.CANCEL
  565. , "Open"
  566. , Gtk.ResponseType.ACCEPT
  567. );
  568. src.select_multiple = true;
  569. foreach (var importer in _importers)
  570. src.add_filter(importer._filter);
  571. src.add_filter(_all_extensions_importer_data._filter);
  572. src.set_filter(_all_extensions_importer_data._filter);
  573. if (src.run() != (int)Gtk.ResponseType.ACCEPT) {
  574. src.destroy();
  575. return ImportResult.CANCEL;
  576. }
  577. string out_dir = "";
  578. if (destination_dir == null) {
  579. Gtk.FileChooserDialog dst = new Gtk.FileChooserDialog("Select destination folder..."
  580. , parent_window
  581. , Gtk.FileChooserAction.SELECT_FOLDER
  582. , "Cancel"
  583. , Gtk.ResponseType.CANCEL
  584. , "Select"
  585. , Gtk.ResponseType.ACCEPT
  586. );
  587. dst.set_current_folder(this.source_dir());
  588. if (dst.run() != (int)Gtk.ResponseType.ACCEPT) {
  589. dst.destroy();
  590. src.destroy();
  591. return ImportResult.CANCEL;
  592. }
  593. out_dir = dst.get_filename();
  594. dst.destroy();
  595. } else {
  596. out_dir = this.absolute_path(destination_dir);
  597. }
  598. Gtk.FileFilter? current_filter = src.get_filter();
  599. GLib.SList<string> filenames = src.get_filenames();
  600. src.destroy();
  601. // Find importer callback.
  602. unowned ImporterDelegate? importer = null;
  603. foreach (var imp in _importers) {
  604. if (imp._filter == current_filter && imp.can_import_filenames(filenames)) {
  605. importer = imp.delegate;
  606. break;
  607. }
  608. }
  609. // Fallback if no importer found.
  610. if (importer == null)
  611. importer = _all_extensions_importer_data.delegate;
  612. return importer(project_store, out_dir, filenames, import_result, parent_window);
  613. }
  614. public void delete_tree(GLib.File file) throws Error
  615. {
  616. GLib.FileEnumerator fe = file.enumerate_children("standard::*"
  617. , GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS
  618. );
  619. GLib.FileInfo info = null;
  620. while ((info = fe.next_file()) != null) {
  621. GLib.File subfile = file.resolve_relative_path(info.get_name());
  622. if (info.get_file_type() == GLib.FileType.DIRECTORY)
  623. delete_tree(subfile);
  624. else
  625. subfile.delete();
  626. }
  627. file.delete();
  628. }
  629. }
  630. } /* namespace Crown */