Skip to content

C# implementation

Generated C# API reference

The C# implementation is the reference implementation for the shared, language-neutral contracts. Runtime, Compiler and C# adapters are separate libraries; the CLI and internal Conformance adapter are executables.

Project / package Responsibility Dependencies
GameEventScript.Runtime Immutable values and Programs, .gesb codecs/validation, GESA dumping, Host, VM, random streams, extensions and external types .NET Standard library
GameEventScript.Compiler Source lexer/parser, validation, lowering, optimization, register allocation, immutable Programs and debug sections Runtime
GameEventScript.CSharpBridge Delegate adapters, Reflection/attribute bindings, CLR value conversion and optional automatic host pumping Runtime
GameEventScript.SyntaxHighlighter GES/GESA UTF-16 spans, TextMate scopes, incremental states and ANSI rendering .NET Standard library
GameEventScript.Conformance Internal observation adapter for the independent Python runner Runtime and Compiler
GameEventScript.Tool / dotnet ges Compile/check/run/dump, interactive console, filesystem and terminal adapters Runtime, Compiler, CSharpBridge, SyntaxHighlighter and PrettyPrompt

All four libraries target .NET Standard 2.1 and use C# 12. Runtime, and Compiler are synchronous, fileless and independent of a test framework. CLR-specific adapters belong to CSharpBridge. Compiler and CSharpBridge each depend only on Runtime, so a product executing precompiled .gesb files can ship Runtime with optional C# adapters without shipping the compiler or test infrastructure.

The public entry points use GameEventScript.Api; values use GameEventScript.Runtime.Values. The compiler’s builder also lives in GameEventScript.Api, even though its assembly is separate. C# adapters use GameEventScript.CSharpBridge. The internal Conformance adapter is not a public library API.

Module layout

Each module owns its production project in src and its native test project in tests. The repository solution groups them by module, so a Runtime or Compiler change and its implementation tests are visible together. Build or test one module directly by passing its .csproj to dotnet build or dotnet test.

implementation/csharp/
GameEventScript.Runtime/ src/ + tests/
GameEventScript.Compiler/ src/ + tests/ + worker/
GameEventScript.CSharpBridge/ src/ + tests/
GameEventScript.Tool/ src/ + tests/
GameEventScript.Conformance/ adapter/ + fixtures/ + fixture-exporter/
verification/ Repository.Tests, consumers, package tool, test support

The internal C# Conformance adapter depends on Runtime and Compiler. The shared Markdown corpus and portable fixtures stay in repository-level conformance/. verification/Tests.props supplies common MSTest settings and linked helpers; it contains no test cases. The six test projects each discover their own tests exactly once. Some native Runtime and Bridge tests use Compiler to build inputs; these are test-only dependencies and do not change the product boundaries.

Build and verify

Solution and toolchain

Open GameEventScript.sln from the repository root in an IDE supporting the repository’s .NET SDK. The solution includes the libraries, CLI, native tests and supporting verification projects.

Development uses the exact .NET SDK 10.0.201 pinned in global.json. The tests and CLI target .NET 8 and also need its runtime installed. The SDK version does not change the libraries’ .NET Standard 2.1 target.

Run these commands from the repository root. The scripts also resolve the checkout correctly when invoked by absolute path from another directory:

Command Purpose
./scripts/clean.sh [--dry-run] [--artifacts-only] Remove repository build outputs; preview with --dry-run (scope)
./scripts/build-csharp.sh Build the complete solution in Release
./scripts/test-csharp.sh Run shared Conformance and native non-performance tests in the default Debug configuration
./scripts/format-csharp.sh Verify solution formatting without modifying files
./scripts/test-csharp-performance.sh Run Release allocation and elapsed-time gates; timing measurements require the matching calibrated profile
./scripts/install-csharp-tool.sh Build the solution and install/update the global dotnet ges CLI
./scripts/uninstall-csharp-tool.sh Remove the C# CLI installation; succeeds when already absent
./scripts/pack-csharp.sh 0.1.0-rc1 Build canonical NuGet and symbol packages for the four public libraries
./scripts/release-csharp-dry-run.sh 0.1.0-rc1 Pack, stage DLL sets and verify artifact consumption without publishing
./scripts/verify-csharp-reproducibility.sh 0.1.0-rc1 Build packages independently twice and verify byte-identical results

Normal library build and test intermediates use project-local bin/obj directories. Packages, staged DLL sets, CLI build outputs and generated reports stay under ignored artifacts. Handwritten C# follows CodeStyle.md and the root .editorconfig. Public XML documentation and the approved API surface are checked by the build and native tests.

Conformance and native tests

For Release verification including the independent allocation gates:

Terminal window
dotnet test GameEventScript.sln \
--configuration Release --filter "TestCategory!=Performance|TestCategory=Allocation"

Portable behavior is authored in the shared Markdown corpus. The independent Python runner evaluates observations from the internal C# adapter. Run sh scripts/test-csharp-conformance.sh for correctness and managed allocations. Each product module owns a separate native test project. Native tests cover C# adapters, implementation details, public API boundaries, CLI/process integration and measurements that require the CLR.

Performance is opt-in and always measured; no reference values are echoed. Managed allocation checks use the csharp-dotnet-release-managed profile. Elapsed-time benchmarks require their calibrated hardware/toolchain profile. See the test guide for measurement scope.

Reports are written below artifacts/conformance/python. Received Markdown is explicitly generated with the Python runner’s received command; it never replaces approved references automatically. The shared CapabilityMatrix records corpus identity and acceptance status across implementations.

Use the CLI

Install or update from the checkout:

Terminal window
./scripts/install-csharp-tool.sh
dotnet ges --help
dotnet ges check game.ges
dotnet ges compile handlers.ges definitions.ges -o artifacts/game.gesb
dotnet ges dump artifacts/game.gesb
dotnet ges run artifacts/game.gesb --args 12 Hello 34
dotnet ges run --interactive --color

The package is GameEventScript.Tool; the installed executable is dotnet-ges, invoked as dotnet ges. The global tool directory (normally $HOME/.dotnet/tools) must be on PATH. The native Swift CLI is named ges, so both implementations can be installed together.

Both installation and removal support --tool-path DIRECTORY for an isolated installation. Run the installer again to update it, or ./scripts/uninstall-csharp-tool.sh to remove the global installation. For a custom directory, use the same --tool-path when uninstalling.

By default, run drains initialization and sends Main(args) with a List of Text values. --interactive selects the event console instead. Full command syntax, scenarios, console handlers, multiline input and inspection commands are documented in the CLI guide.

Compile source text

For a local embedding, add a project reference to the compiler. Replace the path with the checkout’s location:

<ItemGroup>
<ProjectReference Include="/path/to/GameEventScript/implementation/csharp/GameEventScript.Compiler/src/GameEventScript.Compiler.csproj" />
</ItemGroup>

Runtime is brought in transitively. The builder accepts source text and optional diagnostic source names; file access remains with the embedding:

using GameEventScript.Api;
var program = GameEventScriptBuilder.Create()
.AddScript("""
module example
function double(_ value as :Number) be value + value
on Start(value) { emit Done(value: double(value)) }
""", "example.ges")
.Compile();
byte[] bytes = GameEventScriptProgramWriter.ToArray(program);
// The embedding can save bytes, or load program directly into a Host.

Repeated AddScript calls compile sources jointly. Compile() can be repeated; previously returned Programs remain immutable. Debug symbols, source maps and source archives are included by default. Use .WithDebugInfo(GameEventScriptDebugInfoOptions.None) for compact output, .WithProgramVersion(...) to set the portable version, and .WithExternalTypeCatalog(...) for declarative external types. Runtime bindings remain separate. GameEventScriptCompileException.Diagnostics exposes stable diagnostic codes and structured context, including source locations.

Consume the Runtime

A product that receives precompiled Programs needs only the Runtime reference:

<ItemGroup>
<ProjectReference Include="/path/to/GameEventScript/implementation/csharp/GameEventScript.Runtime/src/GameEventScript.Runtime.csproj" />
</ItemGroup>

The following example loads bytes supplied by the embedding, subscribes to Done(value) and sends Start(value) to the program above. The native callback prints Done(value: 84):

using System;
using GameEventScript.Api;
using GameEventScript.Runtime.Values;
static (GameEventScriptStartResult Start, GameEventScriptExecutionResult? Execution) Execute(byte[] bytes)
{
var program = GameEventScriptProgramReader.Read(bytes);
var host = GameEventScriptHost.CreateBuilder().WithRandomSeed(123).Build();
var output = GameEventScriptMessageSignature.Create("Done", ["value"]);
var subscription = host.Subscribe(output, new Output());
var instance = host.Load(program);
try
{
var startup = host.Start();
if (!host.IsReady) return (startup, null);
host.Receive(GameEventScriptMessage.Create("Start", [
new GameEventScriptMessageArgument("value", GesValue.GesInteger(42))
]));
return (startup, host.RunToCompletion());
}
finally
{
instance.Detach();
subscription.Unsubscribe();
}
}
sealed class Output : IGameEventScriptNativeMessageHandler
{
public void Handle(GameEventScriptMessage message, GameEventScriptContext context)
{
// Console I/O belongs to this embedding callback, not Runtime.
Console.WriteLine(message);
}
}

Call Execute(bytes) and inspect the startup and execution results. The initial flow is Build, Load all Programs, register native handlers, then Start. Start initializes the group without draining its emitted messages. A later Load with initialization returns an instance with a pending StartResult; ordinary pumping completes that init. An init failure removes that instance and its pending deliveries while the host remains ready. Host runtime defines the full startup and output rules. ExecuteFrame(opcodeBudget) supports bounded stepping and resumption. Retain the Host and instance between frames when embedding it in a game loop. Dropping an instance or subscription handle does not detach it; use Detach() or Unsubscribe() explicitly. A Host is serial and requires one caller at a time. The immutable Program can be reused in multiple independent Hosts.

Add GameEventScript.CSharpBridge when you need delegate handlers, CLR-backed external types, attribute-based extensions or GameEventScriptCSharpHostRunner for synchronization and automatic pumping. It does not add a Compiler dependency. The public API specification documents the portable contracts and C# mapping; Host runtime owns lifecycle, ordering, execution limits and diagnostic behavior.

Distribution

Runtime, Compiler, CSharpBridge and SyntaxHighlighter are packaged independently with a shared release version. Conformance stays internal. NuGet CLI tools (since 0.3.0) are packaged separately as GameEventScript.Tool and GameEventScript.Tool.Aot; see CLI installation. Local packages are written to artifacts/csharp/packages/<version>; the release dry run also stages versioned DLL sets below artifacts/csharp/dll/<version> for Runtime, Compiler and Unity adapters. XML documentation and portable symbols accompany the assemblies. CLI packaging is handled separately by its installer.

Use the C# distribution guide for local NuGet consumption, direct DLL references, package validation, reproducibility and the gated publication workflow. The dry run never publishes. The Unity DLL set is checked with an external .NET consumer; validation in a real Unity project remains deferred.

The documentation index links the owning language-neutral specifications. Deferred work is tracked only in BACKLOG.md.

Explicit message JSON exchange

The Runtime provides GameEventScriptMessageJson for optional product transport. Use Serialize/Deserialize in C#, or serialize/deserialize in Swift, for messages; the corresponding SerializeValue/serializeValue methods encode a standalone value. Decoding reconstructs immutable data and never runs Record or native constructors. External objects export as typed Record snapshots. Local Publish does not serialize; configure encoding explicitly at your I/O boundary. The message format defines the envelope and errors.