A toolchain for writing an OpenTTD AI in C# and transpiling it to Squirrel, the language OpenTTD runs AIs in. You author the AI ("Brunel"; AJ in the code) with full IDE support, static typing, and unit tests; the build emits .nut files that drop into OpenTTD's ai/ folder.
Every file in AJ is dual-target: it compiles and runs as ordinary C# (against the NoAi/Squirrel stand-in libraries, with mocks for tests) and transpiles to valid Squirrel. The C# side exists for tooling, type-checking, and tests; the Squirrel side is what ships.
See CLAUDE.md for architecture and conventions, and PLAN.md for the modernization roadmap.
dotnet build SquirrelCompiler.slnx
dotnet test
(Requires the .NET 10 SDK.)
OpenTTD's script API drifts between releases — classes, methods, enums, and constants come and go. The NoAi stubs are generated from OpenTTD's C++ headers by the NoAiConverter tool, so retargeting to a newer API version means regenerating and reconciling.
-
Get the OpenTTD source for the target version. Check out the release tag into a local working copy, e.g.
git checkout 15.3underD:\Source\OpenTTD. The script API headers are insrc/script/api/script_*.hpp. -
Regenerate the stubs into the committed
Generated/folder:dotnet run --project NoAiConverter -- --headers <OpenTTD>\src\script\api --out NoAi\Stubs\GeneratedThe generator owns that folder and rewrites it wholesale, so a class dropped upstream leaves no stale file. It writes one
partialC# stub per AI-exposed class (events toGenerated\Events\): it resolves theDOXYGEN_APIview of each header, filters by the@apidoc tag (onlyai/ai gamesurface), and renders summaries, parameters, returns, remarks, and folded@codeexamples into C# XML docs. The hand-written layer lives in the siblingNoAi\Stubs\Manual\, which the generator never touches. -
Build
NoAi. This is the compile-check — the generated stubs are committed and compiled directly. Blockers surface as build errors: a missingNoAi.Typestype, a list with no element-type mapping, astd::/Squirrel-builtin type the generator doesn't handle yet. Fix them in the layers in step 5, then rebuild. -
Read what moved.
git diff -- NoAi/Stubs/Generatedshows new/removed classes, changed signatures, new enums, renamed members. Read the version's script-API changelog (src/script/api/*_changelog.hpp) and the release's game-change notes alongside the diff — both inform later AI design, so don't skip the reading. -
Reconcile what the generator can't decide:
NoAi/Types— add areadonly record structfor each new opaque ID (int- or long-backed, explicit(int)conversions). A type the generator emits butNoAi.Typeslacks shows up as a "type not found" build error — that's the signal to add it.NoAiConverter/ListElementTypes— map each newAIList-derived class to its element type so the generator emits the typed base (AITileList : AIList<TileIndex>); the headers don't carry it. A list left out of the map stays a bareAIList.NoAiConvertermappers —TypeMapperfor new C++ types (thestd::, integer, and Squirrel-builtin families),ConstantValuefor new sentinel constants (real values, each cited to its core header).NoAi/Stubs/Manual— the partial companions the headers can't express:AIModeand the: AIModepartials for the mode classes; theAIListenumerator plumbing and the genericAIList<TItem>.AIMap/AIGameSettingsare hand-written here (mock delegation) with their generated copies excluded inNoAi.csproj;HandWrittenStubSyncTestfails if their method surface drifts from the generated reference — reconcile them and their mocks when it does.CSharpToSquirrelConverter— update any transpiler special-cases the API moved out from under.
-
Build
AJand fix the fallout. Renamed/removed/re-typed API surfaces as compile errors. Change AJ freely — keeping its current behavior is not a goal. -
Bump
info.cs— setGetAPIVersion()to the new version string (e.g."15"), before re-baselining so the golden captures it. -
Re-baseline the golden fixtures. The golden test is
[Explicit]— it pins the whole transpiled-AJ output, so it would break on every deliberate AJ change and is therefore kept out of the everyday run, used only here. Regenerate the fixtures (deleteUnitTestSuite.AJ\Golden\AJfirst if AJ files were added or removed — the transpiler doesn't prune):dotnet run --project SquirrelCompiler -- --source AJ --out UnitTestSuite.AJ\Golden\AJthen review the
git diffofGolden/AJ. It is a reviewed re-baseline, never silent. (To see the failure as NUnit's diff instead, run the test by name:dotnet test --filter FullyQualifiedName~Transpiler_ReproducesGoldenOutput.) -
Load-test in OpenTTD. Drop the emitted
.nutset into OpenTTD'sai/folder and confirm the AI loads. Squirrel compiles function bodies lazily, so loading only really exercisesinfo.nut/main.nutparsing andRegisterAI— a bad body won't error until that code path runs.
If you're jumping several releases at once, step through them one version at a time — the point is to read each version's API and game changes, not just to reach the latest headers.