ManiaScriptSharp.ManiaPlanet3 1.0.0-alpha.5

This is a prerelease version of ManiaScriptSharp.ManiaPlanet3.
dotnet add package ManiaScriptSharp.ManiaPlanet3 --version 1.0.0-alpha.5
                    
NuGet\Install-Package ManiaScriptSharp.ManiaPlanet3 -Version 1.0.0-alpha.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ManiaScriptSharp.ManiaPlanet3" Version="1.0.0-alpha.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ManiaScriptSharp.ManiaPlanet3" Version="1.0.0-alpha.5" />
                    
Directory.Packages.props
<PackageReference Include="ManiaScriptSharp.ManiaPlanet3" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ManiaScriptSharp.ManiaPlanet3 --version 1.0.0-alpha.5
                    
#r "nuget: ManiaScriptSharp.ManiaPlanet3, 1.0.0-alpha.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ManiaScriptSharp.ManiaPlanet3@1.0.0-alpha.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ManiaScriptSharp.ManiaPlanet3&version=1.0.0-alpha.5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ManiaScriptSharp.ManiaPlanet3&version=1.0.0-alpha.5&prerelease
                    
Install as a Cake Tool

ManiaScriptSharp

Write C# in your IDE, get ManiaScript .Script.txt files on disk in real time.

The project ships as a Roslyn incremental source generator plus a runtime/attributes library and a stub API surface. Every time you save (or even type) a C# file, the generator re-runs inside the IDE, re-translates your code, and overwrites the matching .Script.txt next to your project.

Why ManiaScriptSharp?

  1. Easier syntax
  2. Accurate auto-complete
  3. Reduced code bloat
  4. Unit-testability

How it works

flowchart LR
    A[C# source<br/>MyGamemode.cs] -->|saved / typed| B(Roslyn<br/>IIncrementalGenerator)
    C[MSBuild properties] -->|CompilerVisibleProperty| B
    B --> D{{ScriptEmitter}}
    D -->|File.WriteAllText| E[ManiaScript/<br/>MyGamemode.Script.txt]
    D -->|AddSource| F[marker .g.cs<br/>satisfies generator contract]
  1. The generator's SyntaxProvider watches every ClassDeclarationSyntax whose base references IContext or ILib.
  2. For each match, ScriptEmitter walks the syntax tree with a SemanticModel and produces ManiaScript text following the C# to ManiaScript conversion reference below.
  3. BuildSettings reads compiler-visible MSBuild properties to set the output folder and indentation.
  4. The emitted text is written via File.WriteAllText, and a tiny marker // generated C# file is added through SourceProductionContext.AddSource so the generator participates in the compilation legitimately.

Writes are skipped when the existing file is byte-identical, so IDE responsiveness stays smooth.

Installation

ManiaScriptSharp is distributed as NuGet packages. Add the runtime library, the generator, and the API surface package matching your target game:

dotnet add package ManiaScriptSharp
dotnet add package ManiaScriptSharp.Generator
dotnet add package ManiaScriptSharp.ManiaPlanet     # ManiaPlanet (2019)
# or ManiaScriptSharp.ManiaPlanet3                  # ManiaPlanet 3 (2015)
# or ManiaScriptSharp.Trackmania                    # Trackmania (2020)

ManiaScriptSharp.Generator is a DevelopmentDependency Roslyn analyzer package, so it contributes no runtime assembly, and its build/ManiaScriptSharp.Generator.props is imported automatically once referenced.

Optionally configure the build properties in your .csproj. These are the defaults:

<PropertyGroup>
    <ManiaScriptOutputDir>ManiaScript</ManiaScriptOutputDir>
    <ManiaScriptIndentSize>4</ManiaScriptIndentSize>
    <ManiaScriptIndentStyle>spaces</ManiaScriptIndentStyle>
</PropertyGroup>

ManiaScriptOutputDir is the primary output directory. Additional destinations are intended as machine-local deployment/debug mirrors, so configure them in <project>.csproj.user rather than the source-controlled project file. The repository's *.user ignore rule keeps this file out of source control:


<Project>
<PropertyGroup>
    <ManiaScriptAdditionalOutputDirs>../Server/Scripts;../Client/Scripts</ManiaScriptAdditionalOutputDirs>
</PropertyGroup>
</Project>

The value is a semicolon-separated list. Relative paths are resolved from the project directory; absolute paths are also supported. The primary output is always written first and duplicate destinations are ignored. A mirror path that cannot be resolved, created, or written reports an MSS002 warning and does not stop primary output or other mirrors.

Then write a class implementing IContext:

using ManiaScriptSharp;

public class MyGamemode : CTmMode, IContext
{
    public void Main() { }
    public void Loop() { }
}

Building the project generates ManiaScript/MyGamemode.Script.txt next to it, following the C# to ManiaScript conversion reference below.

Project templates

Instead of setting up a project by hand, install the dotnet new template pack and scaffold one, picking the target game via the --Api parameter (ManiaPlanet (default), ManiaPlanet3, or Trackmania):

dotnet new install ManiaScriptSharp.Templates
dotnet new msharp-gamemode -n MyGamemode --Api Trackmania

This generates a ready-to-build project with the runtime library, generator, and matching API surface package (ManiaScriptSharp.ManiaPlanet / ManiaScriptSharp.ManiaPlanet3 / ManiaScriptSharp.Trackmania) already referenced.

ManiaPlanet and ManiaPlanet3 expose both CTmMode and CSmMode as a context base class; pick one with --BaseClass (defaults to CTmMode). Trackmania only has CSmMode, so --BaseClass is ignored (always CSmMode) when --Api Trackmania is selected:

dotnet new msharp-gamemode -n MyGamemode --Api ManiaPlanet --BaseClass CSmMode

Other templates in the pack scaffold the other kinds of ManiaScript projects:

Short name Scaffolds --Api choices
msharp-gamemode Game mode (CTmMode / CSmMode) ManiaPlanet, ManiaPlanet3, Trackmania
msharp-library ILib<T> reusable library ManiaPlanet, ManiaPlanet3, Trackmania
msharp-manialink Ingame manialink (CTmMlScriptIngame / CSmMlScriptIngame) + matching .xml ManiaPlanet, ManiaPlanet3, Trackmania
msharp-razor-manialink Single-file Razor ManiaApp page (.razor) ManiaPlanet, ManiaPlanet3, Trackmania
msharp-map-editor-plugin Map editor plugin (CMapEditorPlugin) ManiaPlanet, Trackmania
msharp-server-plugin Server plugin (CServerPlugin) ManiaPlanet, Trackmania
dotnet new msharp-library -n MyLib --Api Trackmania
dotnet new msharp-manialink -n MyManialink --Api Trackmania
dotnet new msharp-razor-manialink -n MyManialink --Api Trackmania
dotnet new msharp-map-editor-plugin -n MyMapEditorPlugin --Api ManiaPlanet
dotnet new msharp-server-plugin -n MyServerPlugin --Api ManiaPlanet

msharp-map-editor-plugin and msharp-server-plugin only support ManiaPlanet/TrackmaniaCMapEditorPlugin/CServerPlugin aren't exposed by the ManiaPlanet3 API.

IDE setup

It is recommended to enable the generator to run on every keystroke instead of only on save, so you can see the generated .Script.txt update in real time as you type.

Default behavior would be to build the project for the generator to run.

Visual Studio Code

Create a .vscode/settings.json in your own project and add this setting:

{
  "dotnet.server.sourceGeneratorExecution": "Automatic"
}
Visual Studio

You have to globally enable it for all projects. Enable the setting "Automatically run generators on any change" in Tools → Options → Text Editor → C# → Advanced → Source Generator.

Contributing

Building from source, running the sample project, and running the test suite are covered in CONTRIBUTING.md.


Table of contents

  1. Type mappings
  2. Variables
  3. Constants
  4. Settings
  5. Host commands
  6. Operators
  7. String handling
  8. Control flow
  9. Functions
  10. Collections (lists & arrays)
  11. LINQ queries
  12. Structs
  13. Vectors
  14. Contexts
  15. Inheritance (RequireContext & Extends)
  16. Library inclusions
  17. Labels
  18. Timing instructions
  19. Event handling
  20. Change detection (OnChange)
  21. Manialink bindings
  22. Netwrites & netreads
  23. Persistent variables
  24. Metadata variables
  25. Pattern matching
  26. Log & assertions
  27. Quick reference table
  28. Conclusion

Type mappings

Primitive types

C# ManiaScript Notes
void Void Function return type only
int Integer Range: -2147483648 to 2147483647
float Real Floating-point; ManiaScript uses trailing dot (99.)
double Real Same as float in ManiaScript
bool Boolean true/falseTrue/False
string Text Double-quoted strings

Collection types

C# ManiaScript Notes
IList<int> Integer[] Ordered list
ImmutableArray<int> Integer[] Same as list (will change)
List<string> Text[] Ordered list
Dictionary<string, int> Integer[Text] Associative array
Dictionary<int, string> Text[Integer] Associative array

Vector types

C# ManiaScript Notes
Vector2 / custom Vec2 <Real, Real>
Vector3 / custom Vec3 <Real, Real, Real>
custom Int3 Int3 <Integer, Integer, Integer>

Nullability

C# ManiaScript Notes
null Null For class references
null (in an Ident context) NullId e.g. Ident? x = null; or x == null
Ident.NullId NullId Static field on the generated Ident struct (itself typed Ident?)

Boolean literals

C# ManiaScript
true True
false False

Variables

Local variables

C#

string name;                  // declaration with type
int count = 42;               // declaration with initializer
var inferred = "hello";       // type inferred

ManiaScript

declare Text Name;
declare Integer Count = 42;
declare Inferred = "hello";

Global variables

Fields become globals with a G_ prefix, regardless of their C# accessibility. Public fields produce warning MSS016; prefer a private, protected, or protected internal field and expose it through a property instead.

C#

public int PreviousTime = -1;
public string ServerName;

ManiaScript

declare Integer G_PreviousTime;
declare Text G_ServerName;

main() {
  G_PreviousTime = -1;
}

Library fields are also emitted as top-level globals in the generated library script. These globals are private implementation state: library functions reference them directly, but a consuming script cannot access them through an include alias (Alias::G_Field is not legal ManiaScript). Expose mutable state as a property or through methods instead.

Library scripts have no generated main() in which to apply arbitrary field initializers. They permit only new() (for an empty struct, list, or dictionary) and the empty string ("") inline. Other initializers report MSS012; initialize the field from a library function instead. Accessing a library field from a consuming script reports MSS013.

Extension variables (for keyword)

Attach a variable to an existing object with Local<T>.For(provider, out var name) — the generator recognizes this call and emits a declare ... for provider; statement instead of a real method call. provider must implement ILocalProvider (most API classes do, e.g. CSmPlayer, CMap, CMlControl).

C#

Local<int>.For(LocalUser, out var someVar);
someVar.Value = 42;

ManiaScript

declare Integer SomeVar for LocalUser;
SomeVar = 42;

someVar is a StrongBox<int> — read/write through .Value; the generator strips .Value since the declared ManiaScript variable itself holds the value.

Aliasing — pass name: to store the variable under an explicit object-side name while the out variable becomes the alias used by the rest of the script (declare X as Y for Z, where X is the declared-for name on the object side and Y is the name used in the script). This is required when two objects of the same type share a variable name, e.g. declaring the same netwrite for two different receivers:

C#

Netwrite<bool>.For(player, out var ready, name: "Net_Lobby_Ready");
ready.Value = true;

ManiaScript

declare netwrite Boolean Net_Lobby_Ready as Ready for Player;
Ready = True;

With name:, the explicit name is emitted as-is — no Net_/Persistent_/Metadata_ prefix is injected, so include any prefix the object side expects.


Constants

C#

const int MaxPlayers = 16;
const string ScriptVersion = "1.2";
const bool EnableDebug = false;

ManiaScript

#Const C_MaxPlayers 16
#Const C_ScriptVersion "1.2"
#Const C_EnableDebug False

Settings

Settings are decorated using the SettingAttribute. They can be constants or read-only fields.

C#

[Setting]
const string AdminLogin = "bigbang1112";

[Setting(As = "Chat time")]
const int ChatTime = 50;

[Setting(As = "Visible name", Translated = true, Hidden = true)]
const int HiddenSetting = 25;

[Setting(Translated = false)]
const int PointLimit = 25;

ManiaScript

#Setting S_AdminLogin "bigbang1112"
#Setting S_ChatTime 50 as _("Chat time")
#Setting S_HiddenSetting 25 as "<hidden>"
#Setting S_PointLimit 25

Hidden = true takes precedence over As and Translated, and always emits as "<hidden>" without _().


Host commands

Apply [Command] to a context class to emit a host-exposed #Command directive. The attribute's type arguments describe the command value; As supplies the visible label. Command handling remains normal event handling through the context API.

C#

[Command("Command_SetPause", typeof(bool), As = "Pause the game")]
public class MyMode : CTmMode, IContext
{
}

ManiaScript

#Command Command_SetPause (Boolean) as _("Pause the game")

Set Translated = false to emit the label without _().


Operators

All basic operators are supported.

Mixing int and float produces Real in ManiaScript.

Ternary (?:) and ??=

ManiaScript has no inline conditional operator. a ? b : c and a ??= b are only translated when they are the entire value of a local declaration, assignment, or return statement — the generator rewrites them into an if/else statement:

C# ManiaScript
int y = x > 0 ? 1 : -1; declare Integer Y;<br>if (X > 0) { Y = 1; } else { Y = -1; }
return x > 0 ? 1 : -1; if (X > 0) { return 1; } else { return -1; }
x ??= 1; if (X == Null) { X = 1; }

Using ?: or ??= anywhere else (e.g. nested inside another expression or as a call argument) cannot be expressed in ManiaScript and is reported as an unsupported construct (MSS003) — extract it into its own statement first.

Casts

ManiaScript has only one numeric type per family (Integer, Real) and no basic-type cast syntax, so explicit C# casts between bool/numeric/string types translate to a MathLib/TextLib conversion call (or a no-op when both sides map to the same ManiaScript type). System.Convert.ToXxx(value) uses the same table, except Real→Integer rounds instead of truncating, matching Convert.ToInt32 semantics:

C# ManiaScript
(float)intValue MathLib::ToReal(IntValue)
(int)floatValue MathLib::TruncInteger(FloatValue) (truncates toward zero)
Convert.ToInt32(floatValue) MathLib::NearestInteger(FloatValue) (rounds)
(long)intValue / (double)floatValue IntValue / FloatValue (no-op; same ManiaScript type)
(int)stringValue TextLib::ToInteger(StringValue)
(float)stringValue TextLib::ToReal(StringValue)
(string)numberOrBool TextLib::ToText(NumberOrBool)
(bool)intValue (IntValue != 0)
(bool)stringValue (StringValue == "True")

There is no ManiaScript expression for a bool→numeric conversion ((int)boolValue, Convert.ToInt32(boolValue), ...) since ManiaScript has no ternary operator — these are reported as unsupported (MSS003); extract the conversion into an if/else assigning 1/0 explicitly instead.

Casts that aren't between basic types (e.g. object/class casts, downcasts) fall back to ManiaScript's as cast syntax, with the target type mapped through the same type table used everywhere else:

C# ManiaScript
(SomeClass)x (X as SomeClass)
x as string (X as Text)

String handling

Concatenation

ManiaScript uses ^ for string concatenation. C# string concatenation and interpolation map to this:

C#

string result = "Hello " + "world!";
string greeting = name + " has " + score + " points.";

ManiaScript

declare Text Result = "Hello " ^ "world!";
declare Text Greeting = Name ^ " has " ^ Score ^ " points.";

String interpolation → multiline strings

C#

string msg = $"Hello {playerName}, score = {2 + 3}";

ManiaScript

declare Text Msg = """Hello {{{PlayerName}}}, score = {{{2 + 3}}}""";

Verbatim / raw strings → multiline strings

C#

string raw = @"no need to escape ""quotes"" or paths\here";

ManiaScript

declare Text Raw = """no need to escape "quotes" or paths\here""";

Generated multiline strings are kept within ManiaScript's 65,535-byte UTF-8 limit. Longer C# verbatim or raw strings are split into byte-safe fragments joined with ^. Literal """ and {{{ sequences are also emitted as ordinary quoted fragments so they remain text rather than being parsed as a delimiter or interpolation.

Escape sequences

C# ManiaScript
"\n" "\n"
"\\" "\\"
"\"" Not needed in """..."""

ToString() on any type

Calling .ToString() on any value — numeric, bool, Ident, Vec2/Vec3/Int2/Int3, or a class reference like CSmPlayer — converts to Text via ManiaScript's auto-coercing ^ operator instead of a TextLib call:

C#

string s = score.ToString();

ManiaScript

declare Text S = "" ^ Score;

Automatic string method mapping (TextLib)

Common System.String instance/static methods and int.Parse/float.Parse translate directly to TextLib:: calls — no explicit TextLib.Method(...) call is required:

C# ManiaScript
s.Length TextLib::Length(s)
string.Empty ""
s.ToUpper() / ToUpperInvariant() TextLib::ToUpperCase(s)
s.ToLower() / ToLowerInvariant() TextLib::ToLowerCase(s)
s.Trim() TextLib::Trim(s)
s.Substring(start, len) TextLib::SubString(s, start, len)
s.Substring(start) TextLib::SubString(s, start, TextLib::Length(s))
s.Contains(v) TextLib::Find(v, s, True, True)
s.StartsWith(v) / EndsWith(v) TextLib::StartsWith(v, s) / TextLib::EndsWith(v, s)
s.Replace(old, new) TextLib::Replace(s, old, new)
s.Split(sep) TextLib::Split(sep, s)
string.Join(sep, items) TextLib::Join(sep, items)
string.IsNullOrEmpty(s) / IsNullOrWhiteSpace(s) s == ""
string.Concat(a, b, ...) a ^ b ^ ...
int.Parse(s) TextLib::ToInteger(s)
float.Parse(s) TextLib::ToReal(s)

These calls emit a bare TextLib:: reference; ManiaScript still requires #Include "TextLib" as TextLib for it to resolve. Declare a field of the built-in TextLib type named TextLib somewhere in the class (see Library Inclusions) so the include directive is generated — otherwise the script won't compile.


Control flow

If / else if / else

C#

if (list.Count > 2)
{
    DoSomething(list);
}
else if (list.Count == 0)
{
    Log("Empty");
}
else
{
    Log("Too few items");
}

ManiaScript

if (List.count > 2) {
    DoSomething(List);
} else if (List.count == 0) {
    log("Empty");
} else {
    log("Too few items");
}

Switch statement

C#

switch (block.Direction)
{
    case CBlock.CardinalDirections.North:
        Log("North");
        break;
    case CBlock.CardinalDirections.South:
        Log("South");
        break;
    default:
        Log("Other");
        break;
}

ManiaScript

switch (Block.Direction) {
    case CBlock::CardinalDirections::North: {
        log("North");
    }
    case CBlock::CardinalDirections::South: {
        log("South");
    }
    default: {
        log("Other");
    }
}

Note: ManiaScript uses :: for enum/class member access, C# uses ..

Switchtype (type checking)

switchtype is only emitted for an actual C# switch statement whose case labels are type patterns. An if/else if chain using is T t stays an if/else if chain (see Pattern Matching) — it is never rewritten into switchtype.

C#

switch (control)
{
    case CMlEntry entry:
        Log(entry.Value);
        break;
    case CMlTextEdit textEdit:
        Log(textEdit.Value);
        break;
    default:
        Log("not an input element");
        break;
}

ManiaScript

switchtype (Control) {
    case CMlEntry: {
        declare Entry = (Control as CMlEntry);
        log(Entry.Value);
    }
    case CMlTextEdit: {
        declare TextEdit = (Control as CMlTextEdit);
        log(TextEdit.Value);
    }
    default: {
        log("not an input element");
    }
}

While loop

C#

int itemCount = 10;
while (itemCount > 0)
{
    itemCount -= 1;
}

ManiaScript

declare Integer ItemCount = 10;
while (ItemCount > 0) {
    ItemCount -= 1;
}

For loop

C#

for (int i = 2; i <= 5; i++)
{
    Log(i.ToString());
}

ManiaScript

for (I, 2, 5) {
    log("" ^ I);
}

ManiaScript's for uses an inclusive range and accepts an optional fourth Step argument. The generator emits the native form for a single declared integer counter whose condition compares that counter with <, <=, >, or >=. It supports ++, --, +=, and -= increments; exclusive C# bounds are adjusted by one because ManiaScript's final value is inclusive:

C#

for (int i = 0; i < 10; i++)      // exclusive upper bound
{
    Log(i.ToString());
}

for (int i = 0; i < 10; ++i)      // pre-increment — same canonical shape
{
    Log(i.ToString());
}

for (int i = 0; i < 10; i += 1)   // += 1 — same canonical shape
{
    Log(i.ToString());
}

ManiaScript

for (I, 0, 10 - 1) {
    log("" ^ I);
}

for (I, 0, 10 - 1) {
    log("" ^ I);
}

for (I, 0, 10 - 1) {
    log("" ^ I);
}
Stepped and reverse loops

Negative and non-unit integer steps are emitted natively. This also preserves C# continue semantics, since the ManiaScript loop performs its step after every iteration:

C#

// Descending
for (int i = 10; i > 0; i--)
{
    Log(i.ToString());
}

ManiaScript

for (I, 10, 0 + 1, -1) {
    log("" ^ I);
}

C#

// Custom step
for (int i = 0; i < 10; i += 2)
{
    Log(i.ToString());
}

ManiaScript

for (I, 0, 10 - 1, 2) {
    log("" ^ I);
}

Non-integer counters, loop variables declared outside the loop, omitted conditions, and multiple counters still fall back to an equivalent while loop:

C#

// Non-integer counter
for (float f = 0f; f < 1f; f += 0.5f)
{
    Log(f.ToString());
}

ManiaScript

declare Real F = 0.;
while (F < 1.) {
    log("" ^ F);
    F += 0.5;
}

C#

// Loop variable declared outside — reused, not redeclared
int i;
for (i = 0; i < 10; i++)
{
    Log(i.ToString());
}

ManiaScript

declare Integer I;
I = 0;
while (I < 10) {
    log("" ^ I);
    I += 1;
}

C#

// Omitted condition — infinite loop, exited via break
for (int i = 0; ; i++)
{
    if (i >= 10) break;
    Log(i.ToString());
}

ManiaScript

declare Integer I = 0;
while (True) {
    if (I >= 10) break;
    log("" ^ I);
    I += 1;
}

Multiple declared loop variables (for (int i = 0, j = 10; ...; ...)) also fall back to while, since ManiaScript's for only has room for a single counter.

Foreach loop

C#

foreach (var item in myList)
{
    Log(item);
}

ManiaScript

foreach (Item in MyList) {
    log(Item);
}

With index/key: C#

foreach (var (index, item) in myArray)
{
    Log($"{index}: {item}");
}

ManiaScript

foreach (Index => Item in MyArray) {
    log(Index ^ ": " ^ Item);
}

.Index() (System.Linq) works on any list, not just plain ordered arrays, so it can't reuse the native key — it desugars into a manually incremented counter instead:

C#

foreach (var (index, item) in myArray.Index())
{
    Log($"{index}: {item}");
}

ManiaScript

declare Integer Index = 0;
foreach (Item in MyArray) {
    log(Index ^ ": " ^ Item);
    Index += 1;
}

Break and continue

C#

foreach (var control in Page.MainFrame.Controls)
{
    if (control is not CMlLabel)
        continue;

    var label = (CMlLabel)control;
    if (label.Value == "match")
    {
        match = label;
        break;
    }
}

ManiaScript

foreach (Control in Page.MainFrame.Controls) {
    if (!(Control is CMlLabel))
        continue;
    declare Label = (Control as CMlLabel);
    if (Label.Value == "match") {
        Match = Label;
        break;
    }
}

Functions

Basic function definition

C#

int Minimum(int a, int b)
{
    if (a < b) return a;
    return b;
}

ManiaScript

Integer Minimum(Integer _A, Integer _B) {
    if (_A < _B) return _A;
    return _B;
}

Conventions applied automatically

C# Convention ManiaScript Output
private method Private_ prefix added
public/internal method No prefix
Parameter int time Integer _Time (PascalCase + underscore)
static keyword Ignored (use for unit testing)
virtual keyword Becomes a label

Void functions

C#

void DoNothing()
{
}

ManiaScript

Void DoNothing() {
}

Private functions

C#

private static string TimeToTextWithMilli(int time)
{
    return $"{TextLib.TimeToText(time, true)}{MathLib.Abs(time % 10)}";
}

ManiaScript

Text Private_TimeToTextWithMilli(Integer _Time) {
    return TextLib::TimeToText(_Time, True) ^ MathLib::Abs(_Time % 10);
}

Function overloading (polymorphism)

ManiaScript supports overloading by argument types:

C#

int Sum(int a, int b) => a + b;
float Sum(float a, float b) => a + b;

ManiaScript

Integer Sum(Integer _A, Integer _B) { return _A + _B; }
Real Sum(Real _A, Real _B) { return _A + _B; }

Named arguments

C# named arguments are preserved as inline comments, since ManiaScript has no equivalent syntax:

C#

DoSomething(enabled: true, count: 5);

ManiaScript

DoSomething(/* enabled: */ True, /* count: */ 5);

Properties (Get/Set functions)

Properties with accessor bodies (or auto-properties) become Get/Set functions, since ManiaScript has no property syntax. Reading the property calls the getter; assigning to it calls the setter.

C#

public int Score { get; set; }              // auto-property

public int Doubled => Score * 2;            // expression-bodied getter

private int _x;
public int Custom
{
    get { return _x; }
    set { _x = value / 2; }
}

ManiaScript

declare Integer G_Score;

Integer GetScore() { return G_Score; }
Void SetScore(Integer _Value) { G_Score = _Value; }

Integer GetDoubled() { return G_Score * 2; }

Integer GetCustom() { return X; }
Void SetCustom(Integer _Value) { X = _Value / 2; }

Auto-properties get a backing global (G_ prefix if public); properties with a custom body do not — the body decides what to read/write. private properties get Private_Get/Private_Set prefixes, same as private methods.

The main() function

The Main() method in IContext generates main(). If simple enough, the entire script can omit the function header.

Collections (lists & arrays)

Lists

C#

List<string> myList = new() { "Alpha", "Beta", "Gamma" };

// Access
var first = myList[0];
var size = myList.Count;

// Mutate
myList.Add("Omega");
myList.RemoveAt(0);
myList.Remove("Beta");
myList.Clear();

// Query
bool exists = myList.Contains("Gamma");
int idx = myList.IndexOf("Alpha");

// Sort
myList.Sort();

ManiaScript

declare Text[] MyList = ["Alpha", "Beta", "Gamma"];

// Access
declare First = MyList[0];
declare Size = MyList.count;

// Mutate
MyList.add("Omega");
MyList.removekey(0);
MyList.remove("Beta");
MyList.clear();

// Query
declare Exists = MyList.exists("Gamma");
declare Idx = MyList.keyof("Alpha");

// Sort
declare SortedList = MyList.sort();
Full list API mapping
C# ManiaScript
.Count .count
.Add(value) .add(value)
.Insert(0, value) .addfirst(value)
.RemoveAt(index) .removekey(index)
.Remove(value) .remove(value)
.Clear() .clear()
.Contains(value) .exists(value)
.IndexOf(value) .keyof(value)
index >= 0 && index < list.Count .existskey(index)
.Sort() / .OrderBy() .sort()
.Reverse() / .OrderByDescending() .sortreverse()
JSON serialize .tojson()
JSON deserialize .fromjson(json)

Associative arrays (dictionaries)

C#

var scores = new Dictionary<string, float>
{
    ["Pi"] = 3.14f,
    ["Tau"] = 6.28f
};

scores["Leet"] = 13.37f;
var pi = scores["Pi"];

ManiaScript

declare Real[Text] Scores = ["Pi" => 3.14, "Tau" => 6.28];

Scores["Leet"] = 13.37;
declare Pi = Scores["Pi"];

TryGetValue (in an if/if (!...) condition) is translated using .existskey() plus an indexer read, since ManiaScript has no out-parameter equivalent:

C#

if (scores.TryGetValue("Pi", out var pi)) { /* use pi */ }
if (!scores.TryGetValue("Pi", out var pi)) { return; } // use pi below

ManiaScript

if (Scores.existskey("Pi")) {
    declare Real Pi = Scores["Pi"];
    /* use Pi */
}
declare Real Pi;
if (!Scores.existskey("Pi")) {
    return;
} else {
    Pi = Scores["Pi"];
}
// use Pi below

Nested collections

C#

var usersData = new List<Dictionary<string, string>>
{
    new() { ["login"] = "me", ["name"] = "still me" },
    new() { ["login"] = "you", ["name"] = "still you" }
};
var login = usersData[0]["login"];

ManiaScript

declare Text[Text][] UsersData = [
    ["login" => "me", "name" => "still me"],
    ["login" => "you", "name" => "still you"]
];
declare Login = UsersData[0]["login"];

Collection expressions (C# 12)

C# collection expression syntax ([...]) produces the same array/list literal:

C#

int[] nums = [1, 2, 3, 4, 5];
int[] empty = [];

ManiaScript

declare Integer[] Nums = [1, 2, 3, 4, 5];
declare Integer[] Empty = [];

LINQ queries

LINQ chains on local variables are desugared into foreach loops at compile time — see linq-translation.md for the full reference (every supported stage and terminal, composition rules, and partially-supported patterns like GroupBy/Zip).

C#

var result = nums.Where(x => x > 0).Select(x => x * 2).ToList();
var cnt = nums.Where(x => x > 5).Count();

ManiaScript

declare Integer[] Result;
foreach (X in Nums) {
    if (X > 0) {
        Result.add(X * 2);
    }
}

declare Integer Cnt = 0;
foreach (X in Nums) {
    if (X > 5) {
        Cnt += 1;
    }
}

Diagnostic MSS008 is reported when a chain has no terminal call (.ToList(), .Count(), .First(), …) to materialise it, or when a method has no ManiaScript equivalent.

Structs

ManiaScript #Struct maps to C# structs or classes with a special attribute:

C#

public struct MyStruct
{
    public int MyMember;
    public string MyTextMember;
}

// Usage
var myVar = new MyStruct();
Log(myVar.MyMember.ToString());    // 0

myVar.MyMember = 1;
var copy = myVar;                  // value copy
myVar.MyMember = 2;
Log(copy.MyMember.ToString());     // still 1

ManiaScript

#Struct MyStruct {
    Integer MyMember;
    Text MyTextMember;
}

main() {
    declare MyStruct MyVar;
    log("" ^ MyVar.MyMember); // 0

    MyVar.MyMember = 1;
    declare MyStruct MyCopy = MyVar;
    MyVar.MyMember = 2;
    log("" ^ MyCopy.MyMember); // 1
}

A C# object initializer maps to a ManiaScript struct literal; fields that are not specified keep their default values:

var value = new MyStruct { MyMember = 1, MyTextMember = "ready" };
var empty = new MyStruct { };
declare MyStruct Value = MyStruct { MyMember = 1, MyTextMember = "ready" };
declare MyStruct Empty = MyStruct {};

When a struct is nested in an included library, declare it through that library's nested C# type. The generator imports it using #Struct Alias::Type as Type, making the type available in the consuming script:

public class StateLib : ILib<CManiaApp>
{
    public required CManiaApp Context { get; init; }

    public struct Snapshot
    {
        public int Count;
    }
}

public class MyMode : CTmMode, IContext
{
    public required StateLib State;
    public StateLib.Snapshot Current;
}

ManiaScript

#Include "StateLib.Script.txt" as State
#Struct State::Snapshot as Snapshot

declare Snapshot G_Current;

Vectors

C#

var v2 = new Vec2(1.0f, 2.0f);
var v3 = new Vec3(1.0f, 2.0f, 3.0f);
var i3 = new Int3(0, 255, 0);

// Access components
float x = v3.X;
float y = v3.Y;
float z = v3.Z;
// or by index
float first = v3[0];

ManiaScript

declare Vec2 V2 = <1.0, 2.0>;
declare Vec3 V3 = <1.0, 2.0, 3.0>;
declare Int3 I3 = <0, 255, 0>;

// Access components
declare Real X = V3.X;
declare Real Y = V3.Y;
declare Real Z = V3.Z;
// or by index
declare Real First = V3[0];

Null checks

C#

if (player == null)
{
    Log("No player");
}

ManiaScript

if (Player == Null) {
    log("No player");
}

Id comparison

C#

var playerId = Players[0].Id;  // Store Ident
// Later...
var player = Players[playerId]; // Retrieve by Id

ManiaScript

declare PlayerId = Players[0].Id;
// Later...
declare Player <=> Players[PlayerId];

Contexts

IContext generates ManiaScript code from Main() (runs once) and Loop() (wrapped in while(True) { yield; ... }).

ManiaScript only requires a main() entry point when the script also declares other functions — bare top-level statements aren't allowed alongside function definitions. So when the class defines nothing besides Main()/Loop(), the code is emitted directly at the top level, with no main() wrapper:

C#

public class MyMode : CTmMode, IContext
{
    public void Main()
    {
        // Runs once at start
    }

    public void Loop()
    {
        // Runs every frame inside while(True) { yield; ... }
    }
}

ManiaScript

#RequireContext CTmMode

// Main() contents here

while (True) {
    yield;
    // Loop() contents here
}

Once the class declares any other function, the generator wraps Main()/Loop() in main() so ManiaScript accepts the file:

C#

public class MyMode : CTmMode, IContext
{
    public void Main()
    {
        Setup();
    }

    public void Loop()
    {
        // Runs every frame inside while(True) { yield; ... }
    }

    void Setup() { }
}

ManiaScript

#RequireContext CTmMode

Void Setup() {
}

main() {
    Setup();

    while (True) {
        yield;
    }
}

One-off scripts (NoLoopAttribute)

Add NoLoopAttribute on your class to suppress the generated while(True) { yield; ... } wrapper entirely. Loop() is never emitted, even if it has a body — useful for scripts that only need Main() to run once and then end.

C#

[NoLoop]
public class MyOneOffScript : CTmMode, IContext
{
    public void Main()
    {
        // Runs once, script then ends — no while(True) loop is generated
    }

    public void Loop()
    {
        // Never emitted
    }
}

ManiaScript

#RequireContext CTmMode

// Main() contents here

Inheritance (RequireContext & Extends)

API class → #RequireContext

C#

public class MyMode : CTmMode, IContext { }

ManiaScript

#RequireContext CTmMode

Custom class → #Extends

The namespace becomes the directory path, class name becomes the file name:

C#

namespace Modes.TrackMania;

public class MyNextMode : MyMode { }

ManiaScript

#Extends "Modes/TrackMania/MyMode.Script.txt"

Library inclusions

Standard libraries (hardcoded)

C#

// Using TextLib, MathLib, TimeLib, AnimLib, MapUnits is automatic
var number = TextLib.ToInteger("1");
var abs = MathLib.Abs(-5);

ManiaScript

#Include "TextLib" as TextLib
#Include "MathLib" as MathLib

declare Number = TextLib::ToInteger("1");
declare Abs = MathLib::Abs(-5);

Automatic math mapping (System.Math / MathF)

System.Math/System.MathF calls also translate directly to MathLib:: — no explicit MathLib.Method(...) call is required (same caveat as TextLib above: a MathLib field must exist for #Include "MathLib" as MathLib to be emitted):

C# ManiaScript
Math.Abs/Sin/Cos/Tan/Asin/Acos(x) MathLib::Abs/Sin/Cos/Tan/Asin/Acos(x)
Math.Atan(x) MathLib::Atan2(x, 1.)
Math.Atan2(x, y) MathLib::Atan2(x, y)
Math.Sqrt/Pow/Exp(x[, y]) MathLib::Sqrt/Pow/Exp(x[, y])
Math.Log(x) MathLib::Ln(x)
Math.Log(x, b) / Log2(x) / Log10(x) (MathLib::Ln(x) / MathLib::Ln(b))
Math.Floor/Ceiling/Round/Truncate(x) MathLib::FloorInteger/CeilingInteger/NearestInteger/TruncInteger(x)
Math.Max/Min/Clamp(...) MathLib::Max/Min/Clamp(...)
Math.Sign(x) (x > 0 ? 1 : (x < 0 ? -1 : 0))
Math.Cosh/Sinh/Tanh(x) Expanded from MathLib::Exp
Math.PI MathLib::PI()
Math.E MathLib::Exp(1.)
Math.Tau (MathLib::PI() * 2.)

Math.Sign/Cosh/Sinh/Tanh expand to inline ternary/exponential expressions since ManiaScript's MathLib has no direct equivalents.

Custom libraries

A custom library implements ILib. When it needs a host context, it can inherit that context and use its members directly:

public class MapDetails : CMap, ILib
{
    public string GetAuthor() => AuthorNickName;
}

The existing ILib<T> form remains available when a Context property is more appropriate:

public class MapDetails : ILib<CMap>
{
    public required CMap Context { get; init; }

    public string GetAuthor() => Context.AuthorNickName;
}

Neither library form emits #RequireContext; that directive is only emitted for IContext scripts.

Add either form as a field on the consuming class. Any public/internal field whose type implements ILib is auto-#Included, using the field name (PascalCase) as the alias:

C#

public class MyMode : CTmMode, IContext
{
    public required Message Message;

    public void Main()
    {
        var version = Message.Version;
    }
}

ManiaScript

#Include "Libs/Nadeo/Message.Script.txt" as Message

main() {
    declare Version = Message::Version;
}

Note: C# uses . for exported member access on the lib field, ManiaScript uses :: on the alias. Include aliases expose functions, #Const, and #Setting values, but not top-level declare globals. Public properties are exported as Get*/Set* functions. User-defined library constants/settings are emitted with C_/S_ prefixes; generated wrappers for official libraries preserve their original names, such as Message::Version. The [Include] attribute only emits a raw #Include directive — it does not give you a callable/accessible member in C#. Use a lib-typed field for anything you actually call into from code.

Pre-built libraries per game

Each game-specific package (ManiaScriptSharp.ManiaPlanet, ManiaScriptSharp.ManiaPlanet3, ManiaScriptSharp.Trackmania) ships ready-made ILib wrapper classes for Nadeo's own official library scripts, generated from the actual .Script.txt files bundled with that package (e.g. Message, Layers2, ScoresTable3, UISync, WarmUp, …). They live under the ManiaScriptSharp.Scripts.Libs.Nadeo namespace, mirroring the real Libs/Nadeo/*.Script.txt folder layout — the Message field used above is one of these, not something you write yourself:

using ManiaScriptSharp.Scripts.Libs.Nadeo;

public class MyMode : CTmMode, IContext
{
    public required Layers2 Layers;
    public required ScoresTable3 ScoresTable;
}

These classes are pure API stubs (their bodies never run) — the real implementation is the matching Libs/Nadeo/*.Script.txt file, resolved by the game at the #Include path shown above.

Manialink ManiaScript can't #Include arbitrary library files — only the handful of truly built-in globals (TextLib, MathLib, TimeLib, AnimLib, MapUnits) get a real #Include line there. So whenever the consuming class is emitted as a manialink (i.e. it has a matching .xml template), every other ILib field — whether one of the pre-built Nadeo libraries above or your own custom class — has its functions copied directly into the manialink script instead, and call sites drop the Alias:: prefix, since no alias/#Include exists anymore:

C#

public class MyManialink : CTmMlScriptIngame, IContext
{
    public required Layers2 Layers;

    public void Main()
    {
        Layers.DestroyAll();
    }
}

ManiaScript

// Inlined lib: Layers2
Void DestroyAll() {
    ...
}

main() {
    DestroyAll();
}

This applies transitively — if an inlined lib itself references another lib (e.g. a custom lib that uses TextLib or another Nadeo library), that nested dependency's #Include or inlining is hoisted up into the manialink script too.

Labels

The closest C# feature to ManiaScript labels is virtual/override methods.

Only parameterless void virtual and override methods can become labels. A label is inserted at its marker, so it cannot receive arguments or return a value.

Defining a label (virtual method)

C#

public class MyMode : CTmMode, IContext
{
    public virtual void OnMapIntroEnd()
    {
        UIManager.UIAll.UISequence = CUIConfig.EUISequence.Playing;
    }

    public void Main()
    {
        OnMapIntroEnd();
    }

    public void Loop() { }
}

ManiaScript

#RequireContext CTmMode

***OnMapIntroEnd***
***
UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
***

main() {
    {+++OnMapIntroEnd+++}
}

Overriding a label

C#

public class MyNextMode : MyMode
{
    public override void OnMapIntroEnd()
    {
        Log("I do something");
    }
}

ManiaScript

#Extends "Modes/TrackMania/MyMode.Script.txt"

***OnMapIntroEnd***
***
log("I do something");
***

Do not call base.OnMapIntroEnd() from an override. The base label contribution is assembled automatically; the generator omits such calls.

Label types

ManiaScript Behavior
+++Label+++ Can be extended multiple times
---Label--- Only the latest definition applies

Timing instructions

Yield

C#

Yield(); // Pause for one frame

ManiaScript

yield;

Sleep

C#

Sleep(1000); // Pause for 1000ms

ManiaScript

sleep(1000);

Wait

C#

Wait(() => SomeCondition); // Pause until condition is true

ManiaScript

wait(SomeCondition);

Practical pattern (timeout with early exit)

C#

var start = Now;
Wait(() => Now > start + 1000);

ManiaScript

declare Start = Now;
wait(Now > Start + 1000);

Event handling

Use C# event handlers to generate ManiaScript event loops:

C#

public class MyManialink : CTmMlScriptIngame, IContext
{
    [ManialinkControl] public required CMlQuad QuadMapName;
    [ManialinkControl] public required CMlEntry EntryInput;

    public void Main()
    {
        QuadMapName.MouseClick += () =>
        {
            ShowCurChallengeCard();
        };

        EntryInput.EntrySubmit += (text) =>
        {
            Log(text);
        };
    }
}

ManiaScript

declare CMlQuad G_QuadMapName;
declare CMlEntry G_EntryInput;

main() {
    QuadMapName = (Page.GetFirstChild("QuadMapName") as CMlQuad);
    EntryInput = (Page.GetFirstChild("EntryInput") as CMlEntry);

    while (True) {
        yield;
        foreach (Event in PendingEvents) {
            switch (Event.Type) {
                case CMlScriptEvent::Type::MouseClick: {
                    switch (Event.Control) {
                        case QuadMapName: {
                            ShowCurChallengeCard();
                        }
                    }
                }
                case CMlScriptEvent::Type::EntrySubmit: {
                    switch (Event.Control) {
                        case EntryInput: {
                            log(Event.CustomEventData[0]);
                        }
                    }
                }
            }
        }
    }
}

Delegates, lambdas, and method references are all supported. Referencing a named method will call it rather than inlining contents. Subscriptions must be registered inside Main() — the generator only scans Main() for += registrations.

Change detection (OnChange)

CNod.OnChange (available on any CNod-derived context, e.g. inside Loop()) detects when a field/property's value changes between calls and runs a callback with the previous value. ManiaScript has no equivalent runtime mechanism, so the generator translates it statically into a backing global plus an if check.

C#

private int _score;

public void Loop()
{
    OnChange(_score, (int oldScore) =>
    {
        Log($"Score changed from {oldScore} to {_score}");
    });
}

ManiaScript

declare Integer Score;
declare Integer OldScore;

while (True) {
    yield;
    if (Score != OldScore) {
        log("Score changed from " ^ OldScore ^ " to " ^ Score);
        OldScore = Score;
    }
}

The first argument must be a direct field/property reference — that's what names the generated backing global (_scoreOldScore).

Binding retrieves manialink elements by ID in a validated, strongly-typed way.

C#

public class MyManialink : CTmMlScriptIngame, IContext
{
    [ManialinkControl]
    private CMlLabel LabelCountdown = null!;

    [ManialinkControl("CustomId")]
    private CMlQuad SomeQuad = null!;

    [ManialinkControl(IgnoreValidation = true)]
    private CMlFrame DynamicFrame = null!;
}

ManiaScript

declare CMlLabel G_LabelCountdown;
declare CMlQuad G_SomeQuad;
declare CMlFrame G_DynamicFrame;

main() {
    G_LabelCountdown = (Page.GetFirstChild("LabelCountdown") as CMlLabel);
    G_SomeQuad = (Page.GetFirstChild("CustomId") as CMlQuad);
    G_DynamicFrame = (Page.GetFirstChild("DynamicFrame") as CMlFrame);
}
  • If no ID is given on the attribute, the field name is used as the XML id.
  • Set IgnoreValidation = true for dynamically-built manialinks.

As an alternative to separate C# and XML files, put a ManiaApp, its markup, and its page script in a component-style MyManialink.razor file. The Razor document itself declares the outer ManiaApp context with @inherits and @implements. A nested IContext class supplies the optional CMlScriptIngame script embedded in the markup:

@using ManiaScriptSharp
@using static ManiaScriptSharp.ManiaScript
@namespace MyMode
@inherits CManiaApp
@implements IContext

<manialink version="3">
    <label id="LabelHello" text="@_title" />
</manialink>

@code {
    private string _title = "Hello from Razor!";
    private CUILayer? _layer;

    public void Main()
    {
        _layer = UILayerCreate();
        _layer.ManialinkPage = Render();
    }

    public void Loop() { }

    public class PageScript : CMlScriptIngame, IContext
    {
        [ManialinkControl] public required CMlLabel LabelHello;
        public void Main()
        {
            LabelHello.Value = "Ready";
        }
        public void Loop() { }
    }
}

The generator adds a Render() method to the outer context. It returns the complete Manialink as a ManiaScript multiline string, including the generated nested <script> block. Razor value expressions such as @_title become runtime ManiaScript interpolation, so Render() can be assigned directly to CUILayer.ManialinkPage. The outer context is emitted as ManiaScript/MyManialink.Script.txt.

Razor control-flow blocks in markup are not currently supported. Use value expressions for text and attributes, and @@ when the XML needs a literal @ character.

Netwrites & netreads

Network-synchronized variables for communication between server and client scripts, declared with Netwrite<T>.For(provider, out var name) / Netread<T>.For(provider, out var name) — the generator recognizes these calls and emits a declare netwrite/declare netread statement. provider must implement INetwriteProvider/INetreadProvider (e.g. CSmPlayer, CScore).

Netwrite (server → client)

C#

Netwrite<int>.For(player, out var netScore);
netScore.Value = 5;

ManiaScript

declare netwrite Integer Net_NetScore for Player;
Net_NetScore = 5;

Netread (client ← server)

C#

Netread<int>.For(player, out var netScore);
Log(netScore.ToString());

ManiaScript

declare netread Integer Net_NetScore for Player;
log("" ^ Net_NetScore);

Netwrite<T>.For yields a StrongBox<T> — read/write through .Value (stripped by the generator). Netread<T>.For yields a plain T instead — read-only, no .Value, since the client can't write network variables back. Pass name: to declare under an explicit object-side name with the out variable as the alias (declare netwrite T X as Y for Z).

Persistent variables

Variables that survive across script restarts (like cookies), declared with Persistent<T>.For(provider, out var name)provider must implement IPersistentProvider (e.g. CMap, CUser, CTmMode).

C#

Persistent<string>.For(LocalUser, out var savedSetting);
savedSetting.Value = "some value";

ManiaScript

declare persistent Text Persistent_SavedSetting for LocalUser;
Persistent_SavedSetting = "some value";

Limited storage per object. Type cannot be changed once set — you must use a new name. Pass name: to declare under an explicit object-side name with the out variable as the alias (declare persistent T X as Y for Z).

Metadata variables

Variables attached to a CNod-derived object's metadata store, declared with Metadata<T>.For(provider, out var name)provider must implement IMetadataProvider (e.g. CMap, CSmBlock, CEditorAsset).

C#

Metadata<int>.For(block, out var difficulty);
difficulty.Value = 3;

ManiaScript

declare metadata Integer Metadata_Difficulty for Block;
Metadata_Difficulty = 3;

Pass name: to declare under an explicit object-side name with the out variable as the alias (declare metadata T X as Y for Z).

Pattern matching

ManiaScript's is keyword only tests — it does not bind a variable. Every C# pattern that introduces a variable therefore expands into an explicit declare cast. More complex C# patterns (and, or, { }) have no direct equivalent and must be decomposed into separate if / switch blocks.

x is T → type test

C#

if (control is CMlLabel label)
{
    Log(label.Value);
}

ManiaScript

if (Control is CMlLabel) {
    declare Label = (Control as CMlLabel);
    log(Label.Value);
}

x is not T → negated type test

C#

if (control is not CMlLabel)
    continue;

ManiaScript

if (!(Control is CMlLabel))
    continue;

x is T { Prop: value } → property pattern

C# property patterns have no equivalent in ManiaScript. They expand into a type check followed by a value check:

C#

if (control is CMlLabel { Value: "target" })
{
    DoSomething();
}

ManiaScript

if (Control is CMlLabel) {
    declare Label = (Control as CMlLabel);
    if (Label.Value == "target") {
        DoSomething();
    }
}

x is T t and conditionand pattern

C# and inside is must be split into a type check block containing a separate condition:

C#

if (control is CMlLabel label and { Value: not "" })
{
    Log(label.Value);
}

ManiaScript

if (Control is CMlLabel) {
    declare Label = (Control as CMlLabel);
    if (Label.Value != "") {
        log(Label.Value);
    }
}

x is T1 or T2or pattern

C# or inside is expands into two separate is checks joined with ||:

C#

if (control is CMlEntry or CMlTextEdit)
{
    Log("input element");
}

ManiaScript

if (Control is CMlEntry || Control is CMlTextEdit) {
    log("input element");
}

When the body needs the cast, use switchtype instead (see below).

switch(x) { case T: }switchtype

When branching on multiple types, C# type-switch patterns map to switchtype:

C#

switch (control)
{
    case CMlEntry entry:
        Log(entry.Value);
        break;
    case CMlTextEdit edit:
        Log(edit.Value);
        break;
    default:
        Log("other");
        break;
}

ManiaScript

switchtype (Control) {
    case CMlEntry: {
        declare Entry = (Control as CMlEntry);
        log(Entry.Value);
    }
    case CMlTextEdit: {
        declare Edit = (Control as CMlTextEdit);
        log(Edit.Value);
    }
    default: {
        log("other");
    }
}

For single-expression branches the cast can be inlined:

switchtype (Control) {
    case CMlEntry: log((Control as CMlEntry).Value);
    case CMlTextEdit: log((Control as CMlTextEdit).Value);
    default: log("other");
}

Summary

C# is pattern ManiaScript translation
x is T X is T
x is T t if (X is T) { declare t = (X as T); }
x is not T !(X is T)
x is T { P: v } if (X is T) { declare t = (X as T); if (t.P == v) { } }
x is T t and cond if (X is T) { declare t = (X as T); if (cond) { } }
x is T1 or T2 X is T1 \|\| X is T2
switch(x) { case T t: } switchtype (X) { case T: { declare t = (X as T); } }

Log & assertions

C#

Log("Something went wrong!");       // prints to debug console (Ctrl+~)
Assert(myVariable == 3);            // halts script if false

ManiaScript

log("Something went wrong!");
assert(MyVariable == 3);

Console.Write/Console.WriteLine are also mapped to log(...), for code that's shared with regular .NET tests:

C#

Console.WriteLine(score);

ManiaScript

log(Score);

Quick reference table

C# Feature ManiaScript Equivalent
Class inheriting API class #RequireContext
Class inheriting custom class #Extends "path.Script.txt"
IContext interface main() + while(True) { yield; }
Main() method Code before while loop in main()
Loop() method Code inside while loop
const field #Const C_Name
[Setting] attribute #Setting S_Name
[Command("Name", typeof(T))] #Command Name (T)
field declare G_Name (global; public fields warn with MSS016)
private method Private_FunctionName()
Method parameters PascalCased with _ prefix
virtual method Label (***LabelName***)
override method Label extension
[ManialinkControl] field Page.GetFirstChild() binding
Event handler (+=) foreach (Event in PendingEvents)
OnChange(value, old => { ... }) Backing global + if (Value != OldValue) { ...; OldValue = Value; }
String interpolation $"" Multiline string """..{{{expr}}}..."""
String concatenation + ^ operator
IList<T> / List<T> T[] list
Dictionary<K,V> V[K] associative array
Namespace path File path for #Extends
. member access on enums/classes :: in ManiaScript
true / false True / False
null Null
is type check is / switchtype
Cast (Type)x (X as Type)
yield return concept yield; (pause 1 frame)
Thread.Sleep(ms) sleep(ms)
SpinWait.SpinUntil(cond) wait(condition)
Netwrite<T>.For(provider, out var x) declare netwrite
Netread<T>.For(provider, out var x) declare netread
Persistent<T>.For(provider, out var x) declare persistent
Metadata<T>.For(provider, out var x) declare metadata
Local<T>.For(provider, out var x) declare ... for provider (extension var)
Xxx<T>.For(provider, out var y, name: "x") declare ... X as Y for provider (alias)
struct #Struct
Vector2 / Vector3 Vec2 / Vec3
for (i = a; i <= b; i++) for (I, a, b)
foreach (x in list) foreach (X in List)
foreach with index foreach (Key => Val in Array)
break break;
continue continue;
LINQ chain (Where/Select/...) Desugared foreach loop (see LINQ Queries)
Collection expression [1, 2, 3] [1, 2, 3]
Named argument f(x: 1) f(/* x: */ 1)
Auto-property / property with body Get/Set functions
x.ToString() (any type) "" ^ X
Console.WriteLine(x) / Console.Write(x) log(x)
Math.Abs(x) etc. MathLib::Abs(x) etc. (auto-mapped)
s.ToUpper(), int.Parse(s), etc. TextLib:: calls (auto-mapped)

Conclusion

This project does not replace ManiaScript, nor text editor extensions that support ManiaScript. This is just an alternative way to be more productive in ManiaScript by using a language that you prefer more, which some may not agree with, and that is understandable. For code generation and unit testing though, this may not be the worst project. Just note that unit testing is just a theory that wasn't yet implemented.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-alpha.5 37 9/21/2026
1.0.0-alpha.2 47 9/15/2026
1.0.0-alpha.1 80 8/13/2026

- Added XML doc generation