Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_editor.cpp
Go to the documentation of this file.
1#include "mcp_tools_common.h"
2
4
5#include <algorithm>
6#include <logging/logging.h>
7
8namespace unravel::mcp
9{
10namespace
11{
12
13auto level_to_string(level::level_enum level) -> const char*
14{
15 switch(level)
16 {
17 case level::trace:
18 return "trace";
19 case level::debug:
20 return "debug";
21 case level::info:
22 return "info";
23 case level::warn:
24 return "warn";
25 case level::err:
26 return "error";
27 case level::critical:
28 return "critical";
29 default:
30 return "off";
31 }
32}
33
34auto parse_min_level(const std::string& name, level::level_enum& out) -> bool
35{
36 if(name.empty() || name == "trace")
37 {
38 out = level::trace;
39 return true;
40 }
41 if(name == "debug")
42 {
43 out = level::debug;
44 return true;
45 }
46 if(name == "info")
47 {
48 out = level::info;
49 return true;
50 }
51 if(name == "warn" || name == "warning")
52 {
53 out = level::warn;
54 return true;
55 }
56 if(name == "error" || name == "err")
57 {
58 out = level::err;
59 return true;
60 }
61 if(name == "critical")
62 {
63 out = level::critical;
64 return true;
65 }
66 return false;
67}
68
69auto play_state_to_json(const play_state_info& info) -> std::string
70{
71 return fmt::format(
72 R"({{"phase":{},"is_active":{},"is_paused":{},"is_splash":{},"is_simulation_running":{},"frames_running":{}}})",
73 make_json_string(info.phase),
74 info.is_active ? "true" : "false",
75 info.is_paused ? "true" : "false",
76 info.is_splash ? "true" : "false",
77 info.is_simulation_running ? "true" : "false",
78 info.frames_running);
79}
80
81} // namespace
82
84{
85 registry.add(
86 {.name = "play_get_state",
87 .description = "Get editor play mode state: phase, active, paused, splash, frames_running.",
88 .input_schema_json = empty_object_schema(),
89 .handler =
90 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
91 {
92 return {.text = play_state_to_json(editor_actions::get_play_state(ctx)), .is_error = false};
93 },
94 .mutates_scene = false});
95
96 registry.add(
97 {.name = "play_set_active",
98 .description =
99 "Enter or exit play mode. Blocked when inactive and scripts have compile errors. "
100 "Optional allow_splash (default true).",
101 .input_schema_json =
102 R"({"type":"object","properties":{"active":{"type":"boolean"},"allow_splash":{"type":"boolean"}},"required":["active"]})",
103 .handler =
104 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
105 {
106 bool active = false;
107 if(!read_bool(args, "active", active))
108 {
109 return {.text = "Missing active", .is_error = true};
110 }
111 bool allow_splash = true;
112 read_bool(args, "allow_splash", allow_splash);
113 std::string error;
114 if(!editor_actions::set_play_active(ctx, active, allow_splash, &error))
115 {
116 return {.text = error, .is_error = true};
117 }
118 return {.text = play_state_to_json(editor_actions::get_play_state(ctx)), .is_error = false};
119 },
120 .mutates_scene = false});
121
122 registry.add(
123 {.name = "play_set_paused",
124 .description = "Pause or resume simulation while play mode is active.",
125 .input_schema_json =
126 R"({"type":"object","properties":{"paused":{"type":"boolean"}},"required":["paused"]})",
127 .handler =
128 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
129 {
130 bool paused = false;
131 if(!read_bool(args, "paused", paused))
132 {
133 return {.text = "Missing paused", .is_error = true};
134 }
135 std::string error;
137 {
138 return {.text = error, .is_error = true};
139 }
140 return {.text = play_state_to_json(editor_actions::get_play_state(ctx)), .is_error = false};
141 },
142 .mutates_scene = false});
143
144 registry.add(
145 {.name = "play_skip_frame",
146 .description = "Advance one simulation frame while play mode is active and paused.",
147 .input_schema_json = empty_object_schema(),
148 .handler =
149 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
150 {
151 std::string error;
153 {
154 return {.text = error, .is_error = true};
155 }
156 return {.text = play_state_to_json(editor_actions::get_play_state(ctx)), .is_error = false};
157 },
158 .mutates_scene = false});
159
160 registry.add(
161 {.name = "selection_get",
162 .description = "Get the active entity selection (active_entity_id + entity_ids).",
163 .input_schema_json = empty_object_schema(),
164 .handler =
165 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
166 {
167 const auto sel = editor_actions::get_selection(ctx);
168 std::string ids = "[";
169 for(size_t i = 0; i < sel.entity_ids.size(); ++i)
170 {
171 if(i > 0)
172 {
173 ids += ",";
174 }
175 ids += make_json_string(sel.entity_ids[i]);
176 }
177 ids += "]";
178 return {.text = fmt::format(R"({{"active_entity_id":{},"entity_ids":{}}})",
179 sel.active_entity_id.empty() ? "null"
180 : make_json_string(sel.active_entity_id),
181 ids),
182 .is_error = false};
183 },
184 .mutates_scene = false});
185
186 registry.add(
187 {.name = "selection_set_batch",
188 .description =
189 "Set editor entity selection by UUID (or integral) ids. "
190 "mode=normal (default) replaces selection; mode=add appends (ctrl-select).",
191 .input_schema_json =
192 R"({"type":"object","properties":{"entity_ids":{"type":"array","items":{"type":"string"}},"mode":{"type":"string","enum":["normal","add"]}},"required":["entity_ids"]})",
193 .handler =
194 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
195 {
196 simdjson::dom::array arr;
197 if(args["entity_ids"].get(arr))
198 {
199 return {.text = "Missing entity_ids", .is_error = true};
200 }
201 std::vector<std::string> ids;
202 for(auto el : arr)
203 {
204 std::string_view view;
205 if(el.get(view))
206 {
207 return {.text = "entity_ids must be strings", .is_error = true};
208 }
209 ids.emplace_back(view);
210 }
211 bool add = false;
212 std::string mode;
213 if(read_string(args, "mode", mode) && mode == "add")
214 {
215 add = true;
216 }
217 std::string error;
218 if(!editor_actions::set_selection(ctx, ids, add, &error))
219 {
220 return {.text = error, .is_error = true};
221 }
222 const auto sel = editor_actions::get_selection(ctx);
223 std::string out_ids = "[";
224 for(size_t i = 0; i < sel.entity_ids.size(); ++i)
225 {
226 if(i > 0)
227 {
228 out_ids += ",";
229 }
230 out_ids += make_json_string(sel.entity_ids[i]);
231 }
232 out_ids += "]";
233 return {.text = fmt::format(R"({{"active_entity_id":{},"entity_ids":{}}})",
234 sel.active_entity_id.empty() ? "null"
235 : make_json_string(sel.active_entity_id),
236 out_ids),
237 .is_error = false};
238 },
239 .mutates_scene = false});
240
241 registry.add(
242 {.name = "selection_clear",
243 .description = "Clear the editor selection.",
244 .input_schema_json = empty_object_schema(),
245 .handler =
246 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
247 {
249 return {.text = R"({"cleared":true})", .is_error = false};
250 },
251 .mutates_scene = false});
252
253 registry.add(
254 {.name = "window_request_focus",
255 .description =
256 "Focus and raise the editor OS main window (os::window::request_focus). Call before "
257 "asset create/reimport when the editor is unfocused so filesystem watcher events can "
258 "fire (materials, folders, etc.). Restores the window if minimized.",
259 .input_schema_json = empty_object_schema(),
260 .handler =
261 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
262 {
263 std::string error;
265 {
266 return {.text = error, .is_error = true};
267 }
268 return {.text = R"({"ok":true,"requested":true})", .is_error = false};
269 },
270 .mutates_scene = false});
271
272 registry.add(
273 {.name = "panel_focus_scene",
274 .description = "Focus the Scene panel (editor viewport tab) so it becomes the active ImGui window.",
275 .input_schema_json = empty_object_schema(),
276 .handler =
277 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
278 {
279 std::string error;
281 {
282 return {.text = error, .is_error = true};
283 }
284 return {.text = R"({"panel":"scene","focused":true})", .is_error = false};
285 },
286 .mutates_scene = false});
287
288 registry.add(
289 {.name = "panel_focus_game",
290 .description = "Focus the Game panel (play view tab) so it becomes the active ImGui window.",
291 .input_schema_json = empty_object_schema(),
292 .handler =
293 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
294 {
295 std::string error;
297 {
298 return {.text = error, .is_error = true};
299 }
300 return {.text = R"({"panel":"game","focused":true})", .is_error = false};
301 },
302 .mutates_scene = false});
303
304 registry.add(
305 {.name = "logs_get_recent",
306 .description =
307 "Get recent console log entries. Optional min_level (trace|debug|info|warn|error|critical), "
308 "max_count (default 50), after_id (exclusive cursor).",
309 .input_schema_json =
310 R"({"type":"object","properties":{"min_level":{"type":"string"},"max_count":{"type":"integer","minimum":1,"maximum":1024},"after_id":{"type":"integer","minimum":0}}})",
311 .handler =
312 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
313 {
314 level::level_enum min_level = level::info;
315 std::string level_name;
316 if(read_string(args, "min_level", level_name) && !parse_min_level(level_name, min_level))
317 {
318 return {.text = "Invalid min_level", .is_error = true};
319 }
320 double max_count_d = 50.0;
321 size_t max_count = 50;
322 if(read_double(args, "max_count", max_count_d))
323 {
324 max_count = static_cast<size_t>(std::clamp(max_count_d, 1.0, 1024.0));
325 }
326 double after_d = 0.0;
327 uint64_t after_id = 0;
328 if(read_double(args, "after_id", after_d) && after_d >= 0.0)
329 {
330 after_id = static_cast<uint64_t>(after_d);
331 }
332 const auto logs = editor_actions::get_recent_logs(ctx, min_level, max_count, after_id);
333 std::string json = "[";
334 for(size_t i = 0; i < logs.size(); ++i)
335 {
336 if(i > 0)
337 {
338 json += ",";
339 }
340 const auto& e = logs[i];
341 json += fmt::format(R"({{"id":{},"level":{},"text":{},"filename":{},"funcname":{},"line":{}}})",
342 e.id,
343 make_json_string(level_to_string(e.level)),
344 make_json_string(e.text),
345 make_json_string(e.filename),
346 make_json_string(e.funcname),
347 e.line);
348 }
349 json += "]";
350 return {.text = std::move(json), .is_error = false};
351 },
352 .mutates_scene = false});
353
354 registry.add(
355 {.name = "edit_undo",
356 .description = "Undo the last undoable editor action (same as Ctrl+Z).",
357 .input_schema_json = empty_object_schema(),
358 .handler =
359 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
360 {
361 std::string error;
363 {
364 return {.text = error, .is_error = true};
365 }
366 auto& em = ctx.get_cached<editing_manager>();
367 if(!em.can_undo())
368 {
369 return {.text = R"({"ok":false,"undone":false,"reason":"nothing to undo"})", .is_error = false};
370 }
371 auto action = em.undo();
372 const auto name = action ? action->name : std::string{};
373 return {.text = fmt::format(R"({{"ok":true,"undone":true,"action":{}}})", make_json_string(name)),
374 .is_error = false};
375 },
376 .mutates_scene = true});
377
378 registry.add(
379 {.name = "edit_redo",
380 .description = "Redo the last undone editor action (same as Ctrl+Y).",
381 .input_schema_json = empty_object_schema(),
382 .handler =
383 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
384 {
385 std::string error;
387 {
388 return {.text = error, .is_error = true};
389 }
390 auto& em = ctx.get_cached<editing_manager>();
391 if(!em.can_redo())
392 {
393 return {.text = R"({"ok":false,"redone":false,"reason":"nothing to redo"})", .is_error = false};
394 }
395 auto action = em.redo();
396 const auto name = action ? action->name : std::string{};
397 return {.text = fmt::format(R"({{"ok":true,"redone":true,"action":{}}})", make_json_string(name)),
398 .is_error = false};
399 },
400 .mutates_scene = true});
401}
402
403} // namespace unravel::mcp
uint16_t view
std::string name
Definition hub.cpp:33
std::string error
Definition mcp_async.cpp:33
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 read_double(const simdjson::dom::object &args, const char *key, double &out) -> bool
auto make_json_string(const std::string &value) -> std::string
auto read_string(const simdjson::dom::object &args, const char *key, std::string &out) -> bool
void register_editor_tools(mcp_tool_registry &registry)
auto get_cached() -> T &
Definition context.hpp:49
auto undo() -> std::shared_ptr< editing_action_t >
auto redo() -> std::shared_ptr< editing_action_t >
static auto skip_play_frame(rtti::context &ctx, std::string *error=nullptr) -> bool
static auto request_main_window_focus(rtti::context &ctx, std::string *error=nullptr) -> bool
Focus/raise the OS main window so asset watcher and similar focus-gated work can run (e....
static auto set_play_active(rtti::context &ctx, bool active, bool allow_splash=true, std::string *error=nullptr) -> bool
static auto get_play_state(rtti::context &ctx) -> play_state_info
static void clear_selection(rtti::context &ctx)
static auto focus_scene_panel(rtti::context &ctx, std::string *error=nullptr) -> bool
static auto set_selection(rtti::context &ctx, const std::vector< std::string > &entity_ids, bool add=false, std::string *error=nullptr) -> bool
static auto get_recent_logs(rtti::context &ctx, level::level_enum min_level, size_t max_count, uint64_t after_id=0) -> std::vector< log_query_entry >
static auto focus_game_panel(rtti::context &ctx, std::string *error=nullptr) -> bool
static auto set_play_paused(rtti::context &ctx, bool paused, std::string *error=nullptr) -> bool
static auto get_selection(rtti::context &ctx) -> selection_info