Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mcp_tools_scripts.cpp
Go to the documentation of this file.
1#include "mcp_tools_common.h"
2
13
14#include <fstream>
15#include <sstream>
16#include <stdexcept>
17
18namespace unravel::mcp
19{
20namespace
21{
22
23auto read_text_file(const fs::path& path, std::string& out, std::string& error) -> bool
24{
25 std::ifstream input(path, std::ios::binary);
26 if(!input.is_open())
27 {
28 error = "Failed to open file: " + path.generic_string();
29 return false;
30 }
31 std::ostringstream buffer;
32 buffer << input.rdbuf();
33 out = buffer.str();
34 return true;
35}
36
37auto write_text_file_atomic(const fs::path& path, const std::string& contents, std::string& error) -> bool
38{
39 fs::error_code err;
40 const auto absolute = fs::absolute(path);
41 fs::create_directories(absolute.parent_path(), err);
42 err.clear();
44 absolute,
45 [&](const fs::path& temp)
46 {
47 std::ofstream output(temp, std::ios::binary | std::ios::trunc);
48 if(!output.is_open())
49 {
50 throw std::runtime_error("Failed to open temp file for write: " + temp.generic_string());
51 }
52 output.write(contents.data(), static_cast<std::streamsize>(contents.size()));
53 if(!output.good())
54 {
55 throw std::runtime_error("Failed writing temp file: " + temp.generic_string());
56 }
57 },
58 err);
59 if(err)
60 {
61 error = "Atomic write failed: " + absolute.generic_string() + " (" + err.message() + ")";
62 return false;
63 }
64 return true;
65}
66
67auto resolve_source_path(const std::string& path_or_key) -> fs::path
68{
69 if(path_or_key.empty())
70 {
71 return {};
72 }
73 fs::error_code ec;
74 const fs::path as_path(path_or_key);
75 if(as_path.is_absolute())
76 {
77 return as_path;
78 }
79 auto resolved = fs::resolve_protocol(path_or_key);
80 if(!resolved.empty())
81 {
82 return resolved;
83 }
84 return as_path;
85}
86
87auto find_script_object(script_component& script_comp, const std::string& type_name)
88 -> script_component::script_object
89{
90 for(const auto& obj : script_comp.get_script_components())
91 {
92 if(!obj.pinned)
93 {
94 continue;
95 }
96 if(obj.pinned->get_object().get_type().get_fullname() == type_name)
97 {
98 return obj;
99 }
100 }
101 return {};
102}
103
104auto unique_script_path(const fs::path& dir, const std::string& stem) -> fs::path
105{
106 const auto ext = ex::get_format<script>();
107 fs::path candidate = dir / (stem + ext);
108 fs::error_code ec;
109 int i = 0;
110 while(fs::exists(candidate, ec))
111 {
112 ++i;
113 candidate = dir / (fmt::format("{}{}", stem, i) + ext);
114 }
115 return candidate;
116}
117
118auto protocol_for_path(const fs::path& absolute) -> std::string
119{
120 const auto key = fs::convert_to_protocol(absolute).generic_string();
121 return fs::extract_protocol(key).string();
122}
123
124} // namespace
125
127{
128 registry.add(
129 {.name = "scripts_list_types",
130 .description =
131 "List C# ScriptComponent type full names available to add via scene_add_scripts_batch "
132 "(project + engine scriptable types from script_system).",
133 .input_schema_json = empty_object_schema(),
134 .handler =
135 [](rtti::context& ctx, const simdjson::dom::object&) -> tool_result
136 {
137 if(!ctx.has<script_system>())
138 {
139 return {.text = "Script system unavailable", .is_error = true};
140 }
141 const auto& types = ctx.get_cached<script_system>().get_all_scriptable_components();
142 std::string json = "[";
143 for(size_t i = 0; i < types.size(); ++i)
144 {
145 if(i > 0)
146 {
147 json += ",";
148 }
149 json += make_json_string(types[i].get_fullname());
150 }
151 json += "]";
152 return {.text = json, .is_error = false};
153 },
154 .mutates_scene = false});
155
156 registry.add(
157 {.name = "scripts_get_source",
158 .description =
159 "Read C# source for a script. Resolve by path/key, or by entity_id + type_name "
160 "(uses get_script_source_location).",
161 .input_schema_json =
162 R"json({"type":"object","properties":{"path":{"type":"string"},"key":{"type":"string"},"entity_id":{"type":"string"},"type_name":{"type":"string"}}})json",
163 .handler =
164 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
165 {
166 std::string path;
167 std::string key;
168 read_string(args, "path", path);
169 read_string(args, "key", key);
170 if(path.empty() && !key.empty())
171 {
172 path = key;
173 }
174
175 std::string type_name;
176 read_string(args, "type_name", type_name);
177 std::string entity_id;
178 read_string(args, "entity_id", entity_id);
179
180 if(path.empty())
181 {
182 if(entity_id.empty() || type_name.empty())
183 {
184 return {.text = "Provide path/key, or entity_id + type_name", .is_error = true};
185 }
186 auto& em = ctx.get_cached<editing_manager>();
187 auto* scn = em.get_active_scene(ctx);
188 if(!scn || !scn->registry)
189 {
190 return {.text = "No active scene", .is_error = true};
191 }
192 auto entity = find_entity(*scn, entity_id);
193 auto* script_comp = entity ? entity.try_get<script_component>() : nullptr;
194 if(!script_comp)
195 {
196 return {.text = "Entity has no ScriptComponent", .is_error = true};
197 }
198 auto obj = find_script_object(*script_comp, type_name);
199 if(!obj.pinned)
200 {
201 return {.text = "Script type not on entity: " + type_name, .is_error = true};
202 }
203 path = script_comp->get_script_source_location(obj);
204 if(path.empty())
205 {
206 return {.text = "No source path for script type: " + type_name, .is_error = true};
207 }
208 }
209
210 const auto absolute = resolve_source_path(path);
211 std::string code;
212 std::string error;
213 if(!read_text_file(absolute, code, error))
214 {
215 return {.text = error, .is_error = true};
216 }
217 return {.text = fmt::format(R"({{"path":{},"bytes":{},"code":{}}})",
218 make_json_string(absolute.generic_string()),
219 code.size(),
220 make_json_string(code)),
221 .is_error = false};
222 },
223 .mutates_scene = false});
224
225 registry.add(
226 {.name = "scripts_set_sources_batch",
227 .description =
228 "Write C# source files (batch) via asset_writer::atomic_write_file. Each item: code, "
229 "plus path/key or entity_id + type_name. Optional recompile (default true, once for batch).",
230 .input_schema_json =
231 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"path":{"type":"string"},"key":{"type":"string"},"entity_id":{"type":"string"},"type_name":{"type":"string"}},"required":["code"]}},"recompile":{"type":"boolean","default":true}},"required":["items"]})json",
232 .handler =
233 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
234 {
235 std::string error;
237 {
238 return {.text = error, .is_error = true};
239 }
240 simdjson::dom::array items_arr;
241 if(args["items"].get(items_arr))
242 {
243 return {.text = "Missing items array", .is_error = true};
244 }
245 bool recompile = true;
246 read_bool(args, "recompile", recompile);
247 std::string results = "[";
248 bool first = true;
249 size_t ok_count = 0;
250 size_t requested = 0;
251 std::string protocol;
252 for(auto el : items_arr)
253 {
254 ++requested;
255 simdjson::dom::object obj;
256 if(el.get(obj))
257 {
258 return {.text = "Each item must be an object", .is_error = true};
259 }
260 if(!first)
261 {
262 results += ",";
263 }
264 first = false;
265 std::string code;
266 if(!read_string(obj, "code", code))
267 {
268 results += R"({"ok":false,"error":"Missing code"})";
269 continue;
270 }
271 std::string path;
272 std::string key;
273 read_string(obj, "path", path);
274 read_string(obj, "key", key);
275 if(path.empty() && !key.empty())
276 {
277 path = key;
278 }
279 std::string type_name;
280 std::string entity_id;
281 read_string(obj, "type_name", type_name);
282 read_string(obj, "entity_id", entity_id);
283 if(path.empty())
284 {
285 if(entity_id.empty() || type_name.empty())
286 {
287 results += R"({"ok":false,"error":"Provide path/key, or entity_id + type_name"})";
288 continue;
289 }
290 scene* scn = nullptr;
291 if(!require_edit_scene(ctx, scn, error))
292 {
293 results += fmt::format(R"({{"ok":false,"error":{}}})", make_json_string(error));
294 continue;
295 }
296 auto entity = find_entity(*scn, entity_id);
297 auto* script_comp = entity ? entity.try_get<script_component>() : nullptr;
298 if(!script_comp)
299 {
300 results += R"({"ok":false,"error":"Entity has no ScriptComponent"})";
301 continue;
302 }
303 auto script_obj = find_script_object(*script_comp, type_name);
304 if(!script_obj.pinned)
305 {
306 results += fmt::format(R"({{"ok":false,"error":{}}})",
307 make_json_string("Script type not on entity: " + type_name));
308 continue;
309 }
310 path = script_comp->get_script_source_location(script_obj);
311 if(path.empty())
312 {
313 results += fmt::format(R"({{"ok":false,"error":{}}})",
314 make_json_string("No source path for script type: " + type_name));
315 continue;
316 }
317 }
318 const auto absolute = resolve_source_path(path);
319 if(!write_text_file_atomic(absolute, code, error))
320 {
321 results += fmt::format(R"({{"ok":false,"error":{}}})", make_json_string(error));
322 continue;
323 }
324 if(recompile && protocol.empty())
325 {
326 protocol = protocol_for_path(absolute);
327 if(protocol.empty())
328 {
329 protocol = "app";
330 }
331 }
332 ++ok_count;
333 results += fmt::format(R"({{"ok":true,"path":{},"bytes":{}}})",
334 make_json_string(absolute.generic_string()),
335 code.size());
336 }
337 results += "]";
338 if(recompile && ok_count > 0 && !protocol.empty())
339 {
341 }
342 return {.text = fmt::format(R"({{"results":{},"count":{},"requested":{},"recompile":{},"protocol":{}}})",
343 results,
344 ok_count,
345 requested,
346 recompile ? "true" : "false",
347 make_json_string(protocol)),
348 .is_error = ok_count == 0 && requested > 0};
349 },
350 .mutates_scene = false});
351
352 registry.add(
353 {.name = "scripts_create_batch",
354 .description =
355 "Create many C# ScriptComponent .cs files from TemplateComponent.cs.in. Each item: name, "
356 "optional folder (default app:/data/scripts). Optional recompile (default true, once).",
357 .input_schema_json =
358 R"json({"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"folder":{"type":"string"}},"required":["name"]}},"recompile":{"type":"boolean","default":true}},"required":["items"]})json",
359 .handler =
360 [](rtti::context& ctx, const simdjson::dom::object& args) -> tool_result
361 {
362 std::string error;
364 {
365 return {.text = error, .is_error = true};
366 }
367 simdjson::dom::array items_arr;
368 if(args["items"].get(items_arr))
369 {
370 return {.text = "Missing items array", .is_error = true};
371 }
372 bool recompile = true;
373 read_bool(args, "recompile", recompile);
374 const auto template_path =
375 fs::resolve_protocol("engine:/data/templates/TemplateComponent" + ex::get_format<script>() + ".in");
376 std::string results = "[";
377 bool first = true;
378 size_t ok_count = 0;
379 size_t requested = 0;
380 std::string protocol;
381 for(auto el : items_arr)
382 {
383 ++requested;
384 simdjson::dom::object obj;
385 if(el.get(obj))
386 {
387 return {.text = "Each item must be an object", .is_error = true};
388 }
389 if(!first)
390 {
391 results += ",";
392 }
393 first = false;
394 std::string name;
395 if(!read_string(obj, "name", name) || name.empty())
396 {
397 results += R"({"ok":false,"error":"Missing name"})";
398 continue;
399 }
400 if(name.size() > 3 && name.substr(name.size() - 3) == ".cs")
401 {
402 name = name.substr(0, name.size() - 3);
403 }
405 {
406 results += R"({"ok":false,"error":"name must be a valid C# identifier"})";
407 continue;
408 }
409 std::string folder = "app:/data/scripts";
410 read_string(obj, "folder", folder);
411 if(folder.empty())
412 {
413 folder = "app:/data/scripts";
414 }
415 const auto dir = resolve_source_path(folder);
416 const auto dst = unique_script_path(dir, name);
417 if(!asset_actions::create_script_from_template(template_path, dst, &error))
418 {
419 results += fmt::format(R"({{"ok":false,"error":{}}})", make_json_string(error));
420 continue;
421 }
422 if(recompile && protocol.empty())
423 {
424 protocol = protocol_for_path(dst);
425 if(protocol.empty())
426 {
427 protocol = "app";
428 }
429 }
430 ++ok_count;
431 const auto key = fs::convert_to_protocol(dst).generic_string();
432 results += fmt::format(R"({{"ok":true,"path":{},"key":{},"type_name":{}}})",
433 make_json_string(dst.generic_string()),
434 make_json_string(key),
435 make_json_string(dst.stem().string()));
436 }
437 results += "]";
438 if(recompile && ok_count > 0 && !protocol.empty())
439 {
441 }
442 return {.text = fmt::format(R"({{"results":{},"count":{},"requested":{},"recompile":{},"protocol":{}}})",
443 results,
444 ok_count,
445 requested,
446 recompile ? "true" : "false",
447 make_json_string(protocol)),
448 .is_error = ok_count == 0 && requested > 0};
449 },
450 .mutates_scene = false});
451}
452
453} // namespace unravel::mcp
Class that contains core data for audio listeners. There can only be one instance of it per scene.
std::string name
Definition hub.cpp:33
std::string error
Definition mcp_async.cpp:33
auto get_format(bool include_dot=true) -> std::string
path extract_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
path resolve_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
path convert_to_protocol(const path &_path)
Oposite of the resolve_protocol this function tries to convert to protocol path from an absolute one.
auto is_valid_csharp_identifier(const std::string &name) -> bool
auto create_script_from_template(const fs::path &template_path, const fs::path &dst, std::string *error) -> bool
Instantiate a ScriptComponent template (.cs.in), substituting #SCRIPTNAME# with the destination file ...
void atomic_write_file(const fs::path &dst, const std::function< void(const fs::path &)> &callback, fs::error_code &ec) noexcept
auto require_not_play_mode(rtti::context &ctx, std::string &error) -> bool
auto empty_object_schema() -> 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
void register_script_tools(mcp_tool_registry &registry)
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
entt::handle entity
auto get_cached() -> T &
Definition context.hpp:49
auto has() const -> bool
Definition context.hpp:28
auto get_active_scene(rtti::context &ctx) -> scene *
Represents a scene in the ACE framework, managing entities and their relationships.
Definition scene.h:70
static void set_needs_recompile(const std::string &protocol, bool now=false)