project.vala 18 KB

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