| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788 |
- /*
- * Copyright (c) 2012-2025 Daniele Bartolini et al.
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
- namespace Crown
- {
- public enum ImportResult
- {
- SUCCESS, ///< Data imported successfully.
- ERROR, ///< Error during import or elsewhere.
- CANCEL, ///< User cancelled the import.
- }
- public delegate void Import(ImportResult result);
- public class Project
- {
- public const string LEVEL_EDITOR_TEST_NAME = "_level_editor_test";
- public delegate void ImporterDelegate(Import import_result
- , ProjectStore project_store
- , string destination_dir
- , SList<string> filenames
- , Gtk.Window? parent_window
- );
- [Compact]
- public struct ImporterData
- {
- public unowned ImporterDelegate delegate;
- public Gee.ArrayList<string> extensions;
- public double order;
- public Gtk.FileFilter _filter;
- public unowned Import import_result;
- ImporterData()
- {
- delegate = null;
- extensions = new Gee.ArrayList<string>();
- order = 0.0;
- _filter = new Gtk.FileFilter();
- import_result = null;
- }
- public bool can_import_extension(string extension)
- {
- string e = extension.down();
- foreach (var ext in extensions) {
- if (e == ext)
- return true;
- }
- return false;
- }
- public bool can_import_filenames(GLib.SList<string> filenames)
- {
- foreach (var filename in filenames) {
- if (!can_import_extension(path_extension(filename)))
- return false;
- }
- return true;
- }
- }
- // Data
- public File? _source_dir;
- public File _toolchain_dir;
- public File _data_dir;
- public File _user_dir;
- public File _level_editor_test_level;
- public File _level_editor_test_package;
- public string _platform;
- public Database _files;
- public Gee.HashMap<string, Guid?> _map;
- public ImporterData _all_extensions_importer_data;
- public Gee.ArrayList<ImporterData?> _importers;
- public bool _data_compiled;
- public Hashtable _data_index;
- public signal void file_added(string type, string name, uint64 size, uint64 mtime);
- public signal void file_changed(string type, string name, uint64 size, uint64 mtime);
- public signal void file_removed(string type, string name);
- public signal void tree_added(string name);
- public signal void tree_removed(string name);
- public signal void project_reset();
- public signal void project_loaded();
- public Project()
- {
- #if CROWN_PLATFORM_WINDOWS
- _platform = "windows";
- #else
- _platform = "linux";
- #endif
- _files = new Database(this);
- _map = new Gee.HashMap<string, Guid?>();
- _all_extensions_importer_data = ImporterData();
- _all_extensions_importer_data.delegate = import_all_extensions;
- _importers = new Gee.ArrayList<ImporterData?>();
- _data_compiled = false;
- _data_index = new Hashtable();
- }
- public void data_compiled()
- {
- _data_compiled = true;
- try {
- string index_path = Path.build_filename(_data_dir.get_path(), "data_index.sjson");
- _data_index = SJSON.load_from_path(index_path);
- } catch (JsonSyntaxError e) {
- loge(e.message);
- }
- }
- public uint64 mtime(string type, string name)
- {
- var path = ResourceId.path(type, name);
- Guid id = _map[path];
- string mtime = _files.get_property_string(id, "mtime");
- return uint64.parse(mtime);
- }
- public void reset()
- {
- project_reset();
- _source_dir = null;
- _files.reset();
- _map.clear();
- }
- public bool is_loaded()
- {
- return _source_dir != null;
- }
- public void load(string source_dir)
- {
- reset();
- _source_dir = File.new_for_path(source_dir);
- _data_dir = File.new_for_path(_source_dir.get_path() + "_" + _platform);
- _level_editor_test_level = File.new_for_path(Path.build_filename(_source_dir.get_path(), LEVEL_EDITOR_TEST_NAME + ".level"));
- _level_editor_test_package = File.new_for_path(Path.build_filename(_source_dir.get_path(), LEVEL_EDITOR_TEST_NAME + ".package"));
- // Cleanup source directory from previous runs' garbage
- delete_garbage();
- _user_dir = GLib.File.new_for_path(GLib.Path.build_filename(Crown._data_dir.get_path(), "projects", StringId64(source_dir).to_string()));
- try {
- _user_dir.make_directory_with_parents();
- } catch (Error e) {
- /* Nobody cares */
- }
- project_loaded();
- }
- public void set_toolchain_dir(string toolchain_dir)
- {
- _toolchain_dir = File.new_for_path(toolchain_dir);
- }
- public void create_initial_files(string source_dir)
- {
- // Write boot.config
- {
- string text = "// Lua script to launch on boot"
- + "\nboot_script = \"core/game/boot\""
- + "\n"
- + "\n// Package to load on boot"
- + "\nboot_package = \"boot\""
- + "\n"
- + "\nwindow_title = \"New Project\""
- + "\n"
- + "\n// Linux-only configs"
- + "\nlinux = {"
- + "\n renderer = {"
- + "\n resolution = [ 1280 720 ]"
- + "\n }"
- + "\n}"
- + "\n"
- + "\n// Windows-only configs"
- + "\nwindows = {"
- + "\n renderer = {"
- + "\n resolution = [ 1280 720 ]"
- + "\n }"
- + "\n}"
- + "\n"
- ;
- string path = Path.build_filename(source_dir, "boot.config");
- FileStream fs = FileStream.open(path, "wb");
- if (fs != null)
- fs.write(text.data);
- }
- // Write boot.package
- {
- string text = "lua = ["
- + "\n \"core/game/boot\""
- + "\n]"
- + "\nshader = ["
- + "\n \"core/shaders/default\""
- + "\n]"
- + "\nphysics_config = ["
- + "\n \"global\""
- + "\n]"
- + "\nunit = ["
- + "\n \"core/units/camera\""
- + "\n]"
- + "\n"
- ;
- string path = Path.build_filename(source_dir, "boot.package");
- FileStream fs = FileStream.open(path, "wb");
- if (fs != null)
- fs.write(text.data);
- }
- // Write global.physics_config
- {
- string text = "materials = {"
- + "\n default = { friction = 0.8 rolling_friction = 0.5 restitution = 0.81 }"
- + "\n}"
- + "\n"
- + "\ncollision_filters = {"
- + "\n no_collision = { collides_with = [] }"
- + "\n default = { collides_with = [ \"default\" ] }"
- + "\n}"
- + "\n"
- + "\nactors = {"
- + "\n static = { dynamic = false }"
- + "\n dynamic = { dynamic = true }"
- + "\n keyframed = { dynamic = true kinematic = true disable_gravity = true }"
- + "\n}"
- + "\n"
- ;
- string path = Path.build_filename(source_dir, "global.physics_config");
- FileStream fs = FileStream.open(path, "wb");
- if (fs != null)
- fs.write(text.data);
- }
- // Write main.lua
- {
- string text = "require \"core/game/camera\""
- + "\n"
- + "\nGame = Game or {"
- + "\n sg = nil,"
- + "\n pw = nil,"
- + "\n rw = nil,"
- + "\n camera = nil,"
- + "\n}"
- + "\n"
- + "\nGameBase.game = Game"
- + "\nGameBase.game_level = nil"
- + "\n"
- + "\nfunction Game.level_loaded()"
- + "\n Device.enable_resource_autoload(true)"
- + "\n"
- + "\n Game.sg = World.scene_graph(GameBase.world)"
- + "\n Game.pw = World.physics_world(GameBase.world)"
- + "\n Game.rw = World.render_world(GameBase.world)"
- + "\n Game.camera = FPSCamera(GameBase.world, GameBase.camera_unit)"
- + "\nend"
- + "\n"
- + "\nfunction Game.update(dt)"
- + "\n -- Stop the engine when the 'ESC' key is released"
- + "\n if Keyboard.released(Keyboard.button_id(\"escape\")) then"
- + "\n Device.quit()"
- + "\n end"
- + "\n"
- + "\n -- Update camera"
- + "\n local delta = Vector3.zero()"
- + "\n if Mouse.pressed(Mouse.button_id(\"right\")) then move = true end"
- + "\n if Mouse.released(Mouse.button_id(\"right\")) then move = false end"
- + "\n if move then delta = Mouse.axis(Mouse.axis_id(\"cursor_delta\")) end"
- + "\n Game.camera:update(dt, delta.x, delta.y)"
- + "\nend"
- + "\n"
- + "\nfunction Game.render(dt)"
- + "\nend"
- + "\n"
- + "\nfunction Game.shutdown()"
- + "\nend"
- + "\n"
- ;
- string path = Path.build_filename(source_dir, "main.lua");
- FileStream fs = FileStream.open(path, "wb");
- if (fs != null)
- fs.write(text.data);
- }
- }
- public int create_script(string directory, string name, bool empty)
- {
- string script_path = Path.build_filename(directory, name + ".lua");
- string path = this.absolute_path(script_path);
- FileStream fs = FileStream.open(path, "wb");
- if (fs != null) {
- if (empty) {
- return fs.puts("\n");
- } else {
- string text = "local Behavior = Behavior or {}"
- + "\nlocal Data = Data or {}"
- + "\n"
- + "\nfunction Behavior.spawned(world, units)"
- + "\n if Data[world] == nil then"
- + "\n Data[world] = {}"
- + "\n end"
- + "\n"
- + "\n for uu = 1, #units do"
- + "\n local unit = units[uu]"
- + "\n"
- + "\n -- Store instance-specific data"
- + "\n if Data[world][unit] == nil then"
- + "\n -- Data[world][unit] = {}"
- + "\n end"
- + "\n"
- + "\n -- Do something with the unit"
- + "\n end"
- + "\nend"
- + "\n"
- + "\nfunction Behavior.update(world, dt)"
- + "\n -- Update all units"
- + "\nend"
- + "\n"
- + "\nfunction Behavior.unspawned(world, units)"
- + "\n -- Cleanup"
- + "\n for uu = 1, #units do"
- + "\n if Data[world][units] then"
- + "\n Data[world][units] = nil"
- + "\n end"
- + "\n end"
- + "\nend"
- + "\n"
- + "\nreturn Behavior"
- + "\n"
- ;
- return fs.puts(text);
- }
- }
- return -1;
- }
- public int create_unit(string directory, string name)
- {
- string unit_path = Path.build_filename(directory, name + ".unit");
- string path = this.absolute_path(unit_path);
- Database db = new Database(this, null);
- Guid unit_id = Guid.new_guid();
- Unit unit = Unit(db, unit_id);
- unit.create_empty();
- return db.dump(path, unit_id);
- }
- public int create_state_machine(string directory, string name, string? skeleton_name)
- {
- string resource_name = Path.build_filename(directory, name);
- Database db = new Database(this, null);
- Guid node_id = Guid.new_guid();
- StateMachineNode sm_node = StateMachineNode(db, node_id);
- Guid machine_id = Guid.new_guid();
- StateMachineResource machine;
- if (skeleton_name == null)
- machine = StateMachineResource.sprite(db, machine_id, sm_node);
- else
- machine = StateMachineResource.mesh(db, machine_id, sm_node, skeleton_name);
- machine.add_node(sm_node);
- return machine.save(this, resource_name);
- }
- public int create_material(string directory, string name)
- {
- string resource_name = Path.build_filename(directory, name);
- Database db = new Database(this, null);
- Guid material_id = Guid.new_guid();
- MaterialResource material_resource = MaterialResource.mesh(db, material_id);
- return material_resource.save(this, resource_name);
- }
- // Returns the absolute path to the source directory.
- public string source_dir()
- {
- if (_source_dir == null)
- return "";
- else
- return _source_dir.get_path();
- }
- // Returns the absolute path to the toolchain directory.
- public string toolchain_dir()
- {
- return _toolchain_dir.get_path();
- }
- // Returns the absolute path to the data directory.
- public string data_dir()
- {
- return _data_dir.get_path();
- }
- // Returns the absolute path to the user-specific data for this project.
- public string user_dir()
- {
- return _user_dir.get_path();
- }
- public string platform()
- {
- return _platform;
- }
- public string name()
- {
- string sd = source_dir();
- return sd.substring(sd.last_index_of_char(GLib.Path.DIR_SEPARATOR) + 1);
- }
- public bool path_is_within_source_dir(string path)
- {
- GLib.File file = GLib.File.new_for_path(path);
- return file.has_prefix(_source_dir);
- }
- public void delete_garbage()
- {
- try {
- _level_editor_test_level.delete();
- _level_editor_test_package.delete();
- } catch (GLib.Error e) {
- // Ignored
- }
- }
- /// Converts the @a resource_id to its corresponding human-readable @a
- /// resource_name. It returns true if the conversion is successful, otherwise
- /// it returns false and sets @a resource_name to the value of @a resource_id.
- public bool resource_id_to_name(out string resource_name, string resource_id)
- {
- Value? name = _data_index[resource_id];
- if (name != null) {
- resource_name = (string)name;
- return true;
- }
- resource_name = resource_id;
- return false;
- }
- public Database files()
- {
- return _files;
- }
- public void add_file(string path, uint64 size, uint64 mtime)
- {
- string type = path_extension(path);
- string name = type == "" ? path : path.substring(0, path.last_index_of("."));
- Guid id = Guid.new_guid();
- _files.create(id, OBJECT_TYPE_FILE);
- _files.set_property_string(id, "path", path);
- _files.set_property_string(id, "type", type);
- _files.set_property_string(id, "name", name);
- _files.set_property_string(id, "size", size.to_string());
- _files.set_property_string(id, "mtime", mtime.to_string());
- _files.add_to_set(GUID_ZERO, "data", id);
- _map[path] = id;
- file_added(type, name, size, mtime);
- }
- public void change_file(string path, uint64 size, uint64 mtime)
- {
- string type = path_extension(path);
- string name = type == "" ? path : path.substring(0, path.last_index_of("."));
- Guid id = _map[path];
- _files.set_property_string(id, "size", size.to_string());
- _files.set_property_string(id, "mtime", mtime.to_string());
- _data_compiled = false;
- file_changed(type, name, size, mtime);
- }
- public void remove_file(string path)
- {
- if (!_map.has_key(path)) {
- logw("remove_file: map does not contain path: %s".printf(path));
- return;
- }
- Guid id = _map[path];
- file_removed(_files.get_property_string(id, "type"), _files.get_property_string(id, "name"));
- _files.remove_from_set(GUID_ZERO, "data", id);
- _files.destroy(id);
- _map.unset(path);
- }
- public void add_tree(string path)
- {
- tree_added(path);
- }
- public void remove_tree(string path)
- {
- tree_removed(path);
- }
- public string resource_filename(string absolute_path)
- {
- string prefix = _source_dir.get_path();
- if (absolute_path.has_prefix(_toolchain_dir.get_path() + "/core"))
- prefix = _toolchain_dir.get_path();
- return File.new_for_path(prefix).get_relative_path(File.new_for_path(absolute_path));
- }
- public string absolute_path(string resource_path)
- {
- string prefix = _source_dir.get_path();
- if (resource_path.has_prefix("core/") || resource_path == "core")
- prefix = _toolchain_dir.get_path();
- return Path.build_filename(prefix, resource_path);
- }
- public static void import_all_extensions(Import import_result
- , ProjectStore project_store
- , string destination_dir
- , SList<string> filenames
- , Gtk.Window? parent_window
- )
- {
- Project project = project_store._project;
- Gee.ArrayList<string> paths = new Gee.ArrayList<string>();
- foreach (var item in filenames)
- paths.add(item);
- paths.sort((a, b) => {
- int ext_a = a.last_index_of_char('.');
- int ext_b = b.last_index_of_char('.');
- return strcmp(a[ext_a : a.length], b[ext_b : b.length]);
- });
- while (paths.size != 0) {
- // Find importer for the first file in the list of selected filenames.
- ImporterData? importer = project.find_importer_for_path(paths[0]);
- if (importer == null) {
- import_result(ImportResult.ERROR);
- return;
- }
- // Create the list of all filenames importable by importer.
- Gee.ArrayList<string> importables = new Gee.ArrayList<string>();
- var cur = paths.list_iterator();
- for (var has_next = cur.next(); has_next; has_next = cur.next()) {
- string path = paths[cur.index()];
- if (importer.can_import_extension(path_extension(path))) {
- importables.add(path);
- cur.remove();
- }
- }
- // If importables is empty, filenames must have been filled with
- // un-importable filenames...
- if (importables.size == 0) {
- import_result(ImportResult.ERROR);
- return;
- }
- // Convert importables to SList<string> to be used as delegate param.
- SList<string> importables_list = new SList<string>();
- foreach (var item in importables)
- importables_list.append(item);
- importer.delegate(import_result, project_store, destination_dir, importables_list, parent_window);
- }
- }
- public class FileFilterFuncData
- {
- public string extension;
- public FileFilterFuncData(string ext)
- {
- extension = ext;
- }
- public bool handler(Gtk.FileFilterInfo info)
- {
- return info.filename.down().has_suffix("." + extension);
- }
- }
- // Returns a Gtk.FileFilter based on file @a extensions list.
- public Gtk.FileFilter create_gtk_file_filter(string name, Gee.ArrayList<string> extensions)
- {
- Gtk.FileFilter filter = new Gtk.FileFilter();
- string extensions_comma_separated = "";
- foreach (var ext in extensions) {
- extensions_comma_separated += "*.%s, ".printf(ext);
- FileFilterFuncData data = new FileFilterFuncData(ext);
- filter.add_custom(Gtk.FileFilterFlags.FILENAME, data.handler);
- }
- filter.set_filter_name(name + " (%s)".printf(extensions_comma_separated[0 : -2]));
- return filter;
- }
- public void register_importer_internal(string name, ref ImporterData data)
- {
- data._filter = create_gtk_file_filter(name, data.extensions);
- _importers.add(data);
- _importers.sort((a, b) => { return a.order < b.order ? -1 : 1; });
- // Skip duplicated extensions.
- foreach (string ext in data.extensions) {
- if (!_all_extensions_importer_data.extensions.contains(ext))
- _all_extensions_importer_data.extensions.add(ext);
- }
- _all_extensions_importer_data._filter = create_gtk_file_filter("All", _all_extensions_importer_data.extensions);
- }
- // Registers an @a importer for importing source data with the given @a
- // extensions. @a order is used to establish precedence when distinct importers
- // support similar extensions; lower values have higher precedence.
- public void register_importer(string name, string[] extensions, ImporterDelegate importer, Import import_result, double order)
- {
- ImporterData data = ImporterData();
- data.delegate = importer;
- data.extensions.add_all_array(extensions);
- data.order = order;
- data.import_result = import_result;
- register_importer_internal(name, ref data);
- }
- // Returns the preferable importer (lowest order values) which can import files
- // with the given @a extension.
- public ImporterData? find_importer_for_extension(string extension)
- {
- foreach (var imp in _importers) {
- if (imp.can_import_extension(extension))
- return imp;
- }
- return null;
- }
- public ImporterData? find_importer_for_path(string path)
- {
- return find_importer_for_extension(path_extension(path));
- }
- public bool is_type_importable(string type)
- {
- return find_importer_for_extension(type) != null;
- }
- public void import_filenames(string? destination_dir
- , GLib.SList<string> filenames
- , Import import_result
- , ImporterDelegate? importer
- , ProjectStore project_store
- , Gtk.Window? parent_window = null
- )
- {
- GLib.SList<string> _filenames = new GLib.SList<string>();
- foreach (var s in filenames)
- _filenames.append(s);
- if (destination_dir != null) {
- importer(import_result, project_store, this.absolute_path(destination_dir), filenames, parent_window);
- } else {
- Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Select destination folder..."
- , parent_window
- , Gtk.FileChooserAction.SELECT_FOLDER
- , "Cancel"
- , Gtk.ResponseType.CANCEL
- , "Select"
- , Gtk.ResponseType.ACCEPT
- );
- fcd.set_current_folder(this.source_dir());
- fcd.response.connect((response_id) => {
- if (response_id == Gtk.ResponseType.ACCEPT)
- importer(import_result, project_store, fcd.get_file().get_path(), _filenames, parent_window);
- fcd.destroy();
- });
- fcd.show_all();
- }
- }
- public void import(string? destination_dir, Import import_result, ProjectStore project_store, Gtk.Window? parent_window = null)
- {
- Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Import..."
- , parent_window
- , Gtk.FileChooserAction.OPEN
- , "Cancel"
- , Gtk.ResponseType.CANCEL
- , "Open"
- , Gtk.ResponseType.ACCEPT
- );
- fcd.select_multiple = true;
- fcd.add_filter(_all_extensions_importer_data._filter);
- fcd.set_filter(_all_extensions_importer_data._filter);
- foreach (var importer in _importers)
- fcd.add_filter(importer._filter);
- fcd.response.connect((response_id) => {
- if (response_id == Gtk.ResponseType.ACCEPT) {
- GLib.SList<string> filenames = new GLib.SList<string>();
- foreach (var f in fcd.get_files())
- filenames.append(f.get_path());
- // Find importer callback.
- unowned ImporterDelegate? importer = null;
- foreach (var imp in _importers) {
- if (imp._filter == fcd.get_filter() && imp.can_import_filenames(filenames)) {
- importer = imp.delegate;
- break;
- }
- }
- // Fallback if no importer found.
- if (importer == null)
- importer = _all_extensions_importer_data.delegate;
- import_filenames(destination_dir, filenames, import_result, importer, project_store, parent_window);
- }
- fcd.destroy();
- });
- fcd.show_all();
- }
- public void delete_tree(GLib.File file) throws Error
- {
- GLib.FileEnumerator fe = file.enumerate_children("standard::*"
- , GLib.FileQueryInfoFlags.NOFOLLOW_SYMLINKS
- );
- GLib.FileInfo info = null;
- while ((info = fe.next_file()) != null) {
- GLib.File subfile = file.resolve_relative_path(info.get_name());
- if (info.get_file_type() == GLib.FileType.DIRECTORY)
- delete_tree(subfile);
- else
- subfile.delete();
- }
- file.delete();
- }
- }
- } /* namespace Crown */
|