diff --git a/.gitignore b/.gitignore index 5b0774f..6096e40 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ tools/binding_generator/binding_statistics_report.md .cache project/leanclr/live_reload.txt +project/deploy/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 712727d..13c8935 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,11 +110,16 @@ else() -Wl,-dead_strip -Wl,-exported_symbols_list,${LEANCLR_GODOT_EXPORTED_SYMBOLS} ) - elseif(UNIX) + elseif(UNIX AND NOT EMSCRIPTEN) target_link_options(leanclr_godot PRIVATE -Wl,--gc-sections) endif() endif() +if(EMSCRIPTEN) + target_link_options(leanclr_godot PRIVATE -sSIDE_MODULE=1) + set_target_properties(leanclr_godot PROPERTIES SUFFIX ".wasm") +endif() + set_target_properties(leanclr_godot PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES diff --git a/project/export_presets.cfg b/project/export_presets.cfg new file mode 100644 index 0000000..e155df1 --- /dev/null +++ b/project/export_presets.cfg @@ -0,0 +1,52 @@ +[preset.0] + +name="Web" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="leanclr/*.dll,leanclr/live_reload.txt" +exclude_filter="" +export_path="deploy/html/index.html" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +custom_template/debug="/Volumes/External/Misc/templates_4.6/web_dlink_nothreads_debug.zip" +custom_template/release="/Volumes/External/Misc/templates_4.6/web_dlink_nothreads_release.zip" +variant/extensions_support=true +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/ensure_cross_origin_isolation_headers=true +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 +dotnet/include_scripts_content=false +dotnet/include_debug_symbols=true +dotnet/embed_build_outputs=false diff --git a/project/leanclr.gdextension b/project/leanclr.gdextension index 0bd8cb4..05e56f3 100644 --- a/project/leanclr.gdextension +++ b/project/leanclr.gdextension @@ -12,3 +12,4 @@ linux.debug.x86_64 = "res://bin/Debug/libleanclr_godot.so" linux.release.x86_64 = "res://bin/Release/libleanclr_godot.so" macos.debug = "res://bin/Debug/libleanclr_godot.dylib" macos.release = "res://bin/Release/libleanclr_godot.dylib" +web.release.wasm32 = "res://bin/Release/libleanclr_godot.wasm" diff --git a/project/runtime_hot_reload_demo.tscn b/project/runtime_hot_reload_demo.tscn index 3b17bdd..9aa5904 100644 --- a/project/runtime_hot_reload_demo.tscn +++ b/project/runtime_hot_reload_demo.tscn @@ -74,5 +74,8 @@ Name = &"FlappyScript" [node name="HotReloadInputRelay" type="Node" parent="." unique_id=394620894] script = ExtResource("4_input_relay") +attached_assembly_name = "Game" +reload_type_name = "Game.HotReloadSmoke" +script_owner_path = NodePath("../FlappyScript") [node name="RuntimeCSharpEditor" parent="." unique_id=56462960 instance=ExtResource("3_editor_scene")] diff --git a/project/scripts/HotReloadInputRelay.gd b/project/scripts/HotReloadInputRelay.gd index e86f68e..714fe5e 100644 --- a/project/scripts/HotReloadInputRelay.gd +++ b/project/scripts/HotReloadInputRelay.gd @@ -1,10 +1,46 @@ extends Node +@export var marker_path := "res://leanclr/live_reload.txt" +@export var attached_assembly_name := "" +@export var reload_type_name := "" +@export var script_owner_path: NodePath +@export var reload_poll_seconds := 0.25 + @onready var hot_reload_host: Node = get_node("../LiveHotReloadHost") +var _elapsed := 0.0 + func _ready() -> void: + if hot_reload_host != null: + hot_reload_host.set_script_owner_path(script_owner_path) + reload_from_marker() + set_process(reload_poll_seconds > 0.0) set_process_input(true) +func _process(delta: float) -> void: + _elapsed += delta + if _elapsed >= reload_poll_seconds: + _elapsed = 0.0 + reload_from_marker() + func _input(event: InputEvent) -> void: if hot_reload_host != null: hot_reload_host.forward_input(event) + +func reload_from_marker() -> void: + if hot_reload_host == null: + return + + var assembly_name := attached_assembly_name + if marker_path != "" and FileAccess.file_exists(marker_path): + assembly_name = FileAccess.get_file_as_string(marker_path).strip_edges() + if assembly_name == "": + assembly_name = attached_assembly_name + + if assembly_name == "" or assembly_name == hot_reload_host.get_loaded_assembly_name(): + return + + if assembly_name == attached_assembly_name: + hot_reload_host.use_attached_script(assembly_name) + else: + hot_reload_host.reload_assembly(assembly_name, reload_type_name) diff --git a/project/scripts/RuntimeCSharpEditor.gd b/project/scripts/RuntimeCSharpEditor.gd index a0cf5e1..9b20d49 100644 --- a/project/scripts/RuntimeCSharpEditor.gd +++ b/project/scripts/RuntimeCSharpEditor.gd @@ -21,6 +21,11 @@ func _ready() -> void: run_button.pressed.connect(compile_and_reload) show() + if OS.has_feature("web"): + run_button.disabled = true + _set_status("Web export is read-only; runtime build is desktop only.") + return + if OS.has_environment(AUTORUN_ENVIRONMENT) and not autorun_started: autorun_started = true var code := editor.text @@ -69,6 +74,9 @@ func compile_and_reload() -> void: if editor == null: _set_status("Editor is not ready.") return + if OS.has_feature("web"): + _set_status("Web export is read-only; runtime build is desktop only.") + return if not _write_text_file(EDIT_SOURCE_PATH, editor.text): _set_status("Failed to write HotReloadSmoke.cs") diff --git a/src/leanclr_hot_reload_host.cpp b/src/leanclr_hot_reload_host.cpp index 0ee1dcc..a00cdba 100644 --- a/src/leanclr_hot_reload_host.cpp +++ b/src/leanclr_hot_reload_host.cpp @@ -3,7 +3,6 @@ #include "leanclr_runtime_bridge.h" #include -#include #include #include #include @@ -17,9 +16,6 @@ namespace godot namespace { -const char* RELOAD_MARKER_PATH = "res://leanclr/live_reload.txt"; -const char* RELOAD_TYPE_NAME = "Game.HotReloadSmoke"; - bool is_editor_scene_context() { Engine* engine = Engine::get_singleton(); @@ -43,6 +39,13 @@ bool is_editor_scene_context() void LeanCLRHotReloadHost::_bind_methods() { ClassDB::bind_method(D_METHOD("forward_input", "event"), &LeanCLRHotReloadHost::forward_input); + ClassDB::bind_method(D_METHOD("use_attached_script", "assembly_name"), &LeanCLRHotReloadHost::use_attached_script); + ClassDB::bind_method(D_METHOD("reload_assembly", "assembly_name", "type_name"), &LeanCLRHotReloadHost::reload_assembly); + ClassDB::bind_method(D_METHOD("set_script_owner_path", "script_owner_path"), &LeanCLRHotReloadHost::set_script_owner_path); + ClassDB::bind_method(D_METHOD("get_script_owner_path"), &LeanCLRHotReloadHost::get_script_owner_path); + ClassDB::bind_method(D_METHOD("get_loaded_assembly_name"), &LeanCLRHotReloadHost::get_loaded_assembly_name); + + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "script_owner_path"), "set_script_owner_path", "get_script_owner_path"); } LeanCLRHotReloadHost::~LeanCLRHotReloadHost() @@ -55,18 +58,17 @@ void LeanCLRHotReloadHost::_notification(int p_what) { if (p_what == NOTIFICATION_READY) { - if (is_editor_scene_context() || !is_inside_tree() || (get_tree() != nullptr && get_tree()->get_edited_scene_root() != nullptr)) + if (should_skip_runtime()) { set_process(false); return; } set_process(true); - check_reload_marker(); } else if (p_what == NOTIFICATION_PROCESS) { - if (is_editor_scene_context() || !is_inside_tree() || (get_tree() != nullptr && get_tree()->get_edited_scene_root() != nullptr)) + if (should_skip_runtime()) { return; } @@ -76,80 +78,96 @@ void LeanCLRHotReloadHost::_notification(int p_what) { LeanCLRRuntimeBridge::invoke_script_process(managed_object, delta); } - - elapsed += delta; - if (elapsed >= 0.25) - { - elapsed = 0.0; - check_reload_marker(); - } } } void LeanCLRHotReloadHost::forward_input(const Ref& p_event) { - if (managed_object == nullptr || !p_event.is_valid() || is_editor_scene_context() || !is_inside_tree() || - (get_tree() != nullptr && get_tree()->get_edited_scene_root() != nullptr)) + if (!p_event.is_valid() || should_skip_runtime()) { return; } - LeanCLRRuntimeBridge::invoke_script_method(managed_object, "_Input", p_event.ptr()); + void* target_object = get_active_script_object(); + if (target_object != nullptr) + { + LeanCLRRuntimeBridge::invoke_script_method(target_object, "_Input", p_event.ptr()); + } } -void LeanCLRHotReloadHost::check_reload_marker() +void LeanCLRHotReloadHost::use_attached_script(const String& p_assembly_name) { - String assembly_name = "Game"; - if (FileAccess::file_exists(RELOAD_MARKER_PATH)) + LeanCLRRuntimeBridge::release_script_object(managed_object); + managed_object = nullptr; + loaded_assembly_name = p_assembly_name; + UtilityFunctions::print("LeanCLR live reload: using attached script assembly = ", loaded_assembly_name); +} + +void LeanCLRHotReloadHost::set_script_owner_path(const NodePath& p_script_owner_path) +{ + script_owner_path = p_script_owner_path; +} + +NodePath LeanCLRHotReloadHost::get_script_owner_path() const +{ + return script_owner_path; +} + +String LeanCLRHotReloadHost::get_loaded_assembly_name() const +{ + return loaded_assembly_name; +} + +bool LeanCLRHotReloadHost::should_skip_runtime() const +{ + return is_editor_scene_context() || !is_inside_tree() || (get_tree() != nullptr && get_tree()->get_edited_scene_root() != nullptr); +} + +Object* LeanCLRHotReloadHost::get_script_owner() const +{ + if (!script_owner_path.is_empty()) { - assembly_name = FileAccess::get_file_as_string(RELOAD_MARKER_PATH).strip_edges(); - if (assembly_name.is_empty()) + Node* owner = get_node_or_null(script_owner_path); + if (owner != nullptr) { - assembly_name = "Game"; + return owner; } } - if (assembly_name == loaded_assembly_name) - { - return; - } - - reload_managed_object(assembly_name); + return const_cast(this); } -void LeanCLRHotReloadHost::reload_managed_object(const String& p_assembly_name) +void* LeanCLRHotReloadHost::get_attached_script_object() const { - if (p_assembly_name == String("Game")) + Object* owner = get_script_owner(); + return owner != nullptr ? LeanCLRRuntimeBridge::get_script_object_for_owner(owner) : nullptr; +} + +void* LeanCLRHotReloadHost::get_active_script_object() const +{ + return managed_object != nullptr ? managed_object : get_attached_script_object(); +} + +bool LeanCLRHotReloadHost::reload_assembly(const String& p_assembly_name, const String& p_type_name) +{ + if (p_assembly_name.is_empty() || p_type_name.is_empty()) { - LeanCLRRuntimeBridge::release_script_object(managed_object); - managed_object = nullptr; - loaded_assembly_name = p_assembly_name; - UtilityFunctions::print("LeanCLR live reload: using attached script assembly = ", loaded_assembly_name); - return; + UtilityFunctions::printerr("LeanCLR live reload: assembly_name and type_name are required."); + return false; } - Object* owner = this; - Node* parent = get_parent(); - if (parent != nullptr) - { - Node* script_owner = parent->get_node_or_null(NodePath("FlappyScript")); - if (script_owner != nullptr) - { - owner = script_owner; - } - } - - void* previous_object = managed_object != nullptr ? managed_object : LeanCLRRuntimeBridge::get_script_object_for_owner(owner); + Object* owner = get_script_owner(); + void* previous_object = get_active_script_object(); Variant custom_state; const bool has_custom_state = previous_object != nullptr && LeanCLRRuntimeBridge::has_script_method(previous_object, "CaptureHotReloadState", 0) && LeanCLRRuntimeBridge::invoke_script_method(previous_object, "CaptureHotReloadState", nullptr, 0, &custom_state); - void* next_object = LeanCLRRuntimeBridge::create_script_object(p_assembly_name, RELOAD_TYPE_NAME, owner); + void* next_object = LeanCLRRuntimeBridge::create_script_object(p_assembly_name, p_type_name, owner); if (next_object == nullptr) { UtilityFunctions::printerr("LeanCLR live reload: failed to load ", p_assembly_name, ": ", LeanCLRRuntimeBridge::get_last_error()); - return; + return false; } const int32_t migrated_fields = LeanCLRRuntimeBridge::migrate_script_state(previous_object, next_object); @@ -169,6 +187,7 @@ void LeanCLRHotReloadHost::reload_managed_object(const String& p_assembly_name) UtilityFunctions::print("LeanCLR live reload: migrated fields = ", migrated_fields); LeanCLRRuntimeBridge::invoke_script_ready(managed_object); LeanCLRRuntimeBridge::invoke_script_method(managed_object, "OnHotReloaded"); + return true; } } // namespace godot diff --git a/src/leanclr_hot_reload_host.h b/src/leanclr_hot_reload_host.h index c702728..2c6dd65 100644 --- a/src/leanclr_hot_reload_host.h +++ b/src/leanclr_hot_reload_host.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace godot @@ -14,18 +15,26 @@ class LeanCLRHotReloadHost : public Node public: ~LeanCLRHotReloadHost(); void forward_input(const Ref& p_event); + void use_attached_script(const String& p_assembly_name); + bool reload_assembly(const String& p_assembly_name, const String& p_type_name); + + void set_script_owner_path(const NodePath& p_script_owner_path); + NodePath get_script_owner_path() const; + String get_loaded_assembly_name() const; protected: static void _bind_methods(); void _notification(int p_what); private: - void check_reload_marker(); - void reload_managed_object(const String& p_assembly_name); + bool should_skip_runtime() const; + Object* get_script_owner() const; + void* get_attached_script_object() const; + void* get_active_script_object() const; + NodePath script_owner_path; void* managed_object = nullptr; String loaded_assembly_name; - double elapsed = 0.0; }; } // namespace godot diff --git a/src/leanclr_runtime_bridge.cpp b/src/leanclr_runtime_bridge.cpp index ce3b4c7..1cb8593 100644 --- a/src/leanclr_runtime_bridge.cpp +++ b/src/leanclr_runtime_bridge.cpp @@ -26,6 +26,7 @@ #include "vm/settings.h" #include "interp/eval_stack_op.h" +#include #include #include #include @@ -62,9 +63,19 @@ bool& godot_icalls_registered() return value; } -std::unordered_map& script_objects_by_owner() +struct GodotObjectPtrHasher { - static std::unordered_map* objects = new std::unordered_map; + std::size_t operator()(const Object* p_object) const noexcept + { + return static_cast(reinterpret_cast(p_object)); + } +}; + +using ScriptObjectByOwnerMap = std::unordered_map; + +ScriptObjectByOwnerMap& script_objects_by_owner() +{ + static ScriptObjectByOwnerMap* objects = new ScriptObjectByOwnerMap; return *objects; }