Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_materials.cpp
Go to the documentation of this file.
1#include "mcp_async.h"
3#include "mcp_tools_common.h"
4
13
14#include <chrono>
15#include <thread>
16
17namespace unravel::mcp
18{
19namespace
20{
21
22auto apply_result_to_json(const material_apply_result& result) -> std::string
23{
24 auto list_json = [](const std::vector<std::string>& items) -> std::string
25 {
26 std::string json = "[";
27 for(size_t i = 0; i < items.size(); ++i)
28 {
29 if(i > 0)
30 {
31 json += ",";
32 }
33 json += make_json_string(items[i]);
34 }
35 json += "]";
36 return json;
37 };
38
39 return fmt::format(R"({{"ok":{},"applied":{},"unknown":{},"errors":{}}})",
40 result.ok ? "true" : "false",
41 list_json(result.applied),
42 list_json(result.unknown),
43 list_json(result.errors));
44}
45
46} // namespace
47
49{
50 registry.add(
51 {.name = "materials_list_properties",
52 .description = "List supported PBR material property names/types for materials_set and "
53 "scene_set_model_material_instances_batch.",
54 .input_schema_json = empty_object_schema(),
55 .handler =
56 [](rtti::context&, const simdjson::dom::object&) -> tool_result
57 {
58 return {.text = list_material_property_schema_json(), .is_error = false};
59 },
60 .mutates_scene = false});
61
62 registry.add(
63 {.name = "materials_set",
64 .description =
65 "Set PBR material asset properties by key/uid. `properties` is an object of supported "
66 "keys. Saves to disk by default (inspector parity). Set save:false for in-memory only.",
67 .input_schema_json =
68 R"({"type":"object","properties":{"key":{"type":"string"},"uid":{"type":"string"},"properties":{"type":"object"},"save":{"type":"boolean","default":true},"wait_ms":{"type":"integer","minimum":0,"maximum":15000}},"required":["properties"]})",
69 .handler =
70 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
71 {
72 auto& mcp = ctx.get_cached<mcp_manager>();
73 const auto wait_ms = read_wait_ms(args, 1000);
74
75 std::string key;
76 std::string uid;
77 read_string(args, "key", key);
78 read_string(args, "uid", uid);
79
80 bool save = true;
81 read_bool(args, "save", save);
82
83 simdjson::dom::object properties;
84 if(args["properties"].get(properties))
85 {
86 return {.text = "Missing properties object", .is_error = true};
87 }
88
89 // Copy args JSON fragment by re-parsing is awkward; apply on main with captured strings
90 // and re-get properties from a serialized form.
91 const std::string props_json = std::string(simdjson::minify(args["properties"]));
92
93 auto set_result = mcp.invoke_on_main(
94 [&ctx, key, uid, save, props_json]() -> tool_result
95 {
96 std::string error;
97 auto handle = resolve_material_asset(ctx, key, uid, error);
98 if(!handle)
99 {
100 return {.text = error, .is_error = true};
101 }
102
103 auto mat = handle.get();
104 if(!mat)
105 {
106 return {.text = "Material asset not loaded", .is_error = true};
107 }
108
109 simdjson::dom::parser parser;
110 simdjson::dom::element root;
111 if(parser.parse(props_json).get(root))
112 {
113 return {.text = "Failed to parse properties", .is_error = true};
114 }
115 simdjson::dom::object props;
116 if(root.get(props))
117 {
118 return {.text = "properties must be an object", .is_error = true};
119 }
120
121 auto applied = apply_material_properties(ctx, mat, props);
122 if(!applied.ok)
123 {
124 return {.text = apply_result_to_json(applied), .is_error = true};
125 }
126
127 bool saved = false;
128 if(save)
129 {
131 {
132 return {.text = error, .is_error = true};
133 }
134 saved = true;
135 }
136
137 return {.text = fmt::format(R"({{"key":{},"uid":{},"saved":{},"result":{}}})",
139 make_json_string(hpp::to_string(handle.uid())),
140 saved ? "true" : "false",
141 apply_result_to_json(applied)),
142 .is_error = false};
143 });
144
145 if(!set_result)
146 {
147 return {.text = "Timed out setting material on main thread", .is_error = true};
148 }
149 if(!set_result->is_error && save)
150 {
151 sleep_worker(wait_ms);
152 }
153 return *set_result;
154 },
155 .mutates_scene = false,
156 .requires_main_thread = false});
157
158 registry.add(
159 {.name = "materials_get_batch",
160 .description = "Read PBR material properties for many assets. Each item: key and/or uid.",
161 .input_schema_json =
162 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"uid":{"type":"string"}}}}},"required":["items"]})json",
163 .handler =
164 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
165 {
166 simdjson::dom::array items_arr;
167 if(args["items"].get(items_arr))
168 {
169 return {.text = "Missing items array", .is_error = true};
170 }
171 std::string results = "[";
172 bool first = true;
173 size_t ok_count = 0;
174 size_t requested = 0;
175 for(auto el : items_arr)
176 {
177 ++requested;
178 simdjson::dom::object obj;
179 if(el.get(obj))
180 {
181 return {.text = "Each item must be an object", .is_error = true};
182 }
183 std::string key;
184 std::string uid;
185 read_string(obj, "key", key);
186 read_string(obj, "uid", uid);
187 std::string error;
188 auto handle = resolve_material_asset(ctx, key, uid, error);
189 if(!first)
190 {
191 results += ",";
192 }
193 first = false;
194 if(!handle)
195 {
196 results += fmt::format(R"({{"ok":false,"key":{},"uid":{},"error":{}}})",
197 make_json_string(key),
198 make_json_string(uid),
200 continue;
201 }
202 auto mat = handle.get();
203 if(!mat)
204 {
205 results += fmt::format(R"({{"ok":false,"key":{},"uid":{},"error":{}}})",
207 make_json_string(hpp::to_string(handle.uid())),
208 make_json_string("Material asset not loaded"));
209 continue;
210 }
211 ++ok_count;
212 results += fmt::format(R"({{"ok":true,"key":{},"uid":{},"properties":{}}})",
214 make_json_string(hpp::to_string(handle.uid())),
215 material_to_json(*mat));
216 }
217 results += "]";
218 return {.text = fmt::format(R"({{"results":{},"count":{},"requested":{}}})", results, ok_count, requested),
219 .is_error = ok_count == 0 && requested > 0};
220 },
221 .mutates_scene = false});
222
223 registry.add(
224 {.name = "materials_create_batch",
225 .description =
226 "Create many PBR .mat assets. Each item: path, or folder + name. Optional wait_ms after "
227 "the batch (shared).",
228 .input_schema_json =
229 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"folder":{"type":"string"},"name":{"type":"string"}}}},"wait_ms":{"type":"integer","minimum":0,"maximum":15000}},"required":["items"]})json",
230 .handler =
231 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
232 {
233 auto& mcp = ctx.get_cached<mcp_manager>();
234 const auto wait_ms = read_wait_ms(args, 1000);
235 simdjson::dom::array items_arr;
236 if(args["items"].get(items_arr))
237 {
238 return {.text = "Missing items array", .is_error = true};
239 }
240 struct item_t
241 {
242 std::string path;
243 std::string folder;
244 std::string name;
245 };
246 std::vector<item_t> items;
247 for(auto el : items_arr)
248 {
249 simdjson::dom::object obj;
250 if(el.get(obj))
251 {
252 return {.text = "Each item must be an object", .is_error = true};
253 }
254 item_t item{};
255 read_string(obj, "path", item.path);
256 read_string(obj, "folder", item.folder);
257 read_string(obj, "name", item.name);
258 items.push_back(std::move(item));
259 }
260 if(items.empty())
261 {
262 return {.text = "items array is empty", .is_error = true};
263 }
264 auto create_result = mcp.invoke_on_main(
265 [&ctx, items]() -> tool_result
266 {
267 if(ctx.has<project_manager>())
268 {
269 auto& pm = ctx.get_cached<project_manager>();
270 if(!pm.has_open_project())
271 {
272 for(const auto& item : items)
273 {
274 const auto probe = item.path.empty() ? item.folder : item.path;
275 if(probe.rfind("app:/", 0) == 0)
276 {
277 return {.text = "No project open", .is_error = true};
278 }
279 }
280 }
281 }
282 auto& am = ctx.get_cached<asset_manager>();
283 std::string results = "[";
284 bool first = true;
285 size_t ok_count = 0;
286 for(const auto& item : items)
287 {
288 if(!first)
289 {
290 results += ",";
291 }
292 first = false;
293 std::string key;
294 std::string key_error;
295 if(!item.path.empty())
296 {
297 key = normalize_material_key(item.path);
298 }
299 else if(!item.folder.empty() && !item.name.empty())
300 {
301 key = item.folder;
302 if(!key.empty() && key.back() != '/')
303 {
304 key.push_back('/');
305 }
306 key += item.name;
307 key = normalize_material_key(key);
308 }
309 else
310 {
311 key_error = "Provide path, or folder + name";
312 }
313 if(key.empty())
314 {
315 results += fmt::format(R"({{"ok":false,"error":{}}})", make_json_string(key_error));
316 continue;
317 }
318 fs::error_code ec;
319 const auto absolute = fs::absolute(fs::resolve_protocol(key));
320 if(fs::exists(absolute, ec))
321 {
322 results += fmt::format(R"({{"ok":false,"key":{},"error":{}}})",
323 make_json_string(key),
324 make_json_string("Material already exists: " + key));
325 continue;
326 }
327 fs::create_directories(absolute.parent_path(), ec);
328 auto handle = am.get_asset_from_instance<material>(key, std::make_shared<pbr_material>());
329 if(!handle)
330 {
331 results += fmt::format(R"({{"ok":false,"key":{},"error":{}}})",
332 make_json_string(key),
333 make_json_string("Failed to create material instance"));
334 continue;
335 }
336 std::string save_error;
337 if(!save_material_asset(ctx, handle, save_error))
338 {
339 results += fmt::format(R"({{"ok":false,"key":{},"error":{}}})",
340 make_json_string(key),
341 make_json_string(save_error));
342 continue;
343 }
344 ++ok_count;
345 results += fmt::format(R"({{"ok":true,"key":{},"uid":{},"saved":true}})",
347 make_json_string(hpp::to_string(handle.uid())));
348 }
349 results += "]";
350 return {.text = fmt::format(R"({{"results":{},"count":{},"requested":{}}})",
351 results,
352 ok_count,
353 items.size()),
354 .is_error = ok_count == 0};
355 });
356 if(!create_result)
357 {
358 return {.text = "Timed out creating materials on main thread", .is_error = true};
359 }
360 if(!create_result->is_error)
361 {
362 sleep_worker(wait_ms);
363 }
364 return *create_result;
365 },
366 .mutates_scene = false,
367 .requires_main_thread = false});
368
369 registry.add(
370 {.name = "scene_set_model_material_instances_batch",
371 .description =
372 "Edit per-entity runtime material instances on model slots (batch). Does NOT write .mat "
373 "files. Each item: entity_id, properties object, optional index.",
374 .input_schema_json =
375 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"index":{"type":"integer","minimum":0},"properties":{"type":"object"}},"required":["entity_id","properties"]}}},"required":["items"]})json",
376 .handler =
377 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
378 {
379 scene* scn = nullptr;
380 std::string error;
381 if(!require_edit_scene(ctx, scn, error))
382 {
383 return {.text = error, .is_error = true};
384 }
385 simdjson::dom::array items_arr;
386 if(args["items"].get(items_arr))
387 {
388 return {.text = "Missing items array", .is_error = true};
389 }
390 struct item_t
391 {
392 std::string entity_id;
393 uint32_t index{0};
394 std::string props_json;
395 };
396 std::vector<item_t> items;
397 for(auto el : items_arr)
398 {
399 simdjson::dom::object obj;
400 if(el.get(obj))
401 {
402 return {.text = "Each item must be an object", .is_error = true};
403 }
404 item_t item{};
405 if(!read_string(obj, "entity_id", item.entity_id) || item.entity_id.empty())
406 {
407 return {.text = "Item missing entity_id", .is_error = true};
408 }
409 int64_t index_i = 0;
410 if(!obj["index"].get(index_i) && index_i >= 0)
411 {
412 item.index = static_cast<uint32_t>(index_i);
413 }
414 if(obj["properties"].error())
415 {
416 return {.text = "Item missing properties object", .is_error = true};
417 }
418 item.props_json = std::string(simdjson::minify(obj["properties"]));
419 items.push_back(std::move(item));
420 }
421 if(items.empty())
422 {
423 return {.text = "items array is empty", .is_error = true};
424 }
425 std::string results = "[";
426 bool first = true;
427 size_t ok_count = 0;
428 auto& em = ctx.get_cached<editing_manager>();
429 for(const auto& item : items)
430 {
431 if(!first)
432 {
433 results += ",";
434 }
435 first = false;
436 auto entity = find_entity(*scn, item.entity_id);
437 if(!entity || !entity.all_of<model_component>())
438 {
439 results += fmt::format(R"({{"ok":false,"entity_id":{},"error":{}}})",
440 make_json_string(item.entity_id),
441 make_json_string(entity ? "Entity has no model_component"
442 : "Entity not found"));
443 continue;
444 }
445 simdjson::dom::parser parser;
446 simdjson::dom::element root;
447 if(parser.parse(item.props_json).get(root))
448 {
449 results += fmt::format(R"({{"ok":false,"entity_id":{},"error":"Failed to parse properties"}})",
450 make_json_string(item.entity_id));
451 continue;
452 }
453 simdjson::dom::object properties;
454 if(root.get(properties))
455 {
456 results += fmt::format(R"({{"ok":false,"entity_id":{},"error":"properties must be an object"}})",
457 make_json_string(item.entity_id));
458 continue;
459 }
460 auto& model_comp = entity.get<model_component>();
461 const auto old_model = model_comp.get_model();
462 auto new_model = old_model;
463 if(!new_model.get_material(item.index).is_valid())
464 {
465 new_model.set_material_instance(std::make_shared<pbr_material>(), item.index);
466 }
467 auto instance = new_model.get_or_emplace_material_instance(item.index);
468 if(!instance)
469 {
470 results += fmt::format(R"({{"ok":false,"entity_id":{},"error":"Failed to create material instance"}})",
471 make_json_string(item.entity_id));
472 continue;
473 }
474 auto applied = apply_material_properties(ctx, instance, properties);
475 if(!applied.ok)
476 {
477 results += fmt::format(R"({{"ok":false,"entity_id":{},"result":{}}})",
478 make_json_string(item.entity_id),
479 apply_result_to_json(applied));
480 continue;
481 }
482 em.do_action(
483 "MCP Set Model Material Instance",
484 [entity, new_model]()
485 {
486 if(auto* mc = entity.try_get<model_component>())
487 {
488 mc->set_model(new_model);
490 }
491 },
492 [entity, old_model]()
493 {
494 if(auto* mc = entity.try_get<model_component>())
495 {
496 mc->set_model(old_model);
498 }
499 });
500 ++ok_count;
501 results += fmt::format(R"({{"ok":true,"entity_id":{},"index":{},"result":{}}})",
503 item.index,
504 apply_result_to_json(applied));
505 }
506 results += "]";
507 return {.text = fmt::format(R"({{"results":{},"count":{},"requested":{}}})", results, ok_count, items.size()),
508 .is_error = ok_count == 0};
509 },
510 .mutates_scene = true});
511
512 registry.add(
513 {.name = "scene_clear_model_material_instances_batch",
514 .description =
515 "Clear runtime material instances on model slots (batch) so shared assets are used again.",
516 .input_schema_json =
517 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"index":{"type":"integer","minimum":0}},"required":["entity_id"]}}},"required":["items"]})json",
518 .handler =
519 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
520 {
521 scene* scn = nullptr;
522 std::string error;
523 if(!require_edit_scene(ctx, scn, error))
524 {
525 return {.text = error, .is_error = true};
526 }
527 simdjson::dom::array items_arr;
528 if(args["items"].get(items_arr))
529 {
530 return {.text = "Missing items array", .is_error = true};
531 }
532 std::string results = "[";
533 bool first = true;
534 size_t ok_count = 0;
535 size_t requested = 0;
536 auto& em = ctx.get_cached<editing_manager>();
537 for(auto el : items_arr)
538 {
539 ++requested;
540 simdjson::dom::object obj;
541 if(el.get(obj))
542 {
543 return {.text = "Each item must be an object", .is_error = true};
544 }
545 std::string entity_id;
546 if(!read_string(obj, "entity_id", entity_id) || entity_id.empty())
547 {
548 return {.text = "Item missing entity_id", .is_error = true};
549 }
550 uint32_t index = 0;
551 int64_t index_i = 0;
552 if(!obj["index"].get(index_i) && index_i >= 0)
553 {
554 index = static_cast<uint32_t>(index_i);
555 }
556 if(!first)
557 {
558 results += ",";
559 }
560 first = false;
561 auto entity = find_entity(*scn, entity_id);
562 if(!entity || !entity.all_of<model_component>())
563 {
564 results += fmt::format(R"({{"ok":false,"entity_id":{},"error":{}}})",
565 make_json_string(entity_id),
566 make_json_string(entity ? "Entity has no model_component"
567 : "Entity not found"));
568 continue;
569 }
570 auto& model_comp = entity.get<model_component>();
571 const auto old_model = model_comp.get_model();
572 auto new_model = old_model;
573 new_model.set_material_instance(nullptr, index);
574 em.do_action(
575 "MCP Clear Model Material Instance",
576 [entity, new_model]()
577 {
578 if(auto* mc = entity.try_get<model_component>())
579 {
580 mc->set_model(new_model);
582 }
583 },
584 [entity, old_model]()
585 {
586 if(auto* mc = entity.try_get<model_component>())
587 {
588 mc->set_model(old_model);
590 }
591 });
592 ++ok_count;
593 results += fmt::format(R"({{"ok":true,"entity_id":{},"index":{},"cleared":true}})",
595 index);
596 }
597 results += "]";
598 return {.text = fmt::format(R"({{"results":{},"count":{},"requested":{}}})", results, ok_count, requested),
599 .is_error = ok_count == 0 && requested > 0};
600 },
601 .mutates_scene = true});
602}
603
604} // namespace unravel::mcp
605
Manages assets, including loading, unloading, and storage.
Base class for materials used in rendering.
Definition material.h:44
Class that contains core data for meshes.
auto get_model() const -> const model &
Gets the model.
std::vector< render_pass_node_item > items
uint16_t index
std::string name
Definition hub.cpp:33
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 list_material_property_schema_json() -> std::string
void register_material_tools(mcp_tool_registry &registry)
auto sleep_worker(std::chrono::milliseconds wait_ms) -> void
auto empty_object_schema() -> std::string
auto resolve_material_asset(rtti::context &ctx, const std::string &key, const std::string &uid, std::string &error) -> asset_handle< material >
auto find_entity(scene &scn, const std::string &id) -> entt::handle
auto material_to_json(const material &mat) -> std::string
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
auto apply_material_properties(rtti::context &ctx, material::sptr mat, const simdjson::dom::object &properties) -> material_apply_result
auto read_wait_ms(const simdjson::dom::object &args, int64_t default_ms=1000) -> std::chrono::milliseconds
auto read_string(const simdjson::dom::object &args, const char *key, std::string &out) -> bool
auto save_material_asset(rtti::context &ctx, const asset_handle< material > &handle, std::string &error) -> bool
auto normalize_material_key(const std::string &path_or_key) -> std::string
entt::handle entity
auto get_cached() -> T &
Definition context.hpp:49
auto has() const -> bool
Definition context.hpp:28
static void mark_material_as_changed(entt::handle entity)
Marks material as changed in prefab override system.
Represents a scene in the ACE framework, managing entities and their relationships.
Definition scene.h:70
gfx::uniform_handle handle
Definition uniform.cpp:9