# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview LeanCLR-godot runs the [LeanCLR](https://github.com/focus-creative-games/leanclr) interpreter inside Godot 4 as a GDExtension, so C# scripts (.cs) can be loaded and hot-reloaded by Godot without a Mono build. The bridge lets a C# class extend `Godot.Node`, and the engine instantiates it through the interpreter at runtime. This is a hobby project (per the README) — not for production. Expect rough edges, no official bindings, and Godot 4.5+ as the target. ## Build & Run The README's steps are missing pieces. The full sequence: ```bash # 1. Pull the LeanCLR git submodule. git submodule update --init --recursive # 2. Configure (also fetches godot-cpp 4.5 via FetchContent on first run). cmake -S . -B build-master # 3. Generate the Godot API bindings. extension_api.json is shipped inside # the just-fetched godot-cpp tree, not at the repo root. python3 tools/binding_generator/generate_bindings.py \ --api build-master/_deps/godot-cpp-src/gdextension/extension_api.json # (on Windows use `python3` explicitly — `python` is aliased to the # Microsoft Store stub and will pop the installer UI) # 4. Re-configure now that src/generated/godot_api.generated.cpp exists, # then build the native GDExtension. cmake -S . -B build-master cmake --build build-master --config Debug --target leanclr_godot # 5. Build the managed C# facade and the demo (depends on step 4 + the # facade's own output). dotnet msbuild managed/GodotSharpCompat/GodotSharpCompat.csproj -p:Configuration=Debug dotnet msbuild project/Game.csproj -p:Configuration=Debug # 6. Run. "/d/Projects/godot-build/build/godot.exe" --path project # adjust to your Godot install ``` Outputs: - `project/bin//leanclr_godot.{dll,so,dylib,wasm}` — the GDExtension, loaded via `project/leanclr.gdextension`. - `project/leanclr/Game.dll`, `project/leanclr/GodotSharpCompat.dll` — the C# assemblies LeanCLR loads at runtime. - `project/leanclr/{mscorlib,System,System.Core,...}.dll` — LeanCLR's bundled .NET Framework 4.7.2 BCL, referenced by `project/Game.csproj` and `managed/GodotSharpCompat/GodotSharpCompat.csproj` via `HintPath`. They are not built here; they ship inside `thirdparty/leanclr/src/libraries/dotnetframework4.x-linux/`. `project/Game.csproj` and `managed/GodotSharpCompat/GodotSharpCompat.csproj` both use legacy .NET Framework v4.7.2 with `true` and `true` (they link against LeanCLR's bundled mscorlib/System, not the host BCL). They emit a single DLL straight into `project/leanclr/`. WASM build is supported (`-sSIDE_MODULE=1`, output `.wasm` in `project/bin/Release/`) — see CMakeLists.txt's `EMSCRIPTEN` branch. ## Two Source Patches Required for godot-cpp 4.5 The `GODOT_CPP_BRANCH` was bumped from 4.4 → 4.5 (uncommitted change in `CMakeLists.txt`). Two 4.5 API changes break the as-published code; both are fixed in this working tree: 1. **`ClassDB` was renamed to a `ClassDBSingleton` wrapper** in godot-cpp 4.5. The `CPP_HEADER_NAME_OVERRIDES["ClassDB"] = "class_db_singleton"` in `tools/binding_generator/generate_bindings.py` only renames the include path; the C++ class symbol in generated code is still `ClassDB` and breaks compilation. Fix: added a `SINGLETON_CLASS_CPP_NAMES` map in the generator that: - Maps `ClassDB` → `ClassDBSingleton` in the C++ class name emitted in the generated native function. - Forces `is_static=True` for all ClassDB methods (since 4.5 only exposes them as non-static on the singleton instance, accessed via `ClassDBSingleton::get_singleton()`). - Routes the singleton dispatch through `ClassDBSingleton::get_singleton()->method(...)` in both the regular and vararg `generate_native_method_function` paths. - The C# side correspondingly gets `static` methods on `ClassDB` (still extending `GodotObject`, so existing type-checks pass). 2. **`gdextension_interface_script_instance_create3` moved** in godot-cpp 4.5 from `` (which no longer exists) to `namespace godot::internal` inside ``. Fix in `src/leanclr_script.cpp`: - Replaced `#include ` with `#include `. - Changed the call site from `gdextension_interface::script_instance_create3(...)` to `::godot::internal::gdextension_interface_script_instance_create3(...)`. ## One Patch Required Inside the LeanCLR Submodule `thirdparty/leanclr/` is a git submodule, so the patch lives outside this repo's history and won't survive a `git submodule update`. In `src/runtime/build_config.h`, the upstream default is: ```c #define LEANCLR_FATAL_ON_RAISE_NOT_IMPLEMENTED_ERROR 1 ``` Set it to `0`. The binding generator emits ~5800 native icalls but the C# side only calls a tiny fraction; with the default `1`, hitting an unwired `NotImplemented` site calls `fatal_on_not_implemented_error()` and aborts the process (see `LEANCLR_CODEGEN_RETURN_NOT_IMPLEMENTED_ERROR` in `codegen/leanclr_common.h` and `RETURN_NOT_IMPLEMENTED_ERROR` in `core/rt_base.h`). With `0`, the interpreter returns `RtErr::NotImplemented` and execution continues, which is what the demo needs to be runnable today. This patch must be reapplied after every `git submodule update --init --recursive` (or after pulling new commits into the submodule). ## Repository Layout - `src/` — GDExtension C++ (one .h/.cpp per component, see below) - `managed/GodotSharpCompat/` — C# façade: hand-written `GD`, `Node`, `GodotObject`, `ExportAttribute`, `Variant`/marshalling helpers, `CallableDelegateRegistry`; plus a `Generated/` subdir produced by the binding generator (gitignored — must be regenerated). - `project/` — the Godot project itself (`.gdextension`, `.csproj`, scenes, scripts, `leanclr/` output dir, `bin/` for the GDExtension binaries) - `tools/binding_generator/generate_bindings.py` — Python generator for both `src/generated/godot_api.generated.cpp` and `managed/GodotSharpCompat/Generated/*.generated.cs` - `thirdparty/leanclr/` — LeanCLR git submodule, pulled via `add_subdirectory(${LEANCLR_ROOT}/src/runtime leanclr_runtime)` in the top-level CMake - `gdextension_interface.h` — pinned Godot 4 GDExtension interface header (the matching headers used by the binding generator live inside the fetched godot-cpp tree under `_deps/.../gdextension/`) ## Native Bridge Architecture (src/) The GDExtension wires LeanCLR into Godot through six cooperating pieces, registered at `MODULE_INITIALIZATION_LEVEL_SCENE` in `register_types.cpp`: - **`LeanCLRRuntimeBridge`** (`leanclr_runtime_bridge.{h,cpp}`) — the only file that knows about both godot-cpp and LeanCLR. Owns the global `LeanCLRVirtualMachine`, loads assemblies by name from the configured assembly directory, creates/release managed objects, and exposes overloaded `invoke_script_method` / `get_script_property` / `set_script_property` for the typed value marshallers LeanCLR needs. Also handles owner→managed-object registration and `migrate_script_state` for hot reload. - **`LeanCLRScriptLanguage`** (`leanclr_script_language.{h,cpp}`) — `ScriptLanguageExtension` singleton. Tells Godot there's a "LeanCLR" language; provides reserved words, validation, completion, debug hooks (most are stubs), and `_reload_*` entry points. `_reload_all_scripts` / `_reload_scripts` are what the runtime editor hits. - **`LeanCLRScript`** (`leanclr_script.{h,cpp}`) — `ScriptExtension` for an individual `.cs` / `.lcs` file. `parse_source()` extracts `assembly_name`, `type_name`, optional `entry_point`, and `base_type` (defaults to `"Node"`) from header comments via a simple parser in this file. Calls `godot::internal::gdextension_interface_script_instance_create3` to allocate script instances. - **`LeanCLRScriptLoader` / `LeanCLRScriptSaver`** (`leanclr_script_{loader,saver}.{h,cpp}`) — `ResourceFormatLoader`/`Saver` pair. Recognizes `cs` and `lcs` extensions, hands a `LeanCLRScript` resource to Godot so `.cs` files can be attached to nodes and saved in `.tscn`/`.tres`. - **`LeanCLRMain`** (`leanclr_main_node.{h,cpp}`) — a `Node` that, on `NOTIFICATION_READY`, instantiates the configured C# class (default `Game.ClassDbMain`) through the bridge. Used by `main.tscn` as the bootstrapping demo node. - **`LeanCLRHotReloadHost`** (`leanclr_hot_reload_host.{h,cpp}`) — a `Node` that owns the *current* managed object for runtime hot reload. Wires `_Input`/process through the bridge, and on `reload_assembly` captures state via `CaptureHotReloadState` (custom `Variant` blob from the script), instantiates the new assembly, calls `migrate_script_state`, then `RestoreHotReloadState` and `OnHotReloaded` on the new instance. Skips itself while in the editor or while a scene is being edited. - **`leanclr_wasm_libcxx_shim.cpp`** — minimal `std::__1::__hash_memory` shim for Emscripten (libc++ is missing the symbol LeanCLR's stdlib expects when linking into a wasm side module). - **`src/generated/godot_api.generated.cpp`** — auto-generated, ~5800+ per-class marshalling functions. Do not edit; regenerate via the binding generator. ## Demo / Hot Reload Flow `project/runtime_hot_reload_demo.tscn` is the main scene and exercises the hot-reload path end to end: 1. `FlappyScript` (a `LeanCLRScript` pointing at `project/scripts/HotReloadSmoke.cs`) is the gameplay node. 2. `LiveHotReloadHost` (a `LeanCLRHotReloadHost`) is the bridge that drives the C# object's lifecycle. 3. `HotReloadInputRelay.gd` forwards input and polls `res://leanclr/live_reload.txt` every `reload_poll_seconds`. When the marker changes from the default `Game`, it calls `reload_assembly` on the host. 4. `RuntimeCSharpEditor.gd` is a `CodeEdit` window inside the game. Pressing *Run* writes `HotReloadSmoke.cs` back, shells out to `dotnet msbuild` to produce a uniquely-named `GameRuntimeEdit.dll` into `project/leanclr/`, then writes that name to `live_reload.txt` so the relay picks it up on its next tick. It also rebuilds the default `Game.dll` afterward and restores the runtime-edit dll from `user://` so the next editor run is reproducible. 5. `HotReloadSmoke.cs` implements `CaptureHotReloadState` / `RestoreHotReloadState` / `OnHotReloaded` so the bird/physics state survives across reloads. Setting the env var `LEANCLR_RUNTIME_EDITOR_AUTORUN` triggers a one-shot edit-and-rebuild on startup to verify the path. ## Conventions & Gotchas - The C# `GodotSharpCompat` shim has a `Generated/` subdir in its `.gitignore`; if you regenerate bindings keep it out of the repo. - The native bridge is large (`leanclr_runtime_bridge.cpp` is ~87 KB) — it is one translation unit by design. The other bridge files stay focused. - `project/leanclr/*.dll` is gitignored; rebuilt DLLs land alongside but should not be committed. - `compatibility_minimum = "4.4"` in `leanclr.gdextension`, project `config/features = "4.6"`. The current `GODOT_CPP_BRANCH` is 4.5 — bump together (see "Two Source Patches" above). - CMake honors `GODOT_CPP_ROOT` / `GODOT_CPP_LIBRARY` for prebuilt godot-cpp; otherwise it `FetchContent`s from GitHub. On Windows + MSVC the runtime is forced to `MultiThreadedDLL` to match what `godot-cpp` ships. - LeanCLR is a submodule: changes inside `thirdparty/leanclr/` are not part of this repo's history. Don't edit it locally — patch upstream. - No automated test suite. The demo *is* the smoke test; run the Flappy scene and exercise hot reload to validate a change. - The Godot binary on this machine is at `D:\Projects\godot-build\build\godot.exe` (Godot 4.6.3). Pass it as a positional arg to `--path project` to launch.