Unravel Engine C++ Reference
Loading...
Searching...
No Matches
script_system.cpp
Go to the documentation of this file.
1#include "script_system.h"
5#include <engine/ecs/ecs.h>
6#include <engine/engine.h>
7#include <engine/events.h>
8#include <engine/play_mode.h>
14
15
16#include <dotnetpp/dotnetpp.h>
17
18
21#include <logging/logging.h>
22#include <seq/seq.h>
24
25namespace unravel
26{
27namespace
28{
29
30enum class recompile_command : int
31{
32 none,
33 compile_at_schedule,
34 compile_now,
35};
36
37std::chrono::milliseconds check_interval(50);
38std::atomic_bool initted{};
39
40std::atomic<recompile_command> needs_recompile{};
41std::mutex container_mutex;
42std::vector<std::string> needs_to_recompile;
43std::atomic<uint64_t> compilation_version{};
44
45std::atomic_bool debug_mode{true};
46
47auto print_assembly_info(const dotnet::assembly& assembly)
48{
49 std::stringstream ss;
50 auto refs = assembly.dump_references();
51
52 ss << fmt::format(" ----- References -----");
53
54 for(const auto& ref : refs)
55 {
56 ss << fmt::format("\n{}", ref);
57 }
58
59 APPLOG_TRACE("\n{}", ss.str());
60
61 auto types = assembly.get_types();
62
63 ss = {};
64 ss << fmt::format(" ----- Types -----");
65
66 for(const auto& type : types)
67 {
68 ss << fmt::format("\n{}", type.get_fullname());
69 ss << fmt::format("\n sizeof {}", type.get_sizeof());
70 ss << fmt::format("\n alignof {}", type.get_alignof());
71
72 {
73 auto attribs = type.get_attributes();
74 for(const auto& attrib : attribs)
75 {
76 ss << fmt::format("\n - Attribute : {}", attrib.get_type().get_fullname());
77 }
78 }
79
80 auto fields = type.get_fields();
81 for(const auto& field : fields)
82 {
83 ss << fmt::format("\n - Field : {}", field.get_name());
84
85 auto attribs = field.get_attributes();
86 for(const auto& attrib : attribs)
87 {
88 ss << fmt::format("\n -- Attribute : {}", attrib.get_type().get_fullname());
89 }
90 }
91
92 auto properties = type.get_properties();
93 for(const auto& prop : properties)
94 {
95 ss << fmt::format("\n - Property : {}", prop.get_name());
96
97 auto attribs = prop.get_attributes();
98 for(const auto& attrib : attribs)
99 {
100 ss << fmt::format("\n -- Attribute : {}", attrib.get_type().get_fullname());
101 }
102 }
103 }
104 APPLOG_TRACE("\n{}", ss.str());
105}
106
107} // namespace
108
109void script_system::log_exception(const dotnet::exception& e, const hpp::source_location& loc)
110{
111 auto frame = dotnet::extract_relevant_stack_frame(e.what());
112 if(!frame.file_name.empty())
113 {
114 APPLOG_ERROR_LOC(frame.file_name.c_str(), frame.line, frame.function_name.c_str(), e.what());
115 }
116 else
117 {
118 APPLOG_ERROR_LOC(loc.file_name(), int(loc.line()), loc.function_name(), e.what());
119 }
120}
121
122void script_system::copy_compiled_lib(const fs::path& from, const fs::path& to)
123{
124 auto from_debug_info = from;
125 from_debug_info.concat(".mdb");
126 auto from_comments_xml = from;
127 from_comments_xml.replace_extension(".xml");
128
129 auto to_debug_info = to;
130 to_debug_info.concat(".mdb");
131 auto to_comments_xml = to;
132 to_comments_xml.replace_extension(".xml");
133
134 fs::error_code er;
135 fs::copy_file(from, to, fs::copy_options::overwrite_existing, er);
136 fs::copy_file(from_debug_info, to_debug_info, fs::copy_options::overwrite_existing, er);
137 fs::copy_file(from_comments_xml, to_comments_xml, fs::copy_options::overwrite_existing, er);
138
139 fs::remove(from, er);
140 fs::remove(from_debug_info, er);
141 fs::remove(from_comments_xml, er);
142}
143
144auto script_system::find_dotnet_paths(const rtti::context& ctx) -> dotnet::compiler_paths
145{
146 bool is_deploy_mode = ctx.has<deploy>();
147
148 dotnet::compiler_paths result;
149
150 if(is_deploy_mode)
151 {
152#if DOTNETPP_BACKEND_MONO
153 auto mono_dir = fs::resolve_protocol("engine:/mono");
154 result.assembly_dir = fs::absolute(mono_dir / "lib").string();
155 result.config_dir = fs::absolute(mono_dir / "etc").string();
156#else
157 fs::error_code ec;
158
159 // The managed bridge is shipped next to the bundled dotnet root; pass
160 // its location explicitly. If missing, the loader falls back to
161 // probing <exe_dir>/<bridge dir> and the working directory.
162 auto bridge_dir = fs::resolve_protocol("engine:/" + std::string(dotnet::managed_runtime_dir()));
163 if(fs::exists(bridge_dir, ec))
164 {
165 result.assembly_dir = fs::absolute(bridge_dir).string();
166 }
167
168 // Self-contained deploys bundle a pruned dotnet root (hostfxr + shared
169 // framework); pass it as the dotnet root override when present,
170 // otherwise fall back to a machine-wide install.
171 auto dotnet_dir = fs::resolve_protocol("engine:/dotnet");
172 if(fs::exists(dotnet_dir, ec))
173 {
174 result.config_dir = fs::absolute(dotnet_dir).string();
175 }
176#endif
177 }
178 else
179 {
180 const auto& names = dotnet::get_common_library_names();
181 const auto& library_paths = dotnet::get_common_library_paths();
182 const auto& config_paths = dotnet::get_common_config_paths();
183
184 for(size_t i = 0; i < library_paths.size(); ++i)
185 {
186 const auto& library_path = library_paths.at(i);
187 const auto& config_path = config_paths.at(i);
188 std::vector<std::string> paths{library_path};
189 auto found_library = fs::find_library(names, paths);
190
191 if(!found_library.empty())
192 {
193 result.assembly_dir = fs::path(library_path).make_preferred().string();
194 result.config_dir = fs::path(config_path).make_preferred().string();
195
196 break;
197 }
198 }
199 }
200
201 {
202 const auto& names = dotnet::get_common_executable_names();
203 const auto& paths = dotnet::get_common_executable_paths();
204
205 result.msc_executable = fs::find_program(names, paths).make_preferred().string();
206 }
207
208 APPLOG_TRACE("DOTNET_PATHS:");
209 APPLOG_TRACE("Assembly path - {}", result.assembly_dir);
210 APPLOG_TRACE("Config path - {}", result.config_dir);
211
212 return result;
213}
214
215/*
216 * automatic: leave CoreCLR alone (default). JIT platforms use the JIT;
217 * no-JIT packs (iOS) enable the interpreter themselves.
218 * forced: set DOTNET_InterpMode=1 for desktop testing against an
219 * interpreter-capable runtime (--interpreter forced, or
220 * UNRAVEL_FORCE_DOTNET_INTERPRETER).
221 */
222auto select_interpreter_config(const cmd_line::parser& parser) -> dotnet::interpreter_config
223{
224 dotnet::interpreter_config config;
225#if defined(UNRAVEL_FORCE_DOTNET_INTERPRETER)
226 config.interp_mode = dotnet::interpreter_config::mode::forced;
227#endif
228 std::string mode;
229 if(parser.try_get("interpreter", mode) && mode == "forced")
230 {
231 config.interp_mode = dotnet::interpreter_config::mode::forced;
232 }
233 return config;
234}
235
237{
238 (void)ctx;
239 parser.set_optional<std::string>("",
240 "interpreter",
241 "auto",
242 "CoreCLR interpreter mode (auto|forced).");
243}
244
245auto validate_paths(const dotnet::compiler_paths& paths, bool is_deploy_mode) -> bool
246{
247#if DOTNETPP_BACKEND_MONO
248 (void)is_deploy_mode;
249 return !paths.assembly_dir.empty() && !paths.config_dir.empty() && !paths.msc_executable.empty();
250#else
251 if(is_deploy_mode)
252 {
253 // Deployed games load precompiled assemblies, so no compiler is
254 // required. The runtime comes from the bundled dotnet root
255 // (config_dir) or, if absent, from the machine install.
256 return true;
257 }
258 return !paths.msc_executable.empty();
259#endif
260}
261
262auto script_system::init(rtti::context& ctx, const cmd_line::parser& parser) -> bool
263{
264 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
265
266 auto& ev = ctx.get_cached<events>();
267 ev.on_frame_update.connect(sentinel_, this, &script_system::on_frame_update);
268 ev.on_frame_fixed_update.connect(sentinel_, this, &script_system::on_frame_fixed_update);
269 ev.on_frame_update.connect(sentinel_, -100000, this, &script_system::on_frame_late_update);
270 ev.on_play_begin.connect(sentinel_, -1000, this, &script_system::on_play_begin);
271 ev.on_play_end.connect(sentinel_, 1000, this, &script_system::on_play_end);
272 ev.on_pause.connect(sentinel_, 100, this, &script_system::on_pause);
273 ev.on_resume.connect(sentinel_, -100, this, &script_system::on_resume);
274 ev.on_skip_next_frame.connect(sentinel_, -100, this, &script_system::on_skip_next_frame);
275
276 auto mono_paths = find_dotnet_paths(ctx);
277
278 if(!validate_paths(mono_paths, ctx.has<deploy>()))
279 {
280#if DOTNETPP_BACKEND_MONO
281 ctx.get_cached<loading_screen>().fail(
282 "Failed to locate Mono C#. Please install it from - https://www.mono-project.com/download/stable/");
283#else
284 ctx.get_cached<loading_screen>().fail(
285 "Failed to locate the .NET runtime. Please install it from - https://dotnet.microsoft.com/download");
286#endif
287 return false;
288 }
289
290 debug_config_.enable_debugging = true;
291
292 dotnet::set_log_handler("info",
293 [](const std::string& msg)
294 {
295 APPLOG_INFO("{}", msg);
296 });
297 dotnet::set_log_handler("trace",
298 [](const std::string& msg)
299 {
300 APPLOG_TRACE("{}", msg);
301 });
302 dotnet::set_log_handler("warning",
303 [](const std::string& msg)
304 {
305 APPLOG_WARNING("{}", msg);
306 });
307 dotnet::set_log_handler("error",
308 [](const std::string& msg)
309 {
310 APPLOG_ERROR("{}", msg);
311 });
312
313 if(dotnet::init(mono_paths, debug_config_, select_interpreter_config(parser)))
314 {
315 bind_internal_calls(ctx);
316
317 dotnet::domain::set_assemblies_path(fs::resolve_protocol(ex::get_compiled_directory("engine")).string());
318
319 try
320 {
321 if(!load_engine_domain(ctx, true))
322 {
323 return false;
324 }
325 }
326 catch(const dotnet::exception& e)
327 {
328 log_exception(e);
329 return false;
330 }
331
332 initted = true;
333 return true;
334 }
335#if DOTNETPP_BACKEND_MONO
336 ctx.get_cached<loading_screen>().fail(
337 "Failed to initialize Mono C#. Please install it from - https://www.mono-project.com/download/stable/");
338 return false;
339#else
340 ctx.get_cached<loading_screen>().fail(
341 "Failed to initialize the .NET runtime. Please install it from - https://dotnet.microsoft.com/download");
342 return false;
343#endif
344}
345
347{
348 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
349
350 unload_app_domain();
351 unload_engine_domain();
352
353 dotnet::shutdown();
354
355 return true;
356}
357
358void script_system::set_debug_config(const std::string& address, uint32_t port, uint32_t loglevel)
359{
360 debug_config_.address = address;
361 debug_config_.port = port;
362 debug_config_.loglevel = loglevel;
363}
364
365auto script_system::load_engine_domain(rtti::context& ctx, bool recompile) -> bool
366{
367 APPLOG_TRACE_PERF_NAMED(std::chrono::milliseconds, "Load Engine Domain");
368
369 bool is_deploy_mode = ctx.has<deploy>();
370
371 if(!is_deploy_mode && recompile)
372 {
373 bool debug = false;
374#ifndef NDEBUG
375 debug = get_script_debug_mode();
376#endif
377
378 if(!create_compilation_job(ctx, "engine", debug).get())
379 {
380 return false;
381 }
382 }
383
384 domain_ = std::make_unique<dotnet::domain>("Unravel.Engine");
385 dotnet::domain::set_current_domain(domain_.get());
386
387 auto engine_script_lib = fs::resolve_protocol(get_lib_compiled_key("engine"));
388 auto engine_script_lib_temp = fs::resolve_protocol(get_lib_temp_compiled_key("engine"));
389
390 copy_compiled_lib(engine_script_lib_temp, engine_script_lib);
391
392 auto assembly = domain_->get_assembly(engine_script_lib.string());
393 // print_assembly_info(assembly);
394
395 APPLOG_TRACE("-------------------------------------------------------------");
396 APPLOG_TRACE("Loading domain {} with version: {}", domain_->get_name(), domain_->get_version());
397 APPLOG_TRACE("-------------------------------------------------------------");
398
399 cache_.update_manager_type = assembly.get_type("Unravel.Core", "SystemManager");
400 cache_.component_type = assembly.get_type("Unravel.Core", "Component");
401 cache_.script_component_type = assembly.get_type("Unravel.Core", "ScriptComponent");
402 cache_.ui_event_manager_type = assembly.get_type("Unravel.Core", "UIEventManager");
403
404 // Cache methods once per engine domain (avoid per-call name lookup).
405 cache_.update_method = cache_.update_manager_type.get_method("internal_n2m_update", 1);
406 cache_.fixed_update_method = cache_.update_manager_type.get_method("internal_n2m_fixed_update", 1);
407 cache_.late_update_method = cache_.update_manager_type.get_method("internal_n2m_late_update", 0);
408
409 cache_.set_entity_method = cache_.component_type.get_method("internal_n2m_set_entity", 1);
410 cache_.on_create_method = cache_.script_component_type.get_method("internal_n2m_on_create", 0);
411 cache_.on_enable_method = cache_.script_component_type.get_method("internal_n2m_on_enable", 0);
412 cache_.on_disable_method = cache_.script_component_type.get_method("internal_n2m_on_disable", 0);
413 cache_.on_start_method = cache_.script_component_type.get_method("internal_n2m_on_start", 0);
414 cache_.on_destroy_method = cache_.script_component_type.get_method("internal_n2m_on_destroy", 0);
415 cache_.on_sensor_enter_method = cache_.script_component_type.get_method("internal_n2m_on_sensor_enter", 2);
416 cache_.on_sensor_exit_method = cache_.script_component_type.get_method("internal_n2m_on_sensor_exit", 2);
417 cache_.on_collision_enter_method =
418 cache_.script_component_type.get_method("internal_n2m_on_collision_enter", 2);
419 cache_.on_collision_exit_method =
420 cache_.script_component_type.get_method("internal_n2m_on_collision_exit", 2);
421
422 cache_.ui_dispatch_event_method =
423 cache_.ui_event_manager_type.get_method("InternalDispatchEvent", 1);
424
425 return true;
426}
428{
429 APPLOG_TRACE_PERF_NAMED(std::chrono::milliseconds, "Unload Engine Domain");
430 cache_ = {};
431 if(domain_)
432 {
433 APPLOG_TRACE("-------------------------------------------------------------");
434 APPLOG_TRACE("Unloading domain {} with version: {}", domain_->get_name(), domain_->get_version());
435 APPLOG_TRACE("-------------------------------------------------------------");
436 }
437 domain_.reset();
438 dotnet::domain::set_current_domain(nullptr);
439}
440
441auto script_system::load_app_domain(rtti::context& ctx, bool recompile) -> bool
442{
443 APPLOG_TRACE_PERF_NAMED(std::chrono::milliseconds, "Load App Domain");
444
445 bool is_deploy_mode = ctx.has<deploy>();
446
447 bool result = true;
448
449 if(!is_deploy_mode && recompile)
450 {
451 result &= create_compilation_job(ctx, "app", get_script_debug_mode()).get();
452
453 has_compilation_errors_ = !result;
454 }
455
456 app_domain_ = std::make_unique<dotnet::domain>("Unravel.App");
457 dotnet::domain::set_current_domain(app_domain_.get());
458
459 auto app_script_lib = fs::resolve_protocol(get_lib_compiled_key("app"));
460 auto app_script_lib_temp = fs::resolve_protocol(get_lib_temp_compiled_key("app"));
461
462 copy_compiled_lib(app_script_lib_temp, app_script_lib);
463
464 if(!is_deploy_mode)
465 {
466 auto& am = ctx.get_cached<asset_manager>();
467 auto assets = am.get_assets<script>("app");
468 // assets include the empty asset
469 if(assets.size() <= 1)
470 {
471 return result;
472 }
473 }
474
475 fs::error_code ec;
476 if(fs::exists(app_script_lib, ec))
477 {
478 APPLOG_TRACE("-------------------------------------------------------------");
479 APPLOG_TRACE("Loading domain {} with version: {}", app_domain_->get_name(), app_domain_->get_version());
480 APPLOG_TRACE("-------------------------------------------------------------");
481 try
482 {
483 auto assembly = app_domain_->get_assembly(app_script_lib.string());
484 // print_assembly_info(assembly);
485
486 app_cache_.scriptable_component_types.clear();
487 if(!has_compilation_errors_)
488 {
489 app_cache_.scriptable_component_types = assembly.get_types_derived_from(get_scriptable_component_base_type());
490
491 // Same-named types in the engine and app assemblies are two
492 // distinct .NET types - name-based lookups resolve app-first,
493 // so make the shadowing visible instead of silent.
494 auto engine_assembly = get_engine_assembly();
495 for(const auto& type : app_cache_.scriptable_component_types)
496 {
497 auto fullname = type.get_fullname();
498 if(engine_assembly.get_type(fullname).valid())
499 {
500 APPLOG_WARNING("Script type '{}' is defined in both the engine and the app scripts. "
501 "The app version will be used; rename it to avoid ambiguity.",
502 fullname);
503 }
504 }
505 }
506 }
507 catch(const dotnet::exception& e)
508 {
509 log_exception(e);
510 result = false;
511 }
512 }
513
514
515 return result;
516}
518{
519 APPLOG_TRACE_PERF_NAMED(std::chrono::milliseconds, "Unload App Domain");
520
521 app_cache_ = {};
522
523 if(app_domain_)
524 {
525 APPLOG_TRACE("-------------------------------------------------------------");
526 APPLOG_TRACE("Unloading domain {} with version: {}", app_domain_->get_name(), app_domain_->get_version());
527 APPLOG_TRACE("-------------------------------------------------------------");
528 }
529 app_domain_.reset();
530 dotnet::domain::set_current_domain(domain_.get());
531}
532
533void script_system::on_create_component(entt::registry& r, entt::entity e)
534{
535}
536void script_system::on_load_component(entt::registry& r, entt::entity e)
537{
538
539}
540void script_system::on_destroy_component(entt::registry& r, entt::entity e)
541{
542 auto& comp = r.get<script_component>(e);
543 comp.destroy();
544}
545
546void script_system::on_create_active_component(entt::registry& r, entt::entity e)
547{
548 if(auto comp = r.try_get<script_component>(e))
549 {
550 comp->enable();
551 }
552}
553void script_system::on_destroy_active_component(entt::registry& r, entt::entity e)
554{
555 if(auto comp = r.try_get<script_component>(e))
556 {
557 comp->disable();
558 }
559}
560
561void script_system::on_play_begin(hpp::span<const entt::handle> entities)
562{
563 APP_SCOPE_PERF("Script/On Play Begin");
564 if(!app_domain_ || !domain_)
565 {
566 return;
567 }
568 try
569 {
570 create_call_ = call_progress::started;
571
572 {
573 APP_SCOPE_PERF("Script/On Play Begin Create");
574 for(auto entity : entities)
575 {
576 if(auto comp = entity.try_get<script_component>())
577 {
578 comp->create();
579 }
580 }
581 }
582
583 create_call_ = call_progress::finished;
584
585 {
586 APP_SCOPE_PERF("Script/On Play Begin Enable");
587 for(auto entity : entities)
588 {
589 if(auto comp = entity.try_get<script_component>())
590 {
591 if(entity.all_of<active_component>())
592 {
593 comp->enable();
594 }
595 else
596 {
597 comp->disable();
598 }
599 }
600 }
601 }
602 }
603 catch(const dotnet::exception& e)
604 {
605 log_exception(e);
606 }
607}
608
609void script_system::on_play_begin(entt::registry& entities)
610{
611 APP_SCOPE_PERF("Script/On Play Begin Scene");
612 if(!app_domain_ || !domain_)
613 {
614 return;
615 }
616 try
617 {
618 create_call_ = call_progress::started;
619
620 {
621 APP_SCOPE_PERF("Script/On Play Begin Scene Create");
622 entities.view<script_component>().each(
623 [&](auto e, auto&& comp)
624 {
625 comp.create();
626 });
627 }
628
629 create_call_ = call_progress::finished;
630
631 {
632 APP_SCOPE_PERF("Script/On Play Begin Scene Enable");
633 entities.view<script_component>().each(
634 [&](auto e, auto&& comp)
635 {
636 if(entities.all_of<active_component>(e))
637 {
638 comp.enable();
639 }
640 else
641 {
642 comp.disable();
643 }
644 });
645 }
646 }
647 catch(const dotnet::exception& e)
648 {
649 log_exception(e);
650 }
651}
652
654{
655 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
656
657 if(!app_domain_ || !domain_)
658 {
659 return;
660 }
661
662 auto& ec = ctx.get_cached<ecs>();
663 auto& scn = ec.get_scene();
664 auto& registry = *scn.registry;
665
666 registry.on_construct<script_component>().connect<&on_create_component>();
667 registry.on_destroy<script_component>().connect<&on_destroy_component>();
668 on_load<script_component>(registry).connect<&on_load_component>();
669
670 registry.on_construct<active_component>().connect<&on_create_active_component>();
671 registry.on_destroy<active_component>().connect<&on_destroy_active_component>();
672
673 on_play_begin(registry);
674
675 elapsed_time_ = {};
676}
677
678void script_system::on_play_end(rtti::context& ctx)
679{
680 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
681
682 auto& ec = ctx.get_cached<ecs>();
683 auto& scn = ec.get_scene();
684 auto& registry = *scn.registry;
685
686 seq::scope::stop_all("script");
687
688 // try
689 // {
690 // registry.view<script_component>().each(
691 // [&](auto e, auto&& comp)
692 // {
693 // comp.destroy();
694 // });
695 // }
696 // catch(const dotnet::exception& e)
697 // {
698 // log_exception(e);
699 // }
700
701 registry.on_construct<active_component>().disconnect<&on_create_active_component>();
702 registry.on_destroy<active_component>().disconnect<&on_destroy_active_component>();
703
704 registry.on_construct<script_component>().disconnect<&on_create_component>();
705 registry.on_destroy<script_component>().disconnect<&on_destroy_component>();
706 on_load<script_component>(registry).disconnect<&on_load_component>();
707
708 elapsed_time_ = {};
709}
710
711void script_system::on_pause(rtti::context& ctx)
712{
713}
714
715void script_system::on_resume(rtti::context& ctx)
716{
717}
718
719void script_system::on_skip_next_frame(rtti::context& ctx)
720{
721 delta_t step(1.0f / 60.0f);
722 on_frame_update(ctx, step);
723}
724void script_system::on_frame_update(rtti::context& ctx, delta_t dt)
725{
726 APP_SCOPE_PERF("Script/System Update");
727
728 auto& play = ctx.get_cached<play_mode>();
729 if(!play.is_active())
730 {
731 check_for_recompile(ctx, dt, true);
732 }
733
734 is_updating_ = true;
735
736 try
737 {
738 if(!app_domain_ || !domain_)
739 {
740 return;
741 }
742
743 auto& ec = ctx.get_cached<ecs>();
744 auto& scn = ec.get_scene();
745 auto& registry = *scn.registry;
746
747 registry.view<script_component>().each(
748 [&](auto e, auto&& comp)
749 {
750 comp.process_pending_deletions();
751
752 if(play.is_simulation_running() && registry.all_of<active_component>(e))
753 {
754 comp.start();
755 }
756 });
757
758 struct update_data
759 {
760 float time{};
761 float delta_time{};
762 float time_scale{};
763 uint64_t frame_count{};
764 };
765
766 if(play.is_simulation_running() && !play.is_paused())
767 {
768 // Mid-frame enter_running: process() could not zero dt. Start()
769 // above still runs; skip integrating motion on this first frame.
770 if(play.frames_running() == 0)
771 {
772 dt = {};
773 }
774
775 auto& sim = ctx.get_cached<simulation>();
776 auto time_scale = sim.get_time_scale();
777
778 update_data data;
779 data.time = elapsed_time_.count();
780 data.delta_time = dt.count();
781 data.time_scale = time_scale;
782 data.frame_count = sim.get_frame();
783
784 {
785 APP_SCOPE_PERF("Script/System Update Managed");
786 // Use cached method to avoid repeated allocations
787 auto method_thunk = dotnet::make_method_invoker<void(update_data)>(cache_.update_method);
788 method_thunk(data);
789 }
790
791 elapsed_time_ += dt;
792 }
793 }
794 catch(const dotnet::exception& e)
795 {
796 log_exception(e);
797 }
798 is_updating_ = false;
799}
800
801void script_system::on_frame_fixed_update(rtti::context& ctx, delta_t dt)
802{
803 APP_SCOPE_PERF("Script/System Fixed Update");
804
805 auto& play = ctx.get_cached<play_mode>();
806
807 try
808 {
809 if(!app_domain_ || !domain_)
810 {
811 return;
812 }
813
814 auto& ec = ctx.get_cached<ecs>();
815 auto& scn = ec.get_scene();
816 auto& registry = *scn.registry;
817
818 registry.view<script_component>().each(
819 [&](auto e, auto&& comp)
820 {
821 comp.process_pending_deletions();
822 });
823
824 struct update_data
825 {
826 float fixed_delta_time{};
827 };
828
829 if(play.is_simulation_running() && play.frames_running() > 0 && dt > delta_t::zero())
830 {
831 update_data data;
832 data.fixed_delta_time = dt.count();
833
834 {
835 APP_SCOPE_PERF("Script/System Fixed Update Managed");
836 // Use cached method to avoid repeated allocations
837 auto method_thunk = dotnet::make_method_invoker<void(update_data)>(cache_.fixed_update_method);
838 method_thunk(data);
839 }
840 }
841 }
842 catch(const dotnet::exception& e)
843 {
844 log_exception(e);
845 }
846}
847
848void script_system::on_frame_late_update(rtti::context& ctx, delta_t dt)
849{
850 {
851 APP_SCOPE_PERF("Script/System Late Update");
852
853 auto& play = ctx.get_cached<play_mode>();
854
855 try
856 {
857 if(!app_domain_ || !domain_)
858 {
859 return;
860 }
861
862 auto& ec = ctx.get_cached<ecs>();
863 auto& scn = ec.get_scene();
864 auto& registry = *scn.registry;
865
866 if(play.is_simulation_running() && play.frames_running() > 0 && dt > delta_t::zero())
867 {
868 APP_SCOPE_PERF("Script/System Late Update Managed");
869 // Use cached method to avoid repeated allocations
870 auto method_thunk = dotnet::make_method_invoker<void()>(cache_.late_update_method);
871 method_thunk();
872 }
873 }
874 catch(const dotnet::exception& e)
875 {
876 log_exception(e);
877 }
878
879 }
880
881 {
882 APP_SCOPE_PERF("Script/System Cleanup");
883 dt = std::max(delta_t::zero(), dt);
884
885 delta_t secs(dt);
886 seq::update(secs);
887 }
888
889
890}
891
892auto script_system::get_all_scriptable_components() const -> const std::vector<dotnet::type>&
893{
894 return app_cache_.scriptable_component_types;
895}
896
898{
899 return cache_.script_component_type;
900}
901
902auto script_system::get_engine_assembly() const -> dotnet::assembly
903{
904 auto engine_script_lib = fs::resolve_protocol(get_lib_compiled_key("engine"));
905 return domain_->get_assembly(engine_script_lib.string());
906}
907
908auto script_system::get_app_assembly() const -> dotnet::assembly
909{
910 auto app_script_lib = fs::resolve_protocol(get_lib_compiled_key("app"));
911 return app_domain_->get_assembly(app_script_lib.string());
912}
913
914auto script_system::get_type_by_fullname(const std::string& fullname) const -> dotnet::type
915{
916 // App types take precedence so user code shadows engine-provided types
917 // (samples, templates) instead of silently binding to the engine copy.
918 dotnet::type type;
919 if(app_domain_)
920 {
921 type = app_domain_->get_type(fullname);
922 }
923 if(!type.valid() && domain_)
924 {
925 type = domain_->get_type(fullname);
926 }
927
928 return type;
929}
930
931auto script_system::get_type(const std::string& name_space, const std::string& name) const -> dotnet::type
932{
933 dotnet::type type;
934 if(app_domain_)
935 {
936 type = app_domain_->get_type(name_space, name);
937 }
938 if(!type.valid() && domain_)
939 {
940 type = domain_->get_type(name_space, name);
941 }
942 return type;
943}
944
946{
947 return create_call_ == call_progress::finished;
948}
950{
951 return is_updating_;
952}
953
955{
956 return dotnet::is_debugger_attached();
957}
958
959void script_system::check_for_recompile(rtti::context& ctx, delta_t dt, bool emit_callback)
960{
961 time_since_last_check_ += dt;
962
963 if(time_since_last_check_ >= check_interval || needs_recompile == recompile_command::compile_now)
964 {
965 time_since_last_check_ = {};
966
967 recompile_command should_recompile = needs_recompile.exchange(recompile_command::none);
968
969 if(should_recompile != recompile_command::none)
970 {
971 auto container = []()
972 {
973 std::lock_guard<std::mutex> lock(container_mutex);
974 auto result = std::move(needs_to_recompile);
975 return result;
976 }();
977
978 compilation_jobs_.clear();
979
980 compilation_version++;
981
982 auto current_version = compilation_version.load();
983 for(const auto& protocol : container)
984 {
985 auto job = create_compilation_job(ctx, protocol, get_script_debug_mode())
986 .then(tpp::this_thread::get_id(),
987 [this, &ctx, protocol, emit_callback, current_version](auto f)
988 {
989 if(!emit_callback)
990 {
991 return;
992 }
993 auto& play = ctx.get_cached<play_mode>();
994 auto& ev = ctx.get_cached<events>();
995 if(play.is_simulation_running())
996 {
997 return;
998 }
999
1000 if(compilation_version > current_version)
1001 {
1002 return;
1003 }
1004
1005 has_compilation_errors_ = !f.get();
1006 if(!has_compilation_errors_)
1007 {
1008 ev.on_script_recompile(ctx, protocol, current_version);
1009 }
1010 });
1011
1012 compilation_jobs_.emplace_back(std::move(job));
1013 }
1014 }
1015 }
1016}
1017
1019{
1020 APPLOG_TRACE("Waiting for script compilation...");
1021
1022 check_for_recompile(ctx, 100s, false);
1023
1024 auto jobs = std::move(compilation_jobs_);
1025
1026 for(auto& job : jobs)
1027 {
1028 job.wait();
1029 }
1030}
1031
1032auto script_system::create_compilation_job(rtti::context& ctx,
1033 const std::string& protocol,
1034 bool debug) -> tpp::job_future<bool>
1035{
1036 uint32_t flags = 0;
1037 if(debug)
1038 {
1040 }
1041
1042 auto& thr = ctx.get_cached<threader>();
1043 auto& am = ctx.get_cached<asset_manager>();
1044
1045 return thr.pool->schedule(
1046 "Compiling " + ex::get_type<script_library>(),
1047 [&am, flags, protocol]()
1048 {
1049 auto key = get_lib_data_key(protocol);
1050 auto output = get_lib_temp_compiled_key(protocol);
1051
1052 return asset_compiler::compile<script_library>(am, key, fs::resolve_protocol(output), flags);
1053 });
1054}
1055void script_system::set_needs_recompile(const std::string& protocol, bool now)
1056{
1057 if(!initted)
1058 {
1059 return;
1060 }
1061 needs_recompile = now ? recompile_command::compile_now : recompile_command::compile_at_schedule;
1062 {
1063 std::lock_guard<std::mutex> lock(container_mutex);
1064 if(std::find(std::begin(needs_to_recompile), std::end(needs_to_recompile), protocol) ==
1065 std::end(needs_to_recompile))
1066 {
1067 needs_to_recompile.emplace_back(protocol);
1068 }
1069 }
1070}
1071
1073{
1074 return debug_mode;
1075}
1076
1078{
1079 debug_mode = debug;
1080}
1081
1082auto script_system::get_lib_name(const std::string& protocol) -> std::string
1083{
1084 return protocol + "-script.dll";
1085}
1086
1087auto script_system::get_lib_data_key(const std::string& protocol) -> std::string
1088{
1089 std::string output = get_lib_name(protocol + ex::get_data_directory() + "/" + protocol);
1090 return output;
1091}
1092
1093auto script_system::get_lib_temp_compiled_key(const std::string& protocol) -> std::string
1094{
1095 std::string output = get_lib_name(protocol + ex::get_compiled_directory() + "/temp-" + protocol);
1096 return output;
1097}
1098
1099auto script_system::get_lib_compiled_key(const std::string& protocol) -> std::string
1100{
1101 std::string output = get_lib_name(protocol + ex::get_compiled_directory() + "/" + protocol);
1102 return output;
1103}
1104
1105void script_system::on_sensor_enter(entt::handle sensor, entt::handle other, const std::vector<manifold_point>& manifolds)
1106{
1107 if(!other || !sensor)
1108 {
1109 return;
1110 }
1111 auto comp = sensor.try_get<script_component>();
1112 if(!comp)
1113 {
1114 return;
1115 }
1116
1117 try
1118 {
1119 comp->on_sensor_enter(other, manifolds);
1120 }
1121 catch(const dotnet::exception& e)
1122 {
1123 log_exception(e);
1124 }
1125}
1126
1127void script_system::on_sensor_exit(entt::handle sensor, entt::handle other, const std::vector<manifold_point>& manifolds)
1128{
1129 if(!other || !sensor)
1130 {
1131 return;
1132 }
1133
1134 auto comp = sensor.try_get<script_component>();
1135 if(!comp)
1136 {
1137 return;
1138 }
1139
1140 try
1141 {
1142 comp->on_sensor_exit(other, manifolds);
1143 }
1144 catch(const dotnet::exception& e)
1145 {
1146 log_exception(e);
1147 }
1148}
1149
1150void script_system::on_collision_enter(entt::handle a, entt::handle b, const std::vector<manifold_point>& manifolds)
1151{
1152 if(!a || !b)
1153 {
1154 return;
1155 }
1156
1157 try
1158 {
1159 {
1160 auto comp = a.try_get<script_component>();
1161 if(comp)
1162 {
1163 comp->on_collision_enter(b, manifolds, true);
1164 }
1165 }
1166
1167 {
1168 auto comp = b.try_get<script_component>();
1169 if(comp)
1170 {
1171 comp->on_collision_enter(a, manifolds, false);
1172 }
1173 }
1174 }
1175 catch(const dotnet::exception& e)
1176 {
1177 log_exception(e);
1178 }
1179}
1180
1181void script_system::on_collision_exit(entt::handle a, entt::handle b, const std::vector<manifold_point>& manifolds)
1182{
1183 if(!a || !b)
1184 {
1185 return;
1186 }
1187
1188 try
1189 {
1190 {
1191 auto comp = a.try_get<script_component>();
1192 if(comp)
1193 {
1194 comp->on_collision_exit(b, manifolds, true);
1195 }
1196 }
1197
1198 {
1199 auto comp = b.try_get<script_component>();
1200 if(comp)
1201 {
1202 comp->on_collision_exit(a, manifolds, false);
1203 }
1204 }
1205 }
1206 catch(const dotnet::exception& e)
1207 {
1208 log_exception(e);
1209 }
1210}
1211
1213{
1214 return has_compilation_errors_;
1215}
1216
1217} // namespace unravel
entt::handle b
entt::handle a
void set_optional(const std::string &name, const std::string &alternative, T defaultValue, const std::string &description="", bool dominant=false)
Definition parser.h:303
Manages assets, including loading, unloading, and storage.
auto get_assets(const std::string &group={}) const -> std::vector< asset_handle< T > >
Gets all assets in a specified group.
Class that contains core data for audio listeners. There can only be one instance of it per scene.
std::chrono::duration< float > delta_t
uint32_t frame
Definition graphics.cpp:23
bool initted
Definition graphics.cpp:22
std::string name
Definition hub.cpp:33
#define APPLOG_WARNING(...)
Definition logging.h:19
#define APPLOG_ERROR(...)
Definition logging.h:20
#define APPLOG_INFO(...)
Definition logging.h:18
#define APPLOG_TRACE_PERF_NAMED(T, name)
Definition logging.h:120
#define APPLOG_ERROR_LOC(FILE_LOC, LINE_LOC, FUNC_LOC,...)
Definition logging.h:38
#define APPLOG_TRACE(...)
Definition logging.h:17
texture_job_type type
auto get_data_directory(const std::string &prefix={}) -> std::string
auto get_type() -> const std::string &
auto get_compiled_directory(const std::string &prefix={}) -> 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 ...
void stop_all(const std::string &scope)
Stops all actions within the specified scope.
Definition seq.cpp:160
void update(seq_id_t id, duration_t delta)
Updates the elapsed duration of a specific action.
Definition seq.cpp:100
Hash specialization for batch_key to enable use in std::unordered_map.
auto compile< script_library >(asset_manager &am, const fs::path &key, const fs::path &output, uint32_t flags) -> bool
auto on_load(entt::registry &reg) -> typename on_load_bus< T >::sink_t
Definition scene.h:27
auto validate_paths(const dotnet::compiler_paths &paths, bool is_deploy_mode) -> bool
auto select_interpreter_config(const cmd_line::parser &parser) -> dotnet::interpreter_config
emitter_sim_state sim
#define APP_SCOPE_PERF(name_literal)
Create a scoped performance timer that records to the timeline profiler. Only accepts string literals...
Definition profiler.h:675
entt::handle entity
auto get_cached() -> T &
Definition context.hpp:49
Manages the entity-component-system (ECS) operations for the ACE framework.
Definition ecs.h:12
auto get_scene() -> scene &
Gets the current scene.
Definition ecs.cpp:30
hpp::event< void(rtti::context &, delta_t)> on_frame_update
Definition events.h:16
static auto is_debugger_attached() -> bool
static auto get_lib_name(const std::string &protocol) -> std::string
auto get_scriptable_component_base_type() const -> dotnet::type
static void on_load_component(entt::registry &r, entt::entity e)
Called when a script component is loaded.
auto is_create_called() const -> bool
auto get_type(const std::string &name_space, const std::string &name) const -> dotnet::type
static auto get_lib_data_key(const std::string &protocol) -> std::string
static auto get_script_debug_mode() -> bool
auto is_update_called() const -> bool
static auto find_dotnet_paths(const rtti::context &ctx) -> dotnet::compiler_paths
script_system(rtti::context &ctx, cmd_line::parser &parser)
static auto get_lib_compiled_key(const std::string &protocol) -> std::string
static auto get_lib_temp_compiled_key(const std::string &protocol) -> std::string
static void set_script_debug_mode(bool debug)
auto load_engine_domain(rtti::context &ctx, bool recompile) -> bool
auto has_compilation_errors() const -> bool
void on_collision_enter(entt::handle a, entt::handle b, const std::vector< manifold_point > &manifolds)
auto get_app_assembly() const -> dotnet::assembly
static void on_create_component(entt::registry &r, entt::entity e)
Called when a physics component is created.
static void set_needs_recompile(const std::string &protocol, bool now=false)
void set_debug_config(const std::string &address, uint32_t port, uint32_t loglevel)
static void copy_compiled_lib(const fs::path &from, const fs::path &to)
static void on_destroy_active_component(entt::registry &r, entt::entity e)
auto load_app_domain(rtti::context &ctx, bool recompile) -> bool
auto get_all_scriptable_components() const -> const std::vector< dotnet::type > &
static void on_create_active_component(entt::registry &r, entt::entity e)
auto get_type_by_fullname(const std::string &fullname) const -> dotnet::type
static void log_exception(const dotnet::exception &e, const hpp::source_location &loc=hpp::source_location::current())
void on_play_begin(rtti::context &ctx)
Called when playback begins.
auto deinit(rtti::context &ctx) -> bool
auto get_engine_assembly() const -> dotnet::assembly
void on_collision_exit(entt::handle a, entt::handle b, const std::vector< manifold_point > &manifolds)
void wait_for_jobs_to_finish(rtti::context &ctx)
void on_sensor_enter(entt::handle sensor, entt::handle other, const std::vector< manifold_point > &manifolds)
void on_sensor_exit(entt::handle sensor, entt::handle other, const std::vector< manifold_point > &manifolds)
static void on_destroy_component(entt::registry &r, entt::entity e)
Called when a physics component is destroyed.
auto init(rtti::context &ctx, const cmd_line::parser &parser) -> bool
std::vector< GitHubAsset > assets