Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_projects.cpp
Go to the documentation of this file.
1#include "mcp_tools_common.h"
2
8#include <engine/ecs/ecs.h>
10
11#include <chrono>
12
13namespace unravel::mcp
14{
15namespace
16{
17
18auto compat_to_string(project_manager::project_compat status) -> const char*
19{
20 switch(status)
21 {
23 return "no_info_file";
25 return "ok";
27 return "engine_older";
28 }
29 return "ok";
30}
31
32auto project_info_json(project_manager& pm) -> std::string
33{
34 if(!pm.has_open_project())
35 {
36 return R"({"open":false})";
37 }
38 const auto& info = pm.get_project_info();
39 return fmt::format(R"({{"open":true,"name":{},"path":{},"guid":{}}})",
40 make_json_string(pm.get_name()),
41 make_json_string(fs::resolve_protocol("app:/").generic_string()),
42 make_json_string(info.project_guid));
43}
44
45} // namespace
46
48{
49 registry.add(
50 {.name = "project_list_recent",
51 .description =
52 "List recent project directories from editor.cfg (absolute paths), with on-disk "
53 "existence and inspect_project compatibility status.",
54 .input_schema_json = empty_object_schema(),
55 .handler =
56 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
57 {
58 if(!ctx.has<project_manager>())
59 {
60 return {.text = "[]", .is_error = false};
61 }
62
63 auto& pm = ctx.get_cached<project_manager>();
64 const auto& items = pm.get_editor_settings().projects.recent_projects;
65
66 std::string json = "[";
67 for(size_t i = 0; i < items.size(); ++i)
68 {
69 if(i > 0)
70 {
71 json += ",";
72 }
73
74 fs::error_code ec;
75 const auto& path = items[i];
76 const bool exists = fs::exists(path, ec);
77 const auto report = pm.inspect_project(path);
78 json += fmt::format(
79 R"({{"index":{},"path":{},"exists":{},"compat":{},"guid":{},"engine_version_opened":{}}})",
80 i,
81 make_json_string(path.generic_string()),
82 exists ? "true" : "false",
83 make_json_string(compat_to_string(report.status)),
84 make_json_string(report.on_disk.project_guid),
85 make_json_string(report.on_disk.engine_version_opened.to_string()));
86 }
87 json += "]";
88 return {.text = json, .is_error = false};
89 },
90 .mutates_scene = false});
91
92 registry.add(
93 {.name = "project_open",
94 .description =
95 "Open a project by absolute path, recent:true (most recent), or recent_index. "
96 "Bypasses UI confirmations. If open_project would show the create-scene modal "
97 "(no last/startup scene), completes it with preset (default medium). "
98 "May take a while while assets are watched.",
99 .input_schema_json =
100 R"json({"type":"object","properties":{"path":{"type":"string","description":"Absolute project directory, or 'recent'"},"recent":{"type":"boolean"},"recent_index":{"type":"integer","minimum":0},"preset":{"type":"string","enum":["low","medium","high","showcase"],"description":"Used only when a new scene modal would appear; default medium"},"force":{"type":"boolean","description":"Discard unsaved scene changes before open; default true"}}})json",
101 .handler =
102 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
103 {
104 auto& mcp = ctx.get_cached<mcp_manager>();
105
106 std::string path_arg;
107 read_string(args, "path", path_arg);
108 bool recent = false;
109 read_bool(args, "recent", recent);
110 int64_t recent_index = -1;
111 if(args["recent_index"].get(recent_index))
112 {
113 recent_index = -1;
114 }
115 std::string preset_str = "medium";
116 read_string(args, "preset", preset_str);
117 bool force = true;
118 read_bool(args, "force", force);
119
120 auto result = mcp.invoke_on_main(
121 [&ctx, path_arg, recent, recent_index, preset_str, force]() -> tool_result
122 {
123 std::string error;
125 {
126 return {.text = error, .is_error = true};
127 }
128 if(!ctx.has<project_manager>())
129 {
130 return {.text = "project_manager unavailable", .is_error = true};
131 }
132
134 if(!defaults::parse_scene_preset(preset_str, preset))
135 {
136 return {.text = "Invalid preset (use low|medium|high|showcase)", .is_error = true};
137 }
138
139 auto& pm = ctx.get_cached<project_manager>();
140 auto& em = ctx.get_cached<editing_manager>();
141
142 if(em.has_unsaved_changes() && !force)
143 {
144 return {.text = "Unsaved scene changes; pass force:true to discard", .is_error = true};
145 }
146 if(force)
147 {
148 em.clear_unsaved_changes();
149 }
150
151 // Rebuild args object fields into a path resolution using a temp object isn't
152 // available; resolve manually from captured values.
153 fs::path project_path;
154 const auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
155 if(recent || path_arg == "recent")
156 {
157 if(recent_projects.empty())
158 {
159 return {.text = "No recent projects", .is_error = true};
160 }
161 project_path = recent_projects.front();
162 }
163 else if(recent_index >= 0)
164 {
165 if(static_cast<size_t>(recent_index) >= recent_projects.size())
166 {
167 return {.text = "recent_index out of range", .is_error = true};
168 }
169 project_path = recent_projects[static_cast<size_t>(recent_index)];
170 }
171 else if(!path_arg.empty())
172 {
173 project_path = fs::path(path_arg);
174 }
175 else
176 {
177 return {.text = "Provide path, recent:true, or recent_index", .is_error = true};
178 }
179
180 fs::error_code ec;
181 if(!fs::exists(project_path, ec))
182 {
183 return {.text = "Project directory does not exist: " + project_path.generic_string(),
184 .is_error = true};
185 }
186
187 const auto report = pm.inspect_project(project_path);
188 if(!pm.open_project(ctx, project_path))
189 {
190 return {.text = "Failed to open project: " + project_path.generic_string(), .is_error = true};
191 }
192
193 bool created_from_preset = false;
195 {
196 created_from_preset = true;
197 }
198
199 return {.text = fmt::format(
200 R"({{"ok":true,"project":{},"compat":{},"created_scene_from_preset":{},"preset":{}}})",
201 project_info_json(pm),
202 make_json_string(compat_to_string(report.status)),
203 created_from_preset ? "true" : "false",
204 make_json_string(std::string(preset_str.empty() ? "medium" : preset_str))),
205 .is_error = false};
206 },
207 std::chrono::milliseconds(120000));
208
209 if(!result)
210 {
211 return {.text = "Timed out opening project on main thread", .is_error = true};
212 }
213 return *result;
214 },
215 .mutates_scene = true,
216 .requires_main_thread = false});
217
218 registry.add(
219 {.name = "project_close",
220 .description =
221 "Close the current project (no ImGui save prompt). Pass force:true (default) to "
222 "discard unsaved scene changes.",
223 .input_schema_json =
224 R"({"type":"object","properties":{"force":{"type":"boolean","default":true}}})",
225 .handler =
226 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
227 {
228 std::string error;
230 {
231 return {.text = error, .is_error = true};
232 }
233 if(!ctx.has<project_manager>())
234 {
235 return {.text = "project_manager unavailable", .is_error = true};
236 }
237
238 auto& pm = ctx.get_cached<project_manager>();
239 if(!pm.has_open_project())
240 {
241 return {.text = R"({"ok":true,"was_open":false})", .is_error = false};
242 }
243
244 bool force = true;
245 read_bool(args, "force", force);
246
247 auto& em = ctx.get_cached<editing_manager>();
248 if(em.has_unsaved_changes() && !force)
249 {
250 return {.text = "Unsaved scene changes; pass force:true to discard", .is_error = true};
251 }
252 if(force)
253 {
254 em.clear_unsaved_changes();
255 }
256
258 pm.close_project(ctx);
259 return {.text = R"({"ok":true,"was_open":true})", .is_error = false};
260 },
261 .mutates_scene = true});
262}
263
264} // namespace unravel::mcp
static auto complete_if_pending(defaults::scene_preset preset) -> bool
static void cancel_if_pending()
Dismiss a pending create-scene modal without invoking the callback.
auto get_editor_settings() -> editor_settings &
defaults::scene_preset preset
std::vector< render_pass_node_item > items
std::string error
Definition mcp_async.cpp:33
path resolve_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
auto require_not_play_mode(rtti::context &ctx, std::string &error) -> bool
auto empty_object_schema() -> std::string
auto read_bool(const simdjson::dom::object &args, const char *key, bool &out) -> bool
auto make_json_string(const std::string &value) -> std::string
void register_project_tools(mcp_tool_registry &registry)
auto read_string(const simdjson::dom::object &args, const char *key, std::string &out) -> bool
auto get_cached() -> T &
Definition context.hpp:49
auto has() const -> bool
Definition context.hpp:28
static auto parse_scene_preset(hpp::string_view value, scene_preset &out) -> bool
Parse a preset name (low|medium|high|showcase; aliases: standard/default -> medium)....
scene_preset
Quality presets for new scene creation (low = less expensive, high = more expensive).
Definition defaults.h:215