Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_ops_batch.cpp
Go to the documentation of this file.
1#include "mcp_tools_common.h"
2
13#include <hpp/utility.hpp>
14#include <math/math.h>
15
16namespace unravel::mcp
17{
18namespace
19{
20
21auto resolve_addable_component(const std::string& name) -> entt::meta_type
22{
23 entt::meta_type found{};
24 hpp::for_each_tuple_type<all_addable_components>(
25 [&](auto index)
26 {
27 using ctype = std::tuple_element_t<decltype(index)::value, all_addable_components>;
28 auto type = entt::resolve<ctype>();
29 auto pretty = std::string(entt::get_pretty_name(type));
30 auto raw = std::string(entt::get_name(type));
31 if(pretty == name || raw == name)
32 {
33 found = type;
34 }
35 });
36 return found;
37}
38
39auto read_items_array(const simdjson::dom::object& args, simdjson::dom::array& out, std::string& error) -> bool
40{
41 if(args["items"].get(out))
42 {
43 error = "Missing items array";
44 return false;
45 }
46 return true;
47}
48
49auto item_space_is_local(const simdjson::dom::object& obj) -> bool
50{
51 std::string space;
52 if(read_string(obj, "space", space) && space == "local")
53 {
54 return true;
55 }
56 return false;
57}
58
59auto summarize_created(const std::vector<entt::handle>& created, size_t requested) -> tool_result
60{
61 std::string json = "[";
62 for(size_t i = 0; i < created.size(); ++i)
63 {
64 if(i > 0)
65 {
66 json += ",";
67 }
68 json += entity_to_summary_json(created[i], 0, 0);
69 }
70 json += "]";
71 return {.text = fmt::format(R"({{"created":{},"count":{},"requested":{}}})", json, created.size(), requested),
72 .is_error = created.empty() && requested > 0};
73}
74
75} // namespace
76
78{
79 registry.add(
80 {.name = "scene_create_entities_batch",
81 .description =
82 "Create empty entities in one undoable action. Each item: name (required), optional "
83 "parent_id, position/rotation_euler/scale, space (world default|local).",
84 .input_schema_json =
85 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"parent_id":{"type":"string"},"space":{"type":"string","enum":["world","local"]},"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"rotation_euler":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"scale":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["name"]}}},"required":["items"]})json",
86 .handler =
87 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
88 {
89 scene* scn = nullptr;
90 std::string error;
91 if(!require_edit_scene(ctx, scn, error))
92 {
93 return {.text = error, .is_error = true};
94 }
95 simdjson::dom::array items_arr;
96 if(!read_items_array(args, items_arr, error))
97 {
98 return {.text = error, .is_error = true};
99 }
100 struct item_t
101 {
102 std::string name;
103 std::string parent_id;
105 bool is_local{false};
106 };
107 std::vector<item_t> items;
108 for(auto el : items_arr)
109 {
110 simdjson::dom::object obj;
111 if(el.get(obj))
112 {
113 return {.text = "Each item must be an object", .is_error = true};
114 }
115 item_t item{};
116 if(!read_string(obj, "name", item.name) || item.name.empty())
117 {
118 return {.text = "Item missing name", .is_error = true};
119 }
120 read_string(obj, "parent_id", item.parent_id);
121 item.is_local = item_space_is_local(obj);
122 read_transform_snapshot(obj, item.pose);
123 items.push_back(std::move(item));
124 }
125 if(items.empty())
126 {
127 return {.text = "items array is empty", .is_error = true};
128 }
129 std::vector<entt::handle> created;
130 auto& em = ctx.get_cached<editing_manager>();
132 "MCP Batch Create Entities",
133 [&]()
134 {
135 created.clear();
136 created.reserve(items.size());
137 for(const auto& item : items)
138 {
139 entt::handle parent{};
140 if(!item.parent_id.empty())
141 {
142 parent = find_entity(*scn, item.parent_id);
143 }
144 auto entity = scn->create_entity(item.name, parent);
145 if(!entity)
146 {
147 continue;
148 }
149 apply_pose_direct(entity, item.pose, item.is_local);
150 created.push_back(entity);
151 }
152 return created;
153 });
154 return summarize_created(created, items.size());
155 },
156 .mutates_scene = true});
157
158 registry.add(
159 {.name = "scene_create_from_prefab_batch",
160 .description =
161 "Instantiate prefab assets in one undoable action. Each item: asset_key (required), "
162 "optional name/parent_id, position/rotation_euler/scale, space (world default|local).",
163 .input_schema_json =
164 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"asset_key":{"type":"string"},"name":{"type":"string"},"parent_id":{"type":"string"},"space":{"type":"string","enum":["world","local"]},"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"rotation_euler":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"scale":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["asset_key"]}}},"required":["items"]})json",
165 .handler =
166 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
167 {
168 scene* scn = nullptr;
169 std::string error;
170 if(!require_edit_scene(ctx, scn, error))
171 {
172 return {.text = error, .is_error = true};
173 }
174 simdjson::dom::array items_arr;
175 if(!read_items_array(args, items_arr, error))
176 {
177 return {.text = error, .is_error = true};
178 }
179 struct item_t
180 {
181 std::string asset_key;
182 std::string name;
183 std::string parent_id;
185 bool is_local{false};
186 };
187 std::vector<item_t> items;
188 for(auto el : items_arr)
189 {
190 simdjson::dom::object obj;
191 if(el.get(obj))
192 {
193 return {.text = "Each item must be an object", .is_error = true};
194 }
195 item_t item{};
196 if(!read_string(obj, "asset_key", item.asset_key) || item.asset_key.empty())
197 {
198 return {.text = "Item missing asset_key", .is_error = true};
199 }
200 read_string(obj, "name", item.name);
201 read_string(obj, "parent_id", item.parent_id);
202 item.is_local = item_space_is_local(obj);
203 read_transform_snapshot(obj, item.pose);
204 items.push_back(std::move(item));
205 }
206 if(items.empty())
207 {
208 return {.text = "items array is empty", .is_error = true};
209 }
210 std::vector<entt::handle> created;
211 auto& em = ctx.get_cached<editing_manager>();
213 "MCP Batch Create Prefabs",
214 [&]()
215 {
216 created.clear();
217 for(const auto& item : items)
218 {
219 entt::handle entity{};
220 if(item.pose.has_position && !item.is_local)
221 {
222 entity = defaults::create_prefab_at(ctx, *scn, item.asset_key, item.pose.position);
223 }
224 else
225 {
226 entity = defaults::create_prefab_at(ctx, *scn, item.asset_key);
227 }
228 if(!entity)
229 {
230 continue;
231 }
232 if(!item.name.empty())
233 {
234 if(auto* tag = entity.try_get<tag_component>())
235 {
236 tag->name = item.name;
237 }
238 }
239 if(!item.parent_id.empty())
240 {
241 auto parent = find_entity(*scn, item.parent_id);
242 if(parent)
243 {
244 entity.get<transform_component>().set_parent(parent, true);
245 }
246 }
247 apply_pose_direct(entity, item.pose, item.is_local);
248 created.push_back(entity);
249 }
250 return created;
251 });
252 return summarize_created(created, items.size());
253 },
254 .mutates_scene = true});
255
256 registry.add(
257 {.name = "scene_create_meshes_batch",
258 .description =
259 "Create mesh entities from asset keys in one undoable action. Each item: asset_key "
260 "(required), optional name/parent_id, position/rotation_euler/scale, space.",
261 .input_schema_json =
262 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"asset_key":{"type":"string"},"name":{"type":"string"},"parent_id":{"type":"string"},"space":{"type":"string","enum":["world","local"]},"position":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"rotation_euler":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"scale":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3}},"required":["asset_key"]}}},"required":["items"]})json",
263 .handler =
264 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
265 {
266 scene* scn = nullptr;
267 std::string error;
268 if(!require_edit_scene(ctx, scn, error))
269 {
270 return {.text = error, .is_error = true};
271 }
272 simdjson::dom::array items_arr;
273 if(!read_items_array(args, items_arr, error))
274 {
275 return {.text = error, .is_error = true};
276 }
277 struct item_t
278 {
279 std::string asset_key;
280 std::string name;
281 std::string parent_id;
283 bool is_local{false};
284 };
285 std::vector<item_t> items;
286 for(auto el : items_arr)
287 {
288 simdjson::dom::object obj;
289 if(el.get(obj))
290 {
291 return {.text = "Each item must be an object", .is_error = true};
292 }
293 item_t item{};
294 if(!read_string(obj, "asset_key", item.asset_key) || item.asset_key.empty())
295 {
296 return {.text = "Item missing asset_key", .is_error = true};
297 }
298 read_string(obj, "name", item.name);
299 read_string(obj, "parent_id", item.parent_id);
300 item.is_local = item_space_is_local(obj);
301 read_transform_snapshot(obj, item.pose);
302 items.push_back(std::move(item));
303 }
304 if(items.empty())
305 {
306 return {.text = "items array is empty", .is_error = true};
307 }
308 std::vector<entt::handle> created;
309 auto& em = ctx.get_cached<editing_manager>();
311 "MCP Batch Create Meshes",
312 [&]()
313 {
314 created.clear();
315 for(const auto& item : items)
316 {
317 const math::vec3 spawn =
318 (!item.is_local && item.pose.has_position) ? item.pose.position : math::vec3{0, 0, 0};
319 auto entity = defaults::create_mesh_entity_at(ctx, *scn, item.asset_key, spawn);
320 if(!entity)
321 {
322 continue;
323 }
324 if(!item.name.empty())
325 {
326 if(auto* tag = entity.try_get<tag_component>())
327 {
328 tag->name = item.name;
329 }
330 }
331 if(!item.parent_id.empty())
332 {
333 auto parent = find_entity(*scn, item.parent_id);
334 if(parent)
335 {
336 entity.get<transform_component>().set_parent(parent, true);
337 }
338 }
339 apply_pose_direct(entity, item.pose, item.is_local);
340 created.push_back(entity);
341 }
342 return created;
343 });
344 return summarize_created(created, items.size());
345 },
346 .mutates_scene = true});
347
348 registry.add(
349 {.name = "scene_set_parents_batch",
350 .description =
351 "Reparent many entities in one undoable action. Each item: entity_id, optional parent_id "
352 "(omit/empty to detach). Keeps WORLD pose.",
353 .input_schema_json =
354 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"parent_id":{"type":"string"}},"required":["entity_id"]}}},"required":["items"]})json",
355 .handler =
356 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
357 {
358 scene* scn = nullptr;
359 std::string error;
360 if(!require_edit_scene(ctx, scn, error))
361 {
362 return {.text = error, .is_error = true};
363 }
364 simdjson::dom::array items_arr;
365 if(!read_items_array(args, items_arr, error))
366 {
367 return {.text = error, .is_error = true};
368 }
369 struct entry_t
370 {
371 entt::handle entity{};
372 entt::handle old_parent{};
373 entt::handle new_parent{};
374 };
375 std::vector<entry_t> entries;
376 for(auto el : items_arr)
377 {
378 simdjson::dom::object obj;
379 if(el.get(obj))
380 {
381 return {.text = "Each item must be an object", .is_error = true};
382 }
383 std::string entity_id;
384 if(!read_string(obj, "entity_id", entity_id))
385 {
386 return {.text = "Item missing entity_id", .is_error = true};
387 }
388 auto entity = find_entity(*scn, entity_id);
389 if(!entity || !entity.all_of<transform_component>())
390 {
391 return {.text = "Entity not found or missing transform: " + entity_id, .is_error = true};
392 }
393 entry_t e{};
394 e.entity = entity;
395 e.old_parent = entity.get<transform_component>().get_parent();
396 std::string parent_id;
397 if(read_string(obj, "parent_id", parent_id) && !parent_id.empty() && parent_id != "null")
398 {
399 e.new_parent = find_entity(*scn, parent_id);
400 if(!e.new_parent)
401 {
402 return {.text = "Parent not found: " + parent_id, .is_error = true};
403 }
404 }
405 entries.push_back(e);
406 }
407 if(entries.empty())
408 {
409 return {.text = "items array is empty", .is_error = true};
410 }
411 auto& em = ctx.get_cached<editing_manager>();
412 em.do_action(
413 "MCP Batch Set Parents",
414 [entries]()
415 {
416 for(const auto& e : entries)
417 {
418 if(e.entity)
419 {
420 e.entity.get<transform_component>().set_parent(e.new_parent, true);
421 }
422 }
423 },
424 [entries]()
425 {
426 for(const auto& e : entries)
427 {
428 if(e.entity)
429 {
430 e.entity.get<transform_component>().set_parent(e.old_parent, true);
431 }
432 }
433 });
434 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
435 },
436 .mutates_scene = true});
437
438 registry.add(
439 {.name = "scene_set_names_batch",
440 .description = "Set display names on many entities in one undoable action.",
441 .input_schema_json =
442 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"name":{"type":"string"}},"required":["entity_id","name"]}}},"required":["items"]})json",
443 .handler =
444 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
445 {
446 scene* scn = nullptr;
447 std::string error;
448 if(!require_edit_scene(ctx, scn, error))
449 {
450 return {.text = error, .is_error = true};
451 }
452 simdjson::dom::array items_arr;
453 if(!read_items_array(args, items_arr, error))
454 {
455 return {.text = error, .is_error = true};
456 }
457 struct entry_t
458 {
459 entt::handle entity{};
460 std::string old_name;
461 std::string new_name;
462 };
463 std::vector<entry_t> entries;
464 for(auto el : items_arr)
465 {
466 simdjson::dom::object obj;
467 if(el.get(obj))
468 {
469 return {.text = "Each item must be an object", .is_error = true};
470 }
471 std::string entity_id;
472 std::string name;
473 if(!read_string(obj, "entity_id", entity_id) || !read_string(obj, "name", name))
474 {
475 return {.text = "Item missing entity_id or name", .is_error = true};
476 }
477 auto entity = find_entity(*scn, entity_id);
478 if(!entity || !entity.all_of<tag_component>())
479 {
480 return {.text = "Entity not found: " + entity_id, .is_error = true};
481 }
482 entries.push_back({entity, entity.get<tag_component>().name, name});
483 }
484 if(entries.empty())
485 {
486 return {.text = "items array is empty", .is_error = true};
487 }
488 auto& em = ctx.get_cached<editing_manager>();
489 em.do_action(
490 "MCP Batch Set Names",
491 [entries]()
492 {
493 for(const auto& e : entries)
494 {
495 if(auto* tag = e.entity.try_get<tag_component>())
496 {
497 tag->name = e.new_name;
498 }
499 }
500 },
501 [entries]()
502 {
503 for(const auto& e : entries)
504 {
505 if(auto* tag = e.entity.try_get<tag_component>())
506 {
507 tag->name = e.old_name;
508 }
509 }
510 });
511 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
512 },
513 .mutates_scene = true});
514
515 registry.add(
516 {.name = "scene_set_active_batch",
517 .description = "Set active flags on many entities in one undoable action.",
518 .input_schema_json =
519 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"active":{"type":"boolean"}},"required":["entity_id","active"]}}},"required":["items"]})json",
520 .handler =
521 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
522 {
523 scene* scn = nullptr;
524 std::string error;
525 if(!require_edit_scene(ctx, scn, error))
526 {
527 return {.text = error, .is_error = true};
528 }
529 simdjson::dom::array items_arr;
530 if(!read_items_array(args, items_arr, error))
531 {
532 return {.text = error, .is_error = true};
533 }
534 struct entry_t
535 {
536 entt::handle entity{};
537 bool old_active{true};
538 bool new_active{true};
539 };
540 std::vector<entry_t> entries;
541 for(auto el : items_arr)
542 {
543 simdjson::dom::object obj;
544 if(el.get(obj))
545 {
546 return {.text = "Each item must be an object", .is_error = true};
547 }
548 std::string entity_id;
549 bool active = true;
550 if(!read_string(obj, "entity_id", entity_id) || !read_bool(obj, "active", active))
551 {
552 return {.text = "Item missing entity_id or active", .is_error = true};
553 }
554 auto entity = find_entity(*scn, entity_id);
555 if(!entity || !entity.all_of<transform_component>())
556 {
557 return {.text = "Entity not found: " + entity_id, .is_error = true};
558 }
559 entries.push_back({entity, entity.get<transform_component>().is_active(), active});
560 }
561 if(entries.empty())
562 {
563 return {.text = "items array is empty", .is_error = true};
564 }
565 auto& em = ctx.get_cached<editing_manager>();
566 em.do_action(
567 "MCP Batch Set Active",
568 [entries]()
569 {
570 for(const auto& e : entries)
571 {
572 if(e.entity)
573 {
574 e.entity.get<transform_component>().set_active(e.new_active);
575 }
576 }
577 },
578 [entries]()
579 {
580 for(const auto& e : entries)
581 {
582 if(e.entity)
583 {
584 e.entity.get<transform_component>().set_active(e.old_active);
585 }
586 }
587 });
588 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
589 },
590 .mutates_scene = true});
591
592 registry.add(
593 {.name = "scene_add_components_batch",
594 .description =
595 "Add engine components to many entities in one undoable action. Use "
596 "scene_list_component_types for valid names.",
597 .input_schema_json =
598 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"component_type":{"type":"string"}},"required":["entity_id","component_type"]}}},"required":["items"]})json",
599 .handler =
600 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
601 {
602 scene* scn = nullptr;
603 std::string error;
604 if(!require_edit_scene(ctx, scn, error))
605 {
606 return {.text = error, .is_error = true};
607 }
608 simdjson::dom::array items_arr;
609 if(!read_items_array(args, items_arr, error))
610 {
611 return {.text = error, .is_error = true};
612 }
613 struct entry_t
614 {
615 entt::handle entity{};
616 entt::meta_type type{};
617 };
618 std::vector<entry_t> entries;
619 for(auto el : items_arr)
620 {
621 simdjson::dom::object obj;
622 if(el.get(obj))
623 {
624 return {.text = "Each item must be an object", .is_error = true};
625 }
626 std::string entity_id;
627 std::string component_type;
628 if(!read_string(obj, "entity_id", entity_id) || !read_string(obj, "component_type", component_type))
629 {
630 return {.text = "Item missing entity_id or component_type", .is_error = true};
631 }
632 auto entity = find_entity(*scn, entity_id);
633 if(!entity)
634 {
635 return {.text = "Entity not found: " + entity_id, .is_error = true};
636 }
637 auto type = resolve_addable_component(component_type);
638 if(!type)
639 {
640 return {.text = "Unknown or non-addable component_type: " + component_type, .is_error = true};
641 }
642 entries.push_back({entity, type});
643 }
644 if(entries.empty())
645 {
646 return {.text = "items array is empty", .is_error = true};
647 }
648 auto& em = ctx.get_cached<editing_manager>();
649 for(const auto& e : entries)
650 {
651 em.do_action<entity_add_component_action_t>("MCP Add Component", e.entity, e.type);
652 }
653 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
654 },
655 .mutates_scene = true});
656
657 registry.add(
658 {.name = "scene_remove_components_batch",
659 .description = "Remove components from many entities in one undoable action.",
660 .input_schema_json =
661 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"component_type":{"type":"string"}},"required":["entity_id","component_type"]}}},"required":["items"]})json",
662 .handler =
663 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
664 {
665 scene* scn = nullptr;
666 std::string error;
667 if(!require_edit_scene(ctx, scn, error))
668 {
669 return {.text = error, .is_error = true};
670 }
671 simdjson::dom::array items_arr;
672 if(!read_items_array(args, items_arr, error))
673 {
674 return {.text = error, .is_error = true};
675 }
676 struct entry_t
677 {
678 entt::handle entity{};
679 entt::meta_type type{};
680 };
681 std::vector<entry_t> entries;
682 for(auto el : items_arr)
683 {
684 simdjson::dom::object obj;
685 if(el.get(obj))
686 {
687 return {.text = "Each item must be an object", .is_error = true};
688 }
689 std::string entity_id;
690 std::string component_type;
691 if(!read_string(obj, "entity_id", entity_id) || !read_string(obj, "component_type", component_type))
692 {
693 return {.text = "Item missing entity_id or component_type", .is_error = true};
694 }
695 auto entity = find_entity(*scn, entity_id);
696 if(!entity)
697 {
698 return {.text = "Entity not found: " + entity_id, .is_error = true};
699 }
700 auto type = resolve_addable_component(component_type);
701 if(!type)
702 {
703 type = entt::resolve(entt::hashed_string{component_type.c_str()});
704 }
705 if(!type)
706 {
707 return {.text = "Unknown component_type: " + component_type, .is_error = true};
708 }
709 entries.push_back({entity, type});
710 }
711 if(entries.empty())
712 {
713 return {.text = "items array is empty", .is_error = true};
714 }
715 auto& em = ctx.get_cached<editing_manager>();
716 for(const auto& e : entries)
717 {
718 em.do_action<entity_remove_component_action_t>("MCP Remove Component", e.entity, e.type);
719 }
720 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", entries.size()), .is_error = false};
721 },
722 .mutates_scene = true});
723
724 registry.add(
725 {.name = "scene_add_scripts_batch",
726 .description =
727 "Add C# ScriptComponent types to many entities. type_name from scripts_list_types.",
728 .input_schema_json =
729 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"type_name":{"type":"string"}},"required":["entity_id","type_name"]}}},"required":["items"]})json",
730 .handler =
731 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
732 {
733 scene* scn = nullptr;
734 std::string error;
735 if(!require_edit_scene(ctx, scn, error))
736 {
737 return {.text = error, .is_error = true};
738 }
739 if(!ctx.has<script_system>())
740 {
741 return {.text = "Script system unavailable", .is_error = true};
742 }
743 simdjson::dom::array items_arr;
744 if(!read_items_array(args, items_arr, error))
745 {
746 return {.text = error, .is_error = true};
747 }
748 auto& script_sys = ctx.get_cached<script_system>();
749 auto& em = ctx.get_cached<editing_manager>();
750 size_t count = 0;
751 for(auto el : items_arr)
752 {
753 simdjson::dom::object obj;
754 if(el.get(obj))
755 {
756 return {.text = "Each item must be an object", .is_error = true};
757 }
758 std::string entity_id;
759 std::string type_name;
760 if(!read_string(obj, "entity_id", entity_id) || !read_string(obj, "type_name", type_name))
761 {
762 return {.text = "Item missing entity_id or type_name", .is_error = true};
763 }
764 auto entity = find_entity(*scn, entity_id);
765 if(!entity)
766 {
767 return {.text = "Entity not found: " + entity_id, .is_error = true};
768 }
769 if(!script_sys.get_type_by_fullname(type_name).valid())
770 {
771 return {.text = "Unknown script type: " + type_name, .is_error = true};
772 }
773 em.do_action<entity_add_script_component_action_t>("MCP Add Script", entity, type_name);
774 ++count;
775 }
776 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", count), .is_error = count == 0};
777 },
778 .mutates_scene = true});
779
780 registry.add(
781 {.name = "scene_remove_scripts_batch",
782 .description = "Remove C# ScriptComponent types from many entities by type_name.",
783 .input_schema_json =
784 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"type_name":{"type":"string"}},"required":["entity_id","type_name"]}}},"required":["items"]})json",
785 .handler =
786 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
787 {
788 scene* scn = nullptr;
789 std::string error;
790 if(!require_edit_scene(ctx, scn, error))
791 {
792 return {.text = error, .is_error = true};
793 }
794 simdjson::dom::array items_arr;
795 if(!read_items_array(args, items_arr, error))
796 {
797 return {.text = error, .is_error = true};
798 }
799 auto& em = ctx.get_cached<editing_manager>();
800 size_t count = 0;
801 for(auto el : items_arr)
802 {
803 simdjson::dom::object obj;
804 if(el.get(obj))
805 {
806 return {.text = "Each item must be an object", .is_error = true};
807 }
808 std::string entity_id;
809 std::string type_name;
810 if(!read_string(obj, "entity_id", entity_id) || !read_string(obj, "type_name", type_name))
811 {
812 return {.text = "Item missing entity_id or type_name", .is_error = true};
813 }
814 auto entity = find_entity(*scn, entity_id);
815 if(!entity)
816 {
817 return {.text = "Entity not found: " + entity_id, .is_error = true};
818 }
819 em.do_action<entity_remove_script_component_action_t>("MCP Remove Script", entity, type_name);
820 ++count;
821 }
822 return {.text = fmt::format(R"({{"ok":true,"count":{}}})", count), .is_error = count == 0};
823 },
824 .mutates_scene = true});
825
826 registry.add(
827 {.name = "scene_get_transforms_batch",
828 .description =
829 "Get transforms for many entities. Each item: entity_id, optional space world|local "
830 "(omit space for both, same as scene_list_entities_batch fields).",
831 .input_schema_json =
832 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"space":{"type":"string","enum":["world","local"]}},"required":["entity_id"]}}},"required":["items"]})json",
833 .handler =
834 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
835 {
836 auto& em = ctx.get_cached<editing_manager>();
837 auto* scn = em.get_active_scene(ctx);
838 if(!scn || !scn->registry)
839 {
840 return {.text = "No active scene", .is_error = true};
841 }
842 simdjson::dom::array items_arr;
843 std::string error;
844 if(!read_items_array(args, items_arr, error))
845 {
846 return {.text = error, .is_error = true};
847 }
848 std::string json = "[";
849 size_t count = 0;
850 for(auto el : items_arr)
851 {
852 simdjson::dom::object obj;
853 if(el.get(obj))
854 {
855 return {.text = "Each item must be an object", .is_error = true};
856 }
857 std::string entity_id;
858 if(!read_string(obj, "entity_id", entity_id))
859 {
860 return {.text = "Item missing entity_id", .is_error = true};
861 }
862 auto entity = find_entity(*scn, entity_id);
863 if(!entity || !entity.all_of<transform_component>())
864 {
865 return {.text = "Entity not found or missing transform: " + entity_id, .is_error = true};
866 }
867 if(count > 0)
868 {
869 json += ",";
870 }
871 std::string space;
872 read_string(obj, "space", space);
873 if(space.empty())
874 {
875 json += entity_to_summary_json(entity, 0, 0);
876 }
877 else
878 {
879 bool is_local = false;
881 {
882 return {.text = error, .is_error = true};
883 }
884 auto& t = entity.get<transform_component>();
885 const auto pos = is_local ? t.get_position_local() : t.get_position_global();
886 const auto rot = is_local ? t.get_rotation_euler_local() : t.get_rotation_euler_global();
887 const auto scl = is_local ? t.get_scale_local() : t.get_scale_global();
888 json += fmt::format(
889 R"({{"id":{},"space":{},"position":[{:.6g},{:.6g},{:.6g}],"rotation_euler":[{:.6g},{:.6g},{:.6g}],"scale":[{:.6g},{:.6g},{:.6g}]}})",
891 make_json_string(is_local ? "local" : "world"),
892 pos.x,
893 pos.y,
894 pos.z,
895 rot.x,
896 rot.y,
897 rot.z,
898 scl.x,
899 scl.y,
900 scl.z);
901 }
902 ++count;
903 }
904 json += "]";
905 return {.text = fmt::format(R"({{"transforms":{},"count":{}}})", json, count), .is_error = false};
906 },
907 .mutates_scene = false});
908
909 registry.add(
910 {.name = "scene_inspect_entities_batch",
911 .description =
912 "Inspect many entities. Provide entity_ids array, or items with entity_id. Optional "
913 "include_components (default false).",
914 .input_schema_json =
915 R"json({"type":"object","properties":{"entity_ids":{"type":"array","items":{"type":"string"}},"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"}},"required":["entity_id"]}},"include_components":{"type":"boolean"}}})json",
916 .handler =
917 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
918 {
919 bool include_components = false;
920 read_bool(args, "include_components", include_components);
921 std::vector<std::string> ids;
922 simdjson::dom::array id_arr;
923 if(!args["entity_ids"].get(id_arr))
924 {
925 for(auto el : id_arr)
926 {
927 std::string_view id_view;
928 if(el.get(id_view))
929 {
930 return {.text = "entity_ids must be strings", .is_error = true};
931 }
932 ids.emplace_back(id_view);
933 }
934 }
935 simdjson::dom::array items_arr;
936 if(!args["items"].get(items_arr))
937 {
938 for(auto el : items_arr)
939 {
940 simdjson::dom::object obj;
941 if(el.get(obj))
942 {
943 return {.text = "Each item must be an object", .is_error = true};
944 }
945 std::string entity_id;
946 if(!read_string(obj, "entity_id", entity_id))
947 {
948 return {.text = "Item missing entity_id", .is_error = true};
949 }
950 ids.push_back(entity_id);
951 }
952 }
953 if(ids.empty())
954 {
955 return {.text = "Provide entity_ids or items", .is_error = true};
956 }
957 std::string json = "[";
958 for(size_t i = 0; i < ids.size(); ++i)
959 {
960 if(i > 0)
961 {
962 json += ",";
963 }
964 std::string error;
965 auto one = editor_actions::inspect_entity(ctx, ids[i], include_components, &error);
966 if(one.empty())
967 {
968 return {.text = error.empty() ? "Inspect failed" : error, .is_error = true};
969 }
970 json += one;
971 }
972 json += "]";
973 return {.text = fmt::format(R"({{"entities":{},"count":{}}})", json, ids.size()), .is_error = false};
974 },
975 .mutates_scene = false});
976
977 registry.add(
978 {.name = "scene_list_scripts_batch",
979 .description = "List ScriptComponent instances on many entities (entity_ids required).",
980 .input_schema_json =
981 R"json({"type":"object","properties":{"entity_ids":{"type":"array","items":{"type":"string"}}},"required":["entity_ids"]})json",
982 .handler =
983 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
984 {
985 auto& em = ctx.get_cached<editing_manager>();
986 auto* scn = em.get_active_scene(ctx);
987 if(!scn || !scn->registry)
988 {
989 return {.text = "No active scene", .is_error = true};
990 }
991 simdjson::dom::array ids;
992 if(args["entity_ids"].get(ids))
993 {
994 return {.text = "Missing entity_ids", .is_error = true};
995 }
996 std::string json = "[";
997 size_t count = 0;
998 for(auto el : ids)
999 {
1000 std::string_view id_view;
1001 if(el.get(id_view))
1002 {
1003 return {.text = "entity_ids must be strings", .is_error = true};
1004 }
1005 auto entity = find_entity(*scn, std::string(id_view));
1006 if(!entity)
1007 {
1008 return {.text = "Entity not found: " + std::string(id_view), .is_error = true};
1009 }
1010 if(count > 0)
1011 {
1012 json += ",";
1013 }
1014 std::string scripts = "[";
1015 bool first_script = true;
1016 if(auto* sc = entity.try_get<script_component>())
1017 {
1018 for(const auto& obj : sc->get_script_components())
1019 {
1020 if(!obj.pinned)
1021 {
1022 continue;
1023 }
1024 if(!first_script)
1025 {
1026 scripts += ",";
1027 }
1028 first_script = false;
1029 const auto type_name = obj.pinned->get_object().get_type().get_fullname();
1030 const auto source = sc->get_script_source_location(obj);
1031 scripts += fmt::format(R"({{"type":{},"source_path":{}}})",
1032 make_json_string(type_name),
1033 make_json_string(source));
1034 }
1035 }
1036 scripts += "]";
1037 json += fmt::format(R"({{"entity_id":{},"scripts":{}}})",
1039 scripts);
1040 ++count;
1041 }
1042 json += "]";
1043 return {.text = fmt::format(R"({{"entities":{},"count":{}}})", json, count), .is_error = false};
1044 },
1045 .mutates_scene = false});
1046}
1047
1048} // namespace unravel::mcp
Class that contains core data for audio listeners. There can only be one instance of it per scene.
Component that handles transformations (position, rotation, scale, etc.) in the ACE framework.
std::vector< render_pass_node_item > items
uint16_t index
std::vector< render_pass_entry > entries
std::string name
Definition hub.cpp:33
std::string tag
Definition hub.cpp:32
std::string error
Definition mcp_async.cpp:33
std::string parent_id
bool is_local
transform_snapshot pose
texture_job_type type
auto get_pretty_name(const meta_type &t) -> std::string
auto get_name(const meta_type &t) -> std::string
auto find_entity(scene &scn, const std::string &id) -> entt::handle
auto require_edit_scene(rtti::context &ctx, scene *&out_scene, std::string &error) -> bool
auto read_bool(const simdjson::dom::object &args, const char *key, bool &out) -> bool
auto make_json_string(const std::string &value) -> std::string
auto entity_id_string(entt::handle entity) -> std::string
void apply_pose_direct(entt::handle entity, const transform_snapshot &pose, bool is_local)
auto read_transform_space(const simdjson::dom::object &args, bool &out_is_local, std::string &error) -> bool
Parse transform space: "world" (default) or "local". Returns false on invalid values.
auto read_string(const simdjson::dom::object &args, const char *key, std::string &out) -> bool
auto read_transform_snapshot(const simdjson::dom::object &obj, transform_snapshot &out) -> void
void register_ops_batch_tools(mcp_tool_registry &registry)
std::tuple< test_component, model_component, animation_component, camera_component, volume_component, auto_exposure_component, tonemapping_component, assao_component, bloom_component, fxaa_component, taa_component, ssr_component, ssil_component, light_component, skylight_component, reflection_probe_component, physics_component, character_controller_component, audio_source_component, audio_listener_component, text_component, particle_emitter_component, ui_document_component > all_addable_components
auto entity_to_summary_json(entt::handle entity, int depth, int max_depth) -> std::string
JSON summary: transform, component pretty-names, optional children.
entt::handle entity
auto get_cached() -> T &
Definition context.hpp:49
auto has() const -> bool
Definition context.hpp:28
Undoable create for entity subtrees: invokes a user-supplied factory on first execution,...
static auto create_mesh_entity_at(rtti::context &ctx, scene &scn, const std::string &key, const camera &cam, math::vec2 pos, bool align_to_surface=false) -> entt::handle
Creates a mesh entity at a specified position.
Definition defaults.cpp:897
static auto create_prefab_at(rtti::context &ctx, scene &scn, const std::string &key, const camera &cam, math::vec2 pos, bool align_to_surface=false) -> entt::handle
Creates a prefab entity at a specified position.
Definition defaults.cpp:842
auto get_active_scene(rtti::context &ctx) -> scene *
void do_action(const std::string &name, const std::function< void()> &action)
static auto inspect_entity(rtti::context &ctx, const std::string &entity_id, bool include_components, std::string *error=nullptr) -> std::string
Represents a scene in the ACE framework, managing entities and their relationships.
Definition scene.h:70
auto create_entity(const std::string &tag={}, entt::handle parent={}) -> entt::handle
Creates an entity in the scene with an optional tag and parent.
Definition scene.cpp:360
Component that provides a tag (name or label) for an entity.