RadicalBeard.Evaluate
0.7.2
dotnet add package RadicalBeard.Evaluate --version 0.7.2
NuGet\Install-Package RadicalBeard.Evaluate -Version 0.7.2
<PackageReference Include="RadicalBeard.Evaluate" Version="0.7.2" />
<PackageVersion Include="RadicalBeard.Evaluate" Version="0.7.2" />
<PackageReference Include="RadicalBeard.Evaluate" />
paket add RadicalBeard.Evaluate --version 0.7.2
#r "nuget: RadicalBeard.Evaluate, 0.7.2"
#:package RadicalBeard.Evaluate@0.7.2
#addin nuget:?package=RadicalBeard.Evaluate&version=0.7.2
#tool nuget:?package=RadicalBeard.Evaluate&version=0.7.2
<p align="center"><img src="logo.png" alt="Evaluate" width="180"></p>
Evaluate
A data-driven, moddable game framework built as an extension that runs inside
Godot (Godot is the host; Evaluate has no main). Engine-side code is .NET/C#
(net10.0, Godot 4.6-mono). Gameplay is authored in "evt" scripts whose YAML
frontmatter declares a capability-scoped signature; a custom loader sandboxes
each script to exactly what it declares. Scripts are auto-discovered and
hot-reloaded by default.
Architecture
- Frontmatter → signature. The leading
---block is split off and parsed as real YAML (YamlDotNet) intoconfig:,apis:,register:,returns:,assets:,scenes:; the Lua body is handed to the VM verbatim (line numbers preserved). The signature is the C#↔Lua boundary contract —returnsdeclares typed, access-scoped members (no Lua type system). (src/Evaluate/Frontmatter.cs) - Per-script sandbox. Each body runs via
load(body, name, "t", env)on a sharedLuaState, whereenvholds only the declaredconfig.*, declaredapis, the always-availablestdand ambientgodot, plus a few safe primitives — including the metatable builtins (setmetatable/getmetatable/rawget/rawset/rawequal/rawlen) so scripts can define classes the idiomatic Lua way. Undeclared globals (os,io, …) are absent, andpcallis intentionally withheld (errors surface rather than being swallowed). (src/Evaluate/Loader.cs) - Custom
requirenarrows the returned module to itsreturnscontract (get/set property →get_/set_accessors; plain method; read-only hides the setter; missing accessor errors). - Data lives in the engine. Game objects are Godot nodes — declared in scene
files or created via
godot.<Type>.new()/world— so there is no separate entity system. The Lua handle is a thin proxy whose metatable routes reads/writes into the engine. (src/Evaluate/GodotBinder.cs) - Auto-discovery. The runtime recursively scans
res://scripts; any system.evtwith aregister:block is wired to the Godot lifecycle. A hook may be registered once per scene (see Scenes & layers) — so the sameon_updatecan be registered inmenu,level1, and globally, each by a different script. - Hot reload (default). A
FileSystemWatcherwatches scripts, scene files, configs, and frontmatter-declaredassets:; changes reload on the main thread. A changed system or node script re-runs its body and refreshes its hook closures while live nodes persist; a changed*.scenerebuilds the active scene. (src/Evaluate/EvaluateRuntime.cs) - Scenes & layers. Gameplay is split into a persistent global layer and a
swappable scene layer, so one program holds many scenes, each with its own
registered functions:
global.scene(reserved manifest) declares nodes that never clear (e.g. the player) plusstart_scene. Loaded once into a persistent Global root.*.scenefiles (TOML content) declare a node tree as keyed, nested tables —[nodes.Player]then[nodes.Player.Camera]makes Camera a child of Player. A node's name is the table key; reserved keystype/script; a sub-table is a child node and a scalar/array is a property (position = [x, y, z]). A sub-table tagged with_typeis a property, not a child — it names an inline resource (mesh = { _type = "BoxMesh", size = [1,1,1] }) or a builtin struct (custom_aabb = { _type = "Aabb", position = [1,2,3], size = [4,5,6] }), which is how composite structs likeTransform3D/Rect2/Basis/Projectionare written. A property key may be quoted to carry a/(Control theme overrides:"theme_override_colors/font_color" = [..]). An optional top-leveldescription = "…"holds scene docs as data. Parsed with Tomlyn; instantiated under a per-scene container that is freed wholesale on switch. The editor addon can round-trip all of this back to the.scene(see below).*.node.evtis one node's behavior, attached via the scene file; its hooks run withselfbound to that node (no spawning in the script).*.evtsystems are conductors: noscenes:⇒ global (always run);scenes: [a, b]⇒ active only whilea/bis current. ThesceneAPI does routing —scene.change(name)(applied at the next frame boundary, never mid-hook),scene.current(),scene.find(path)(unique by node path, e.g."Level/Enemy"),scene.add(node).
- Editor preview + write-back (optional addon). A
.scenefile is TOML, which the Godot editor doesn't render natively. Thedev/addons/evaluate_sceneaddon registers anEditorImportPluginthat converts.scene→ aPackedScene(via the sameSceneFile/SceneBuilderthe runtime uses), so the editor shows and renders the node tree. It also adds an "Evaluate" dock (and Tools-menu entries) to open a.sceneas an editable native scene, edit it with the normal editor — move nodes, create and place new nodes, set properties, attach a node script via a__evt_scriptmetadata entry — and Save to .scene, which serializes the edited tree back to the TOML file viaSceneWriter(the inverse ofSceneFile/SceneBuilder). Editor-only (#if TOOLS) and needs no running game (a running game's hot-reload picks the saved file up for free). The addon is not part of the NuGet package (the runtime library carries no editor types); instead it ships as a drop-in zip on each GitHub release (evaluate_scene-<version>.zip): extract it into your project to getres://addons/evaluate_scene/, then enable the plugin in Project Settings (it needs theRadicalBeard.Evaluatepackage for the build/serialize logic). A C# editor addon can't be a bare DLL — its.csmust live underres://addons/and compile with the game — so the zip carries the source; the stableEvaluate.Editornamespace means it drops in verbatim. - Lifecycle hooks.
register:wires Godot's Node lifecycle. System hooks:on_start(global, once),on_enter/on_exit(scene-scoped, per activation),on_update(dt),on_physics_update(dt),on_input(event). Node hooks (*.node.evt, withself):on_ready,on_update,on_physics_update,on_input,on_exit. std.*standard library. Real C#-backed types via the[LuaObject]source generator —std.vec3,std.vec2,std.color,std.vector,std.linked_list(src/Evaluate/Std.cs).- Persistence (
save). SQLite-backed runtime/player data in Godot's per-projectuser://directory (the platform-native, per-game path Steam Cloud syncs from) —save.set/get/delete(src/Evaluate/Persistence.cs). - Godot binding (ambient, default).
godot.<Type>resolves any Godot type on first use (src/Evaluate/GodotBinder.cs):- instances —
godot.Node3D.new()(Activator); member access routes through the engine ClassDB (GodotObject.Call/Get/Set), not reflection. Instances areILuaUserData, so they pass back into the engine (world:add_child(node)); - properties —
node.position = std.vec3(…)/node.position; - signals —
node:connect("timeout", fn)(0–6 args, dispatched on the main thread) andnode:emit_signal(...); - enums/constants —
godot.Key.Space,godot.Timer.TimerProcessCallback.Idle; - marshalling — exhaustive: primitives, strings,
NodePath, objects,Array/Dictionary; rich C#-backedVector2/3&Color(↔std.*); and every other struct (Vector4,Vector2I/3I,Quaternion,Rect2,Plane,Aabb,Basis,Transform2D/3D) + packed arrays as named-field/list tables. Reads are tables; writes are target-type-aware — assigning a table to a struct property builds the exact struct (sonode.transform = tround-trips); - statics — reflection fallback, or a pre-baked zero-reflection binding.
- instances —
- Pre-bake (source generator).
generator/is a RoslynIIncrementalGenerator:[BindGodot(typeof(Godot.OS))]emitsOsBinding.Create()with direct, reflection-free calls (53 boundOSmethods), filtered to Lua-convertible signatures. The binder prefers pre-baked bindings.
Base VM
Lua-CSharp (LuaCSharp 0.5.5, Lua 5.2).
Source-generator interop ([LuaObject]) → AOT-clean, low-allocation. Its API is
async-only; we bridge to sync for Godot's _Process (safe because script calls
don't actually await). Dialect note: scripts use x = x + …, not the planned
custom dialect's +=.
Run
godot --headless --path dev # demo: global layer, scene switch (menu -> level1), node script
godot --headless --path dev -- --test # enforcement suite (18 tests)
godot --headless --path dev -- --quit-after 8 # demo, then quit after 8 frames
Layout
README.md CHANGELOG.md Directory.Build.props Evaluate.slnx global.json LICENSE.md
src/Evaluate/ Evaluate.csproj EvaluateRuntime.cs Loader.cs Std.cs
Frontmatter.cs Toml.cs SceneFile.cs SceneBuilder.cs
GodotBinder.cs Persistence.cs BindGodotAttribute.cs Prebaked.cs EvaluateTests.cs
src/Evaluate.Generator/ Evaluate.Generator.csproj BindGodotGenerator.cs (Roslyn source generator)
dev/ the demo / enforcement-test harness game (consumes the lib by project reference):
project.godot main.tscn EvaluateHost.cs Dev.csproj
scripts/ global.scene menu.scene level1.scene
player.node.evt showcase.evt menu.evt level1.evt game.toml player.toml
addons/evaluate_scene/ editor import plugin (.scene -> PackedScene preview)
tests/ enforcement suite (.evt): forbidden_global / undeclared_api / missing_accessor /
system_a / system_b / readonly_module / metatable_oop / scene_a / scene_b /
scene_a_dup / global_update / self_node, + global.scene
artifacts/ packed .nupkgs (gitignored)
Remaining work toward "full featured"
- Godot coverage is complete: instance + static members (any type), all
Variant structs + packed arrays, enums/constants, and signals — all two-way.
Static methods with struct/packed signatures bind via the reflection fallback
(layered over the pre-baked primitives). Instance access is reflection-free
(engine ClassDB
Call/Get/Set). Remaining (perf only): extend the source-gen pre-bake to struct/packed static signatures so they too avoid reflection. - Frontmatter LSP (deferred): editor diagnostics for the signature block —
e.g. red-underline a non-existent
godot.*API or missing config file. The Lua body itself rides the standard Lua LSP. - AOT export & mod packaging/distribution: not yet built.
- Out of scope by decision: a Lua type system (signature contract is enough) and sandbox resource limits / DoS protection (modding is free).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- GodotSharp (>= 4.6.0)
- LuaCSharp (>= 0.5.5)
- Microsoft.Data.Sqlite (>= 9.0.0)
- Tomlyn (>= 0.19.0)
- YamlDotNet (>= 16.2.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.