Unravel Engine C++ Reference
Loading...
Searching...
No Matches
project_manager.cpp
Go to the documentation of this file.
1#include "project_manager.h"
2#include "version/version.h"
4#include <editor/events.h>
19#include <engine/ecs/ecs.h>
20#include <engine/events.h>
27
28// must be below all
30
31#include <uuid/uuid.h>
32
33#include <filesystem/watcher.h>
34#include <graphics/graphics.h>
35#include <hpp/uuid.hpp>
36#include <logging/logging.h>
39
40namespace unravel
41{
42
43namespace
44{
45fs::path app_deploy_cfg = "app:/deploy/deploy.cfg";
46fs::path app_deploy_file = "deploy/deploy.cfg";
47fs::path app_settings_cfg = "app:/settings/settings.cfg";
48fs::path app_editor_cfg = "app:/editor/editor.cfg";
49fs::path app_project_cfg = "app:/project.cfg";
50fs::path editor_cfg = fs::persistent_path() / "unravel" / "editor.cfg";
51fs::path agents_template_path = "editor:/data/project/AGENTS.template";
52fs::path claude_template_path = "editor:/data/project/CLAUDE.template";
53
54auto seed_project_agent_files(const fs::path& project_path) -> bool
55{
56 fs::error_code err;
57 bool agents_ok = false;
58 const fs::path agents_template = fs::resolve_protocol(agents_template_path);
59 const fs::path agents_dst = project_path / "AGENTS.md";
60 if(fs::exists(agents_template, err))
61 {
62 fs::copy_file(agents_template, agents_dst, fs::copy_options::overwrite_existing, err);
63 if(err)
64 {
65 APPLOG_WARNING("Failed to seed AGENTS.md into {}: {}", project_path.string(), err.message());
66 }
67 else
68 {
69 agents_ok = true;
70 }
71 }
72 else
73 {
74 APPLOG_WARNING("Agent instructions template missing: {}", agents_template.string());
75 }
76 const fs::path claude_template = fs::resolve_protocol(claude_template_path);
77 const fs::path claude_dst = project_path / "CLAUDE.md";
78 if(fs::exists(claude_template, err))
79 {
80 fs::copy_file(claude_template, claude_dst, fs::copy_options::overwrite_existing, err);
81 if(err)
82 {
83 APPLOG_WARNING("Failed to seed CLAUDE.md into {}: {}", project_path.string(), err.message());
84 }
85 }
86 return agents_ok;
87}
88
89} // namespace
90
92{
93 auto& ui_ev = ctx.get_cached<ui_events>();
94 ui_ev.on_close_project.emit(ctx);
95
97 {
103 project_settings_ = {};
104 deploy_settings_ = {};
105 project_editor_settings_ = {};
106 project_info_ = {};
107 }
108
109 ctx.remove<settings>();
110
111 auto& em = ctx.get_cached<editing_manager>();
112 em.clear();
113
114 auto& tm = ctx.get_cached<thumbnail_manager>();
115 tm.clear_thumbnails();
116
117 auto& ec = ctx.get_cached<ecs>();
118 ec.unload_scene();
119
120 auto& scr = ctx.get_cached<script_system>();
121 scr.unload_app_domain();
122
123 auto& ui = ctx.get_cached<ui_system>();
125
126 set_name({});
127
128 auto& aw = ctx.get_cached<asset_watcher>();
129 aw.unwatch_assets(ctx, "app:/");
130}
131
132auto project_manager::open_project(rtti::context& ctx, const fs::path& project_path) -> bool
133{
134 close_project(ctx);
135
136 fs::error_code err;
137 if(!fs::exists(project_path, err))
138 {
139 APPLOG_ERROR("Project directory doesn't exist {0}", project_path.string());
140 return false;
141 }
142
143 APPLOG_TRACE("Opening project directory {0}", project_path.string());
144
145 fs::add_path_protocol("app", project_path);
146
147 {
148 fs::error_code err;
149 fs::create_directories(fs::resolve_protocol(ex::get_data_directory("app")), err);
150 fs::create_directories(fs::resolve_protocol(ex::get_compiled_directory("app")), err);
151 fs::create_directories(fs::resolve_protocol(ex::get_meta_directory("app")), err);
152 fs::create_directories(fs::resolve_protocol("app:/settings"), err);
153 fs::create_directories(fs::resolve_protocol("app:/deploy"), err);
154 fs::create_directories(fs::resolve_protocol("app:/editor"), err);
155
156 }
157
158 set_name(project_path.filename().string());
159
160 save_editor_settings();
161
162
164
165 auto& ls = ctx.get_cached<loading_screen>();
166 ls.begin_module("Opening project");
167 auto& aw = ctx.get_cached<asset_watcher>();
168 aw.watch_assets(ctx, "app:/", true, [&ls](size_t completed, size_t total, const std::string& job) -> void
169 {
170 ls.progress(completed, total, job);
171 });
172
173 auto& tm = ctx.get_cached<thumbnail_manager>();
174 tm.set_cache_directory(fs::resolve_protocol("app:/editor/.cache/"));
175
176 auto& scr = ctx.get_cached<script_system>();
177
178 ls.begin_module("Scripting");
179 scr.load_app_domain(ctx, true);
180
181 ls.begin_module("Project Info");
182 {
183 // Load or create the project signature file.
184 //
185 // * No file on disk: legacy project (or freshly created). Stamp the
186 // current engine version in both `created` and `opened` and mint a
187 // fresh GUID. This is the backward-compatibility path - opening an
188 // older project simply writes a new `project.cfg` on the spot.
189 //
190 // * File present & engine_version_opened >= current engine: the
191 // project is up to date (or from a newer engine entirely, which we
192 // don't guard against in this path). Just update `opened` to the
193 // running engine version.
194 //
195 // * File present & engine_version_opened is strictly older than the
196 // running engine (across all version components): the project was
197 // authored against an older build and is now being upgraded. We log
198 // a loud warning but do NOT abort - the UI layer is expected to have
199 // already shown a confirmation modal via `inspect_project`.
200 // Headless/CLI opens intentionally proceed so automation is not
201 // blocked by this.
202 const bool had_info_file = load_project_info();
203 const auto current = version::get_current();
204
205 if(!had_info_file)
206 {
207 project_info_.engine_version_created = current;
208 project_info_.project_guid = hpp::to_string(generate_uuid());
209 APPLOG_INFO("No project.cfg found - creating project signature "
210 "(engine {}, guid {}).",
211 current.to_string(),
212 project_info_.project_guid);
213 }
214 else if(is_from_older_engine(project_info_.engine_version_opened, current))
215 {
216 APPLOG_WARNING("Project was last opened with an older engine ({} < {}). "
217 "Proceeding - data loss is possible if the on-disk "
218 "format has diverged.",
219 project_info_.engine_version_opened.to_string(),
220 current.to_string());
221 }
222
223 project_info_.engine_version_opened = current;
224 save_project_info();
225 }
226
227 ls.begin_module("Project Settings");
228 load_project_settings();
229 save_project_settings(ctx);
230
231 ls.begin_module("Deploy Settings");
232 load_deploy_settings();
233 save_deploy_settings();
234
235 ls.begin_module("Project Editor Settings");
236 load_project_editor_settings();
237
238 ls.begin_module("Scene");
239
240 auto opened_scene = project_editor_settings_.scene.opened_scene;
241 auto startup_scene = project_settings_.standalone.startup_scene;
242
243 bool scene_loaded = false;
244 if(opened_scene)
245 {
246 scene_loaded = editor_actions::open_scene_from_asset(ctx, opened_scene);
247 }
248 if(!scene_loaded && startup_scene)
249 {
250 scene_loaded = editor_actions::open_scene_from_asset(ctx, startup_scene);
251 }
252 if(!scene_loaded)
253 {
255 }
256
257 auto& ev = ctx.get_cached<events>();
258 ev.on_project_opened.emit(ctx);
259
260 return true;
261}
262
264{
265 load_from_file(fs::resolve_protocol(app_settings_cfg).string(), project_settings_);
266}
267
269{
270 asset_writer::atomic_save_to_file(fs::resolve_protocol(app_settings_cfg).string(), project_settings_);
271
272 ctx.add<settings>(project_settings_);
273}
274
276{
277 load_from_file(fs::resolve_protocol(app_deploy_cfg).string(), deploy_settings_);
278
279 fs::error_code ec;
280 if(!fs::exists(deploy_settings_.deploy_location, ec))
281 {
282 deploy_settings_.deploy_location.clear();
283 }
284}
285
287{
288 asset_writer::atomic_save_to_file(fs::resolve_protocol(app_deploy_cfg).string(), deploy_settings_);
289}
290
292{
293 load_from_file(fs::resolve_protocol(app_editor_cfg).string(), project_editor_settings_);
294}
295
297{
298 asset_writer::atomic_save_to_file(fs::resolve_protocol(app_editor_cfg).string(), project_editor_settings_);
299}
300
302{
303 project_info_ = {};
304 return load_from_file(fs::resolve_protocol(app_project_cfg).string(), project_info_);
305}
306
308{
309 asset_writer::atomic_save_to_file(fs::resolve_protocol(app_project_cfg).string(), project_info_);
310}
311
313{
314 return project_editor_settings_;
315}
316
318{
319 return project_info_;
320}
321
323{
324 return project_info_;
325}
326
327auto project_manager::inspect_project(const fs::path& project_path) const -> project_compat_report
328{
330
331 // We cannot rely on `app:/` here because the inspected project may not be
332 // the one currently open; resolve the on-disk path directly instead.
333 const fs::path info_path = project_path / "project.cfg";
334
335 fs::error_code ec;
336 if(!fs::exists(info_path, ec) || ec)
337 {
338 report.status = project_compat::no_info_file;
339 return report;
340 }
341
342 if(!load_from_file(info_path.string(), report.on_disk))
343 {
344 // The file exists but failed to parse. Treat as legacy so we'll
345 // rewrite it on open, but log - this is unusual and worth surfacing.
346 APPLOG_WARNING("project.cfg at {} exists but could not be parsed. "
347 "It will be regenerated on open.",
348 info_path.string());
349 report.status = project_compat::no_info_file;
350 report.on_disk = {};
351 return report;
352 }
353
355 {
356 report.status = project_compat::engine_older;
357 }
358 else
359 {
360 report.status = project_compat::ok;
361 }
362 return report;
363}
364
365void project_manager::create_project(rtti::context& ctx, const fs::path& project_path)
366{
367 fs::error_code err;
368 if(fs::exists(project_path, err) && !fs::is_empty(project_path, err))
369 {
370 APPLOG_ERROR("Project directory already exists and is not empty {0}", project_path.string());
371 return;
372 }
373
374 fs::create_directories(project_path, err);
375
376 if(err)
377 {
378 APPLOG_ERROR("Failed to create project directory {0}", project_path.string());
379 return;
380 }
381
382 seed_project_agent_files(project_path);
383
384 fs::add_path_protocol("app", project_path);
385
386 open_project(ctx, project_path);
387}
388
390{
391 if(!has_open_project())
392 {
393 APPLOG_WARNING("Cannot regenerate agent files: no project is open");
394 return false;
395 }
396 const fs::path project_path = fs::resolve_protocol("app:/");
397 return seed_project_agent_files(project_path);
398}
399
400void project_manager::fixup_editor_settings_on_save()
401{
402 // fixup recent_projects
403 if(has_open_project())
404 {
405 auto& rp = editor_settings_.projects.recent_projects;
406 auto project_path = fs::resolve_protocol("app:/");
407 if(std::find_if(std::begin(rp),
408 std::end(rp),
409 [&](const auto& prj)
410 {
411 return project_path.generic_string() == prj;
412 }) == std::end(rp))
413 {
414 rp.emplace_back(std::move(project_path));
415 }
416
417 std::sort(std::begin(rp),
418 std::end(rp),
419 [](const auto& lhs_path, const auto& rhs_path)
420 {
421 fs::error_code ec;
422 auto lhs_time = fs::last_write_time(lhs_path / app_deploy_file, ec);
423 auto rhs_time = fs::last_write_time(rhs_path / app_deploy_file, ec);
424
425 return lhs_time > rhs_time;
426 });
427 }
428}
429void project_manager::fixup_editor_settings_on_load()
430{
431 fs::error_code err;
432
433 // fixup recent_projects
434 {
435 auto& items = editor_settings_.projects.recent_projects;
436 auto iter = std::begin(items);
437 while(iter != items.end())
438 {
439 auto& item = *iter;
440
441 if(!fs::exists(item, err))
442 {
443 iter = items.erase(iter);
444 }
445 else
446 {
447 ++iter;
448 }
449 }
450 }
451}
452
454{
455 fs::error_code err;
456 const fs::path config = editor_cfg;
457 if(!fs::exists(config, err))
458 {
460 }
461 else
462 {
463 APPLOG_INFO("Loading editor settings {}", config.string());
464 if(load_from_file(config.string(), editor_settings_))
465 {
466 fixup_editor_settings_on_load();
467 }
468 }
469}
470
472{
473 fixup_editor_settings_on_save();
474
475 fs::error_code err;
476 fs::create_directories(editor_cfg.parent_path(), err);
477
478 const fs::path config = editor_cfg;
479 asset_writer::atomic_save_to_file(config.string(), editor_settings_);
480}
481
482auto project_manager::get_name() const -> const std::string&
483{
484 return project_name_;
485}
486
487void project_manager::set_name(const std::string& name)
488{
489 project_name_ = name;
490}
491
493{
494 return project_settings_;
495}
496
498{
499 return deploy_settings_;
500}
501
503{
504 return editor_settings_;
505}
506
508{
509 return !get_name().empty();
510}
511
513{
515
516 auto& scripting = ctx.get_cached<script_system>();
517 scripting.set_debug_config(editor_settings_.debugger.ip,
518 editor_settings_.debugger.port,
519 editor_settings_.debugger.loglevel);
520
521 auto& ev = ctx.get_cached<events>();
522 ev.on_script_recompile.connect(sentinel_,
523 -1000,
524 [this](rtti::context& ctx, const std::string& protocol, uint64_t version)
525 {
526 if(protocol == "app" && has_open_project())
527 {
529 }
530 });
531
532 parser.set_optional<std::string>("p", "project", "", "Project folder to open.");
533
534}
535
536auto project_manager::init(rtti::context& ctx, const cmd_line::parser& parser) -> bool
537{
538 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
539
540 std::string project;
541 if(parser.try_get("project", project) && !project.empty())
542 {
543 if(project == "recent")
544 {
545 const auto& items = editor_settings_.projects.recent_projects;
546 if(!items.empty())
547 {
548 fs::path project_path = items.front();
549 return open_project(ctx, project_path);
550 }
551 }
552 else
553 {
554 fs::path project_path = project;
555 return open_project(ctx, project_path);
556 }
557 }
558
559 return true;
560}
561
563{
564 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
565
566 close_project(ctx);
567
568 return true;
569}
570
575} // namespace unravel
void set_optional(const std::string &name, const std::string &alternative, T defaultValue, const std::string &description="", bool dominant=false)
Definition parser.h:303
void watch_assets(rtti::context &ctx, const std::string &protocol, bool wait=false, const on_wait_progress_t &on_progress=nullptr)
void unwatch_assets(rtti::context &ctx, const std::string &protocol)
auto regenerate_agent_files() -> bool
auto inspect_project(const fs::path &project_path) const -> project_compat_report
auto init(rtti::context &ctx, const cmd_line::parser &parser) -> bool
auto get_project_editor_settings() -> project_editor_settings &
auto get_deploy_settings() -> deploy_settings &
auto get_project_info() -> project_info &
project_manager(rtti::context &ctx, cmd_line::parser &parser)
void set_name(const std::string &name)
auto has_open_project() const -> bool
auto get_editor_settings() -> editor_settings &
auto open_project(rtti::context &ctx, const fs::path &project_path) -> bool
auto deinit(rtti::context &ctx) -> bool
void create_project(rtti::context &ctx, const fs::path &project_path)
auto get_settings() -> settings &
void save_project_settings(rtti::context &ctx)
auto get_name() const -> const std::string &
void close_project(rtti::context &ctx)
std::vector< render_pass_node_item > items
std::string name
Definition hub.cpp:33
#define APPLOG_WARNING(...)
Definition logging.h:19
#define APPLOG_ERROR(...)
Definition logging.h:20
#define APPLOG_INFO(...)
Definition logging.h:18
#define APPLOG_TRACE(...)
Definition logging.h:17
auto get_data_directory(const std::string &prefix={}) -> std::string
auto get_compiled_directory(const std::string &prefix={}) -> std::string
auto get_meta_directory(const std::string &prefix={}) -> std::string
bool add_path_protocol(const std::string &protocol, const path &dir)
Allows us to map a protocol to a specific directory. A path protocol gives the caller the ability to ...
path resolve_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
Hash specialization for batch_key to enable use in std::unordered_map.
auto atomic_save_to_file(const fs::path &key, const asset_handle< T > &obj) -> bool
auto generate_uuid() -> hpp::uuid
Definition uuid.cpp:25
void load_from_file(const std::string &absolute_path, animation_clip &obj)
auto is_from_older_engine(const version::engine_version &on_disk, const version::engine_version &running) -> bool
auto get_current() -> engine_version
Definition version.cpp:202
auto get_cached() -> T &
Definition context.hpp:49
auto add(Args &&... args) -> T &
Definition context.hpp:16
void remove()
Definition context.hpp:78
fs::path deploy_location
Definition deploy.h:11
Manages the entity-component-system (ECS) operations for the ACE framework.
Definition ecs.h:12
void unload_scene()
Unloads the current scene.
Definition ecs.cpp:25
static auto open_scene_from_asset(rtti::context &ctx, const asset_handle< scene_prefab > &asset) -> bool
static auto new_scene(rtti::context &ctx) -> bool
static void generate_script_workspace()
std::vector< fs::path > recent_projects
Definition settings.h:35
struct unravel::editor_settings::projects_settings projects
struct unravel::editor_settings::debugger_settings debugger
hpp::event< void(rtti::context &)> on_project_opened
Definition events.h:32
hpp::event< void(rtti::context &, const std::string &protocol, uint64_t version)> on_script_recompile
Definition events.h:37
void begin_module(const std::string &module_name)
version::engine_version engine_version_opened
void set_debug_config(const std::string &address, uint32_t port, uint32_t loglevel)
auto load_app_domain(rtti::context &ctx, bool recompile) -> bool
void set_cache_directory(const fs::path &cache_dir)
Sets the directory for persistent thumbnail caching.
hpp::event< void(rtti::context &)> on_close_project
Definition events.h:17
System responsible for managing user interface components and rendering.
Definition ui_system.h:39