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.",
60 .mutates_scene =
false});
63 {.name =
"materials_set",
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.",
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"]})",
83 simdjson::dom::object properties;
84 if(args[
"properties"].get(properties))
86 return {.text =
"Missing properties object", .is_error =
true};
91 const std::string props_json = std::string(simdjson::minify(args[
"properties"]));
93 auto set_result = mcp.invoke_on_main(
100 return {.text =
error, .is_error =
true};
106 return {.text =
"Material asset not loaded", .is_error =
true};
109 simdjson::dom::parser parser;
110 simdjson::dom::element root;
111 if(parser.parse(props_json).get(root))
113 return {.text =
"Failed to parse properties", .is_error =
true};
115 simdjson::dom::object props;
118 return {.text =
"properties must be an object", .is_error =
true};
124 return {.text = apply_result_to_json(applied), .is_error =
true};
132 return {.text =
error, .is_error =
true};
137 return {.text = fmt::format(R
"({{"key":{},"uid":{},"saved":{},"result":{}}})",
140 saved ? "true" :
"false",
141 apply_result_to_json(applied)),
147 return {.text =
"Timed out setting material on main thread", .is_error =
true};
149 if(!set_result->is_error && save)
155 .mutates_scene =
false,
156 .requires_main_thread =
false});
159 {.name =
"materials_get_batch",
160 .description =
"Read PBR material properties for many assets. Each item: key and/or uid.",
162 R
"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"uid":{"type":"string"}}}}},"required":["items"]})json",
166 simdjson::dom::array items_arr;
167 if(args[
"items"].get(items_arr))
169 return {.text =
"Missing items array", .is_error =
true};
171 std::string results =
"[";
174 size_t requested = 0;
175 for(
auto el : items_arr)
178 simdjson::dom::object obj;
181 return {.text =
"Each item must be an object", .is_error =
true};
196 results += fmt::format(R
"({{"ok":false,"key":{},"uid":{},"error":{}}})",
205 results += fmt::format(R
"({{"ok":false,"key":{},"uid":{},"error":{}}})",
212 results += fmt::format(R
"({{"ok":true,"key":{},"uid":{},"properties":{}}})",
218 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})", results, ok_count, requested),
219 .is_error = ok_count == 0 && requested > 0};
221 .mutates_scene = false});
224 {.name =
"materials_create_batch",
226 "Create many PBR .mat assets. Each item: path, or folder + name. Optional wait_ms after "
227 "the batch (shared).",
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",
235 simdjson::dom::array items_arr;
236 if(args[
"items"].get(items_arr))
238 return {.text =
"Missing items array", .is_error =
true};
246 std::vector<item_t>
items;
247 for(
auto el : items_arr)
249 simdjson::dom::object obj;
252 return {.text =
"Each item must be an object", .is_error =
true};
258 items.push_back(std::move(item));
262 return {.text =
"items array is empty", .is_error =
true};
264 auto create_result = mcp.invoke_on_main(
270 if(!pm.has_open_project())
272 for(
const auto& item :
items)
274 const auto probe = item.path.empty() ? item.folder : item.path;
275 if(probe.rfind(
"app:/", 0) == 0)
277 return {.text =
"No project open", .is_error =
true};
283 std::string results =
"[";
286 for(
const auto& item :
items)
294 std::string key_error;
295 if(!item.path.empty())
299 else if(!item.folder.empty() && !item.name.empty())
302 if(!key.empty() && key.back() !=
'/')
311 key_error =
"Provide path, or folder + name";
315 results += fmt::format(R
"({{"ok":false,"error":{}}})", make_json_string(key_error));
320 if(fs::exists(absolute, ec))
322 results += fmt::format(R
"({{"ok":false,"key":{},"error":{}}})",
327 fs::create_directories(absolute.parent_path(), ec);
328 auto handle = am.get_asset_from_instance<
material>(key, std::make_shared<pbr_material>());
331 results += fmt::format(R
"({{"ok":false,"key":{},"error":{}}})",
336 std::string save_error;
339 results += fmt::format(R
"({{"ok":false,"key":{},"error":{}}})",
345 results += fmt::format(R
"({{"ok":true,"key":{},"uid":{},"saved":true}})",
350 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})",
354 .is_error = ok_count == 0};
358 return {.text =
"Timed out creating materials on main thread", .is_error =
true};
360 if(!create_result->is_error)
364 return *create_result;
366 .mutates_scene =
false,
367 .requires_main_thread =
false});
370 {.name =
"scene_set_model_material_instances_batch",
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.",
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",
379 scene* scn =
nullptr;
383 return {.text =
error, .is_error =
true};
385 simdjson::dom::array items_arr;
386 if(args[
"items"].get(items_arr))
388 return {.text =
"Missing items array", .is_error =
true};
392 std::string entity_id;
394 std::string props_json;
396 std::vector<item_t>
items;
397 for(
auto el : items_arr)
399 simdjson::dom::object obj;
402 return {.text =
"Each item must be an object", .is_error =
true};
405 if(!
read_string(obj,
"entity_id", item.entity_id) || item.entity_id.empty())
407 return {.text =
"Item missing entity_id", .is_error =
true};
410 if(!obj[
"index"].get(index_i) && index_i >= 0)
412 item.index =
static_cast<uint32_t
>(index_i);
414 if(obj[
"properties"].
error())
416 return {.text =
"Item missing properties object", .is_error =
true};
418 item.props_json = std::string(simdjson::minify(obj[
"properties"]));
419 items.push_back(std::move(item));
423 return {.text =
"items array is empty", .is_error =
true};
425 std::string results =
"[";
429 for(
const auto& item :
items)
439 results += fmt::format(R
"({{"ok":false,"entity_id":{},"error":{}}})",
442 :
"Entity not found"));
445 simdjson::dom::parser parser;
446 simdjson::dom::element root;
447 if(parser.parse(item.props_json).get(root))
449 results += fmt::format(R
"({{"ok":false,"entity_id":{},"error":"Failed to parse properties"}})",
453 simdjson::dom::object properties;
454 if(root.get(properties))
456 results += fmt::format(R
"({{"ok":false,"entity_id":{},"error":"properties must be an object"}})",
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())
465 new_model.set_material_instance(std::make_shared<pbr_material>(), item.index);
467 auto instance = new_model.get_or_emplace_material_instance(item.index);
470 results += fmt::format(R
"({{"ok":false,"entity_id":{},"error":"Failed to create material instance"}})",
477 results += fmt::format(R
"({{"ok":false,"entity_id":{},"result":{}}})",
479 apply_result_to_json(applied));
483 "MCP Set Model Material Instance",
488 mc->set_model(new_model);
496 mc->set_model(old_model);
501 results += fmt::format(R
"({{"ok":true,"entity_id":{},"index":{},"result":{}}})",
504 apply_result_to_json(applied));
507 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})", results, ok_count, items.size()),
508 .is_error = ok_count == 0};
510 .mutates_scene = true});
513 {.name =
"scene_clear_model_material_instances_batch",
515 "Clear runtime material instances on model slots (batch) so shared assets are used again.",
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",
521 scene* scn =
nullptr;
525 return {.text =
error, .is_error =
true};
527 simdjson::dom::array items_arr;
528 if(args[
"items"].get(items_arr))
530 return {.text =
"Missing items array", .is_error =
true};
532 std::string results =
"[";
535 size_t requested = 0;
537 for(
auto el : items_arr)
540 simdjson::dom::object obj;
543 return {.text =
"Each item must be an object", .is_error =
true};
545 std::string entity_id;
546 if(!
read_string(obj,
"entity_id", entity_id) || entity_id.empty())
548 return {.text =
"Item missing entity_id", .is_error =
true};
552 if(!obj[
"index"].get(index_i) && index_i >= 0)
554 index =
static_cast<uint32_t
>(index_i);
564 results += fmt::format(R
"({{"ok":false,"entity_id":{},"error":{}}})",
567 :
"Entity not found"));
571 const auto old_model = model_comp.
get_model();
572 auto new_model = old_model;
573 new_model.set_material_instance(
nullptr,
index);
575 "MCP Clear Model Material Instance",
580 mc->set_model(new_model);
588 mc->set_model(old_model);
593 results += fmt::format(R
"({{"ok":true,"entity_id":{},"index":{},"cleared":true}})",
598 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})", results, ok_count, requested),
599 .is_error = ok_count == 0 && requested > 0};
601 .mutates_scene = true});
static void mark_material_as_changed(entt::handle entity)
Marks material as changed in prefab override system.