RadicalBeard.Evaluate
0.2.0
dotnet add package RadicalBeard.Evaluate --version 0.2.0
NuGet\Install-Package RadicalBeard.Evaluate -Version 0.2.0
<PackageReference Include="RadicalBeard.Evaluate" Version="0.2.0" />
<PackageVersion Include="RadicalBeard.Evaluate" Version="0.2.0" />
<PackageReference Include="RadicalBeard.Evaluate" />
paket add RadicalBeard.Evaluate --version 0.2.0
#r "nuget: RadicalBeard.Evaluate, 0.2.0"
#:package RadicalBeard.Evaluate@0.2.0
#addin nuget:?package=RadicalBeard.Evaluate&version=0.2.0
#tool nuget:?package=RadicalBeard.Evaluate&version=0.2.0
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:; 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). (runtime/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). (runtime/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.
entity.spawn{…}creates a C# entity (runtime/Engine.cs); the Lua handle proxies reads/writes into C# via metatable. - Auto-discovery. The runtime recursively scans
res://scripts; any script with aregister:block is wired to Godot lifecycle (on_startonce,on_update(dt)per frame). Each hook may be registered by only one script project-wide. - Hot reload (default). A
FileSystemWatcherwatches scripts, configs, and frontmatter-declaredassets:; changes reload on the main thread. A changed system re-runs its body and refreshes its hook closures while live entities persist. (runtime/EvaluateRuntime.cs) - Lifecycle hooks.
register:wireson_start,on_update(dt),on_physics_update(dt),on_input(event)to Godot's Node lifecycle. One registration per hook, project-wide. std.*standard library. Real C#-backed types via the[LuaObject]source generator —std.vec3,std.vec2,std.color,std.vector,std.linked_list(runtime/Std.cs).- Persistence (
save). SQLite-backed runtime/player data under~/.local/share/evaluate/—save.set/get/delete(runtime/Persistence.cs). - Godot binding (ambient, default).
godot.<Type>resolves any Godot type on first use (runtime/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 . # demo: discovery, engine call, instance+signal, std, movement
godot --headless --path . -- --test # enforcement suite (6 tests)
Layout
project.godot main.tscn Evaluate.csproj
runtime/ EvaluateRuntime.cs Loader.cs Std.cs Frontmatter.cs Toml.cs
GodotBinder.cs Persistence.cs BindGodotAttribute.cs Prebaked.cs
EvaluateTests.cs
generator/ Evaluate.Generator.csproj BindGodotGenerator.cs (Roslyn source generator)
scripts/ game.evt player.evt enemy_factory.evt game.toml player.toml
tests/ forbidden_global / undeclared_api / missing_accessor /
system_a / system_b / readonly_module (.evt)
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)
- 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.