27auto asset_entry_json(
const std::string& uid,
const std::string& location,
const std::string&
type) -> std::string
29 return fmt::format(R
"({{"uid":{},"location":{},"type":{}}})",
35auto ends_with_ci(std::string_view value, std::string_view suffix) ->
bool
37 if(suffix.size() > value.size())
41 const auto v = value.substr(value.size() - suffix.size());
45auto path_matches_type(
const std::string& location,
const std::string& type_filter) ->
bool
47 if(type_filter.empty())
53 if(ends_with_ci(lower_loc, lower_type))
57 if(!lower_type.empty() && lower_type.front() ==
'.')
59 return ends_with_ci(lower_loc, lower_type.substr(1));
64auto normalize_folder_key(std::string key) -> std::string
71 const fs::path as_path(key);
72 if(as_path.is_absolute())
76 while(!
key.empty() && (
key.back() ==
'/' ||
key.back() ==
'\\'))
84auto matches_asset_extension(
const std::string& ext) ->
bool
99auto poll_handle_ready(asset_manager& am,
const std::string& key, std::chrono::milliseconds timeout) ->
bool
102 const auto deadline = std::chrono::steady_clock::now() + timeout;
103 while(std::chrono::steady_clock::now() < deadline)
109 std::this_thread::sleep_for(std::chrono::milliseconds(32));
114auto try_wait_asset_ready(
rtti::context& ctx,
const std::string& key, std::chrono::milliseconds timeout) ->
bool
116 auto& am = ctx.get_cached<asset_manager>();
117 const auto ext = fs::path(key).extension().generic_string();
118 if(matches_asset_extension<material>(ext))
120 return poll_handle_ready<material>(am, key, timeout);
122 if(matches_asset_extension<mesh>(ext))
124 return poll_handle_ready<mesh>(am, key, timeout);
126 if(matches_asset_extension<prefab>(ext))
128 return poll_handle_ready<prefab>(am, key, timeout);
130 if(matches_asset_extension<scene_prefab>(ext))
132 return poll_handle_ready<scene_prefab>(am, key, timeout);
134 const auto deadline = std::chrono::steady_clock::now() + timeout;
135 while(std::chrono::steady_clock::now() < deadline)
137 auto meta = am.get_metadata_for_key(key);
138 if(!meta.location.empty() || !meta.meta.type.empty())
144 if(fs::exists(absolute, ec) && fs::file_size(absolute, ec) > 0)
148 std::this_thread::sleep_for(std::chrono::milliseconds(32));
153auto read_import_timeout_ms(
const simdjson::dom::object& args, int64_t default_ms) -> std::chrono::milliseconds
155 int64_t timeout_ms = default_ms;
156 if(args[
"wait_ms"].get(timeout_ms))
158 timeout_ms = default_ms;
164 if(timeout_ms > 60000)
168 return std::chrono::milliseconds(timeout_ms);
171auto collect_imported_asset_keys(
const import_files_item& item) -> std::vector<std::string>
173 std::vector<std::string> keys;
174 if(item.dest_key.empty())
178 if(!item.is_directory)
180 keys.push_back(item.dest_key);
184 const fs::path root(item.dest_path);
186 for(fs::recursive_directory_iterator it(root, ec), end; it !=
end && !ec; it.increment(ec))
188 if(!it->is_regular_file(ec))
192 const auto path = it->path();
193 if(path.extension().generic_string() == meta_ext)
205 keys.push_back(item.dest_key);
215 {.name =
"project_get_info",
216 .description =
"Get the currently open project name/path, or empty if none.",
223 return {.text = R
"({"open":false})", .is_error = false};
226 if(!pm.has_open_project())
228 return {.text = R
"({"open":false})", .is_error = false};
233 return {.text = fmt::format(R
"({{"open":true,"name":{},"path":{},"guid":{}}})",
239 .mutates_scene =
false});
242 {.name =
"assets_list_batch",
244 "List assets for a protocol group: app, engine, or editor. Optional type filter "
245 "(extension e.g. mat, .mat, pfb, emesh, etex, cs, spfb).",
247 R
"({"type":"object","properties":{"protocol":{"type":"string","enum":["app","engine","editor"]},"type":{"type":"string"}},"required":["protocol"]})",
251 std::string protocol;
254 return {.text =
"Missing protocol", .is_error =
true};
259 return {.text =
"Invalid protocol (use app|engine|editor)", .is_error =
true};
262 std::string type_filter;
264 if(!type_filter.empty())
272 std::string json =
"[";
274 for(
const auto& location : locations)
276 auto meta = am.get_metadata_for_key(location);
277 if(!type_filter.empty())
280 if(meta_type != type_filter && !path_matches_type(location, type_filter))
290 json += asset_entry_json(hpp::to_string(meta.meta.uid), location, meta.meta.type);
293 return {.text = json, .is_error =
false};
295 .mutates_scene =
false});
298 {.name =
"assets_find_batch",
300 "Search assets across app/engine/editor (or one protocol). Filters: type (any extension: "
301 "mat, pfb, emesh, etex, cs, spfb, ...), prefix (location starts with), name_contains "
302 "(case-insensitive path/stem match). Optional limit (default 200).",
303 .input_schema_json = R
"json({"type":"object","properties":{"protocol":{"type":"string","enum":["app","engine","editor","all"],"default":"all"},"type":{"type":"string","description":"Asset extension/type filter e.g. mat, .pfb, emesh"},"prefix":{"type":"string","description":"Location prefix e.g. app:/data/Materials/"},"name_contains":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":5000}}})json",
307 std::string protocol =
"all";
314 std::vector<std::string> groups;
315 if(protocol ==
"all")
317 groups = {
"app:/",
"engine:/",
"editor:/"};
324 return {.text =
"Invalid protocol (use app|engine|editor|all)", .is_error =
true};
326 groups.push_back(group);
329 std::string type_filter;
331 if(!type_filter.empty())
338 std::string name_contains;
342 if(args[
"limit"].get(limit))
356 std::string json =
"[";
360 for(
const auto& group : groups)
362 for(
const auto& location : am.get_all_assets(group))
365 if(!prefix.empty() && !
starts_with(location, prefix))
369 if(!name_contains.empty() && !
contains_ci(location, name_contains))
373 auto meta = am.get_metadata_for_key(location);
374 if(!type_filter.empty())
377 if(meta_type != type_filter && !path_matches_type(location, type_filter))
387 json += asset_entry_json(hpp::to_string(meta.meta.uid), location, meta.meta.type);
389 if(
static_cast<int64_t
>(
count) >= limit)
392 return {.text = fmt::format(
393 R
"({{"assets":{},"count":{},"scanned":{},"truncated":true,"limit":{}}})",
403 return {.text = fmt::format(R
"({{"assets":{},"count":{},"scanned":{},"truncated":false,"limit":{}}})",
410 .mutates_scene =
false});
413 {.name =
"assets_list_types",
414 .description =
"List known registered asset type names.",
419 std::string json =
"[";
423 for(
const auto& ext : group)
434 return {.text = json, .is_error =
false};
436 .mutates_scene =
false});
439 {.name =
"assets_list_embedded_primitives",
441 "List embedded mesh primitive names usable with scene_create_primitives_batch. "
442 "Axes: X-right, Y-up, Z-forward. Cube is 1x1x1 centered at origin.",
447 static const char* names[] = {
"Cube",
464 std::string json =
"[";
465 for(
size_t i = 0; i < std::size(names); ++i)
474 return {.text = json, .is_error =
false};
476 .mutates_scene =
false});
479 {.name =
"assets_create_folder",
481 "Create a folder under a protocol path (content-browser parity). Provide path key "
482 "(e.g. app:/data/Props) or folder+name. Optional wait_ms after create.",
484 R
"json({"type":"object","properties":{"path":{"type":"string"},"folder":{"type":"string"},"name":{"type":"string"},"wait_ms":{"type":"integer","minimum":0,"maximum":15000}}})json",
501 key = normalize_folder_key(path);
503 else if(!folder.empty() && !
name.empty())
505 key = normalize_folder_key(folder);
508 key = normalize_folder_key(key);
512 return {.text =
"Provide path, or folder + name", .is_error =
true};
515 auto create_result = mcp.invoke_on_main(
521 return {.text =
error, .is_error =
true};
525 if(fs::exists(absolute, ec))
527 if(fs::is_directory(absolute, ec))
529 return {.text = fmt::format(R
"({{"key":{},"created":false,"exists":true}})",
533 return {.text =
"Path exists and is not a folder: " + key, .is_error =
true};
535 fs::create_directories(absolute, ec);
538 return {.text =
"Failed to create folder: " + ec.message(), .is_error =
true};
540 return {.text = fmt::format(R
"({{"key":{},"created":true,"exists":false}})",
547 return {.text =
"Timed out creating folder on main thread", .is_error =
true};
549 if(!create_result->is_error)
553 return *create_result;
555 .mutates_scene =
false,
556 .requires_main_thread =
false});
559 {.name =
"assets_import_files",
561 "Import external files/folders into the open project (content-browser Import parity). "
562 "Never download into the project first — stage files outside app:/ (e.g. OS temp), "
563 "then pass those absolute paths here. paths: absolute filesystem paths outside the "
564 "project. folder: destination protocol key (e.g. app:/data/Imported). Waits for "
565 "async copy jobs, then polls until imported asset keys are ready (wait_ms, default "
566 "15000, max 60000). Focuses the editor window so the asset watcher can process "
569 R
"json({"type":"object","properties":{"paths":{"type":"array","items":{"type":"string"}},"folder":{"type":"string"},"wait_ms":{"type":"integer","minimum":0,"maximum":60000}},"required":["paths","folder"]})json",
573 const auto wait_ms = read_import_timeout_ms(args, 15000);
574 simdjson::dom::array paths_arr;
575 if(args[
"paths"].get(paths_arr))
577 return {.text =
"Missing paths array", .is_error =
true};
580 if(!
read_string(args,
"folder", folder) || folder.empty())
582 return {.text =
"Missing folder", .is_error =
true};
584 folder = normalize_folder_key(folder);
587 return {.text =
"folder must be under app:/ (project data)", .is_error =
true};
589 std::vector<std::string> paths;
590 for(
auto el : paths_arr)
592 std::string_view path_view;
593 if(el.get(path_view) || path_view.empty())
595 return {.text =
"Each paths entry must be a non-empty string", .is_error =
true};
597 paths.emplace_back(path_view);
601 return {.text =
"paths array is empty", .is_error =
true};
603 std::string project_error;
606 return {.text = project_error, .is_error =
true};
610 for(
const auto& path : paths)
612 const auto absolute_source = fs::absolute(fs::path(path));
614 fs::equivalent(project_root, absolute_source, ec))
617 "paths must be outside the project folder; download/stage "
618 "elsewhere then import (rejected: " +
619 absolute_source.generic_string() +
")",
624 if(fs::exists(target_path, ec) && !fs::is_directory(target_path, ec))
626 return {.text =
"folder resolves to a non-directory path: " + folder, .is_error =
true};
629 std::string focus_error;
634 const auto ready_deadline = std::chrono::steady_clock::now() + wait_ms;
635 std::string results =
"[";
637 size_t ready_count = 0;
638 size_t imported_count = 0;
639 for(
auto& item :
items)
641 bool copy_ok =
false;
642 if(item.future.valid() &&
643 item.future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready)
645 copy_ok = item.future.get();
647 const auto keys = collect_imported_asset_keys(item);
655 results += fmt::format(
656 R
"({{"ok":{},"source":{},"dest":{},"key":{},"is_directory":{},"ready":false,"error":"No destination key"}})",
657 copy_ok ? "true" :
"false",
661 item.is_directory ?
"true" :
"false");
664 imported_count += keys.size();
665 for(
const auto& key : keys)
670 const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
671 ready_deadline - std::chrono::steady_clock::now());
672 ready = try_wait_asset_ready(
675 remaining.count() > 0 ? remaining : std::chrono::milliseconds(0));
686 results += fmt::format(
687 R
"({{"ok":{},"source":{},"dest":{},"key":{},"is_directory":{},"ready":{}}})",
688 copy_ok ? "true" :
"false",
692 item.is_directory ?
"true" :
"false",
693 ready ?
"true" :
"false");
697 const bool ok = copied && ready_count == imported_count && imported_count > 0;
698 return {.text = fmt::format(
699 R
"({{"folder":{},"results":{},"imported":{},"ready":{},"copied":{}}})",
704 copied ? "true" :
"false"),
707 .mutates_scene =
false,
708 .requires_main_thread =
false});
711 {.name =
"assets_get_mesh_info",
713 "Get mesh asset local AABB (min/max/center/extents). key is any mesh-format asset "
714 "(see ex::get_suported_formats<mesh>). Axes: X-right, Y-up, Z-forward.",
715 .input_schema_json = R
"json({"type":"object","properties":{"key":{"type":"string"}},"required":["key"]})json",
722 return {.text =
"Missing key", .is_error =
true};
728 return {.text =
"Mesh asset not found: " + key, .is_error =
true};
730 auto mesh_ptr =
handle.get(
true);
733 return {.text =
"Mesh failed to load: " + key, .is_error =
true};
736 return {.text = fmt::format(R
"({{"key":{},"uid":{},"bounds":{}}})",
742 .mutates_scene =
false});
745 {.name =
"assets_get_batch",
746 .description =
"Get many assets by key and/or uid. Each item: key or uid.",
748 R
"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"uid":{"type":"string"}}}}},"required":["items"]})json",
753 simdjson::dom::array items_arr;
754 if(args[
"items"].get(items_arr))
756 return {.text =
"Missing items array", .is_error =
true};
758 std::string results =
"[";
761 size_t requested = 0;
762 for(
auto el : items_arr)
765 simdjson::dom::object obj;
768 return {.text =
"Each item must be an object", .is_error =
true};
782 meta = am.get_metadata_for_key(key);
784 else if(!uid_str.empty())
786 auto uuid = hpp::uuid::from_string(uid_str);
789 results += fmt::format(R
"({{"ok":false,"uid":{},"error":"Invalid uid"}})",
793 meta = am.get_metadata(*uuid);
797 results += R
"({"ok":false,"error":"Provide key or uid"})";
800 if(meta.location.empty() && meta.meta.type.empty())
802 results += fmt::format(R
"({{"ok":false,"key":{},"uid":{},"error":"Asset not found"}})",
808 results += fmt::format(
809 R
"({{"ok":true,"uid":{},"location":{},"type":{}}})",
815 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})", results, ok_count, requested),
816 .is_error = ok_count == 0 && requested > 0};
818 .mutates_scene = false});
821 {.name =
"assets_reimport_batch",
822 .description =
"Reimport many assets by key. Optional wait_ms after the batch.",
824 R
"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]}},"wait_ms":{"type":"integer","minimum":0,"maximum":15000}},"required":["items"]})json",
830 simdjson::dom::array items_arr;
831 if(args[
"items"].get(items_arr))
833 return {.text =
"Missing items array", .is_error =
true};
835 std::vector<std::string> keys;
836 for(
auto el : items_arr)
838 simdjson::dom::object obj;
841 return {.text =
"Each item must be an object", .is_error =
true};
846 return {.text =
"Item missing key", .is_error =
true};
848 keys.push_back(std::move(key));
852 return {.text =
"items array is empty", .is_error =
true};
854 auto result = mcp.invoke_on_main(
857 std::string results =
"[";
860 for(
const auto& key : keys)
869 if(source.empty() || !fs::exists(source, ec))
871 results += fmt::format(R
"({{"ok":false,"key":{},"error":{}}})",
878 results += fmt::format(R
"({{"ok":false,"key":{},"error":{}}})",
885 results += fmt::format(R
"({{"ok":true,"key":{},"reimported":true}})", make_json_string(key));
888 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})",
892 .is_error = ok_count == 0};
896 return {.text =
"Timed out reimporting on main thread", .is_error =
true};
898 if(!result->is_error)
904 .mutates_scene =
false,
905 .requires_main_thread =
false});
908 {.name =
"assets_wait_ready_batch",
910 "Wait until many asset keys are loadable/ready (poll). timeout_ms applies per key "
913 R
"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]}},"timeout_ms":{"type":"integer","minimum":0,"maximum":60000}},"required":["items"]})json",
917 simdjson::dom::array items_arr;
918 if(args[
"items"].get(items_arr))
920 return {.text =
"Missing items array", .is_error =
true};
922 int64_t timeout_ms = 5000;
923 if(args[
"timeout_ms"].get(timeout_ms))
931 if(timeout_ms > 60000)
935 std::string results =
"[";
938 size_t requested = 0;
939 for(
auto el : items_arr)
942 simdjson::dom::object obj;
945 return {.text =
"Each item must be an object", .is_error =
true};
950 return {.text =
"Item missing key", .is_error =
true};
953 try_wait_asset_ready(ctx, key, std::chrono::milliseconds(timeout_ms));
963 results += fmt::format(R
"({{"key":{},"ready":{}}})",
965 ready ? "true" :
"false");
968 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})", results, ok_count, requested),
969 .is_error = ok_count != requested};
971 .mutates_scene = false,
972 .requires_main_thread =
false});
975 {.name =
"prefabs_create_from_entities_batch",
977 "Save entity hierarchies as .pfb prefab assets (batch). Each item: entity_id, path or "
978 "folder (+ optional name), optional attach. Optional wait_ms after the batch.",
980 R
"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"entity_id":{"type":"string"},"path":{"type":"string"},"folder":{"type":"string"},"name":{"type":"string"},"attach":{"type":"boolean"}},"required":["entity_id"]}},"wait_ms":{"type":"integer","minimum":0,"maximum":15000}},"required":["items"]})json",
986 simdjson::dom::array items_arr;
987 if(args[
"items"].get(items_arr))
989 return {.text =
"Missing items array", .is_error =
true};
993 std::string entity_id;
999 std::vector<item_t>
items;
1000 for(
auto el : items_arr)
1002 simdjson::dom::object obj;
1005 return {.text =
"Each item must be an object", .is_error =
true};
1008 if(!
read_string(obj,
"entity_id", item.entity_id) || item.entity_id.empty())
1010 return {.text =
"Item missing entity_id", .is_error =
true};
1016 items.push_back(std::move(item));
1020 return {.text =
"items array is empty", .is_error =
true};
1022 auto result = mcp.invoke_on_main(
1025 scene* scn =
nullptr;
1029 return {.text =
error, .is_error =
true};
1033 return {.text =
error, .is_error =
true};
1036 std::string results =
"[";
1038 size_t ok_count = 0;
1039 for(
const auto& item :
items)
1049 results += fmt::format(R
"({{"ok":false,"entity_id":{},"error":"Entity not found"}})",
1054 if(!item.path.empty())
1058 else if(!item.folder.empty())
1060 key = normalize_folder_key(item.folder);
1062 if(!item.name.empty())
1077 results += fmt::format(
1078 R
"json({{"ok":false,"entity_id":{},"error":"Provide path, or folder and optional name"}})json",
1083 const fs::path as_path(key);
1084 if(as_path.is_absolute())
1089 if(!ends_with_ci(key, prefab_ext))
1094 fs::create_directories(absolute.parent_path(), ec);
1097 results += fmt::format(R
"({{"ok":false,"entity_id":{},"key":{},"error":"Failed to save prefab"}})",
1102 auto prefab_handle = am.get_asset<
prefab>(key);
1103 if(item.attach && prefab_handle)
1108 results += fmt::format(
1109 R
"({{"ok":true,"key":{},"uid":{},"entity_id":{},"attached":{}}})",
1113 item.attach ? "true" :
"false");
1116 return {.text = fmt::format(R
"({{"results":{},"count":{},"requested":{}}})",
1120 .is_error = ok_count == 0};
1122 std::chrono::milliseconds(15000));
1125 return {.text =
"Timed out creating prefabs on main thread", .is_error =
true};
1127 if(!result->is_error)
1133 .mutates_scene =
true,
1134 .requires_main_thread =
false});
Manages assets, including loading, unloading, and storage.
auto get_all_assets(const std::string &group) const -> std::vector< std::string >
Gets all assets.
auto get_asset(const std::string &key, load_flags flags=load_flags::standard, load_mode mode=load_mode::immediate) -> asset_handle< T >
Gets an asset by its key.
Main class representing a 3D mesh with support for different LODs, submeshes, and skinning.
auto get_bounds() const -> const math::bbox &
Gets the local bounding box for this mesh.
auto get_project_info() -> project_info &
auto get_all_formats() -> const std::vector< std::vector< std::string > > &
auto get_format(bool include_dot=true) -> std::string
auto is_format(const std::string &ex) -> bool
auto get_meta_format() -> const std::string &
path resolve_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
bool is_any_parent_path(const path &parent, const path &child)
path convert_to_protocol(const path &_path)
Oposite of the resolve_protocol this function tries to convert to protocol path from an absolute one.
void end(encoder *_encoder)
auto resolve_asset_source_path(const std::string &asset_key) -> fs::path
void reimport_key(const std::string &asset_key)
auto can_reimport(const fs::path &absolute_path) -> bool
auto atomic_save_to_file(const fs::path &key, const asset_handle< T > &obj) -> bool
void register_asset_tools(mcp_tool_registry ®istry)
auto sleep_worker(std::chrono::milliseconds wait_ms) -> void
auto protocol_to_group(const std::string &protocol) -> std::string
auto empty_object_schema() -> std::string
auto find_entity(scene &scn, const std::string &id) -> entt::handle
auto to_lower_ascii(std::string value) -> 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 starts_with(std::string_view value, std::string_view prefix) -> bool
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 require_open_project(rtti::context &ctx, std::string &error) -> bool
auto contains_ci(std::string_view haystack, std::string_view needle) -> bool
auto normalize_asset_type_filter(std::string type) -> std::string
auto bbox_to_json(const math::bbox &bounds) -> std::string
static auto import_files(rtti::context &ctx, const std::vector< std::string > &paths, const fs::path &target_path, bool async=true) -> std::vector< import_files_item >
Copy external files/folders into target_path (content-browser Import parity).
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 wait_import_jobs(std::vector< import_files_item > &items, std::chrono::milliseconds timeout) -> bool
Block until all import copy jobs complete or timeout elapses.
Component that holds a reference to a prefab asset and tracks property overrides.
Represents a generic prefab with a buffer for serialized data.
Represents a scene in the ACE framework, managing entities and their relationships.
Component that provides a tag (name or label) for an entity.