ManiaScriptSharp.ManiaPlanet3
1.0.0-alpha.5
dotnet add package ManiaScriptSharp.ManiaPlanet3 --version 1.0.0-alpha.5
NuGet\Install-Package ManiaScriptSharp.ManiaPlanet3 -Version 1.0.0-alpha.5
<PackageReference Include="ManiaScriptSharp.ManiaPlanet3" Version="1.0.0-alpha.5" />
<PackageVersion Include="ManiaScriptSharp.ManiaPlanet3" Version="1.0.0-alpha.5" />
<PackageReference Include="ManiaScriptSharp.ManiaPlanet3" />
paket add ManiaScriptSharp.ManiaPlanet3 --version 1.0.0-alpha.5
#r "nuget: ManiaScriptSharp.ManiaPlanet3, 1.0.0-alpha.5"
#:package ManiaScriptSharp.ManiaPlanet3@1.0.0-alpha.5
#addin nuget:?package=ManiaScriptSharp.ManiaPlanet3&version=1.0.0-alpha.5&prerelease
#tool nuget:?package=ManiaScriptSharp.ManiaPlanet3&version=1.0.0-alpha.5&prerelease
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?
- Easier syntax
- Accurate auto-complete
- Reduced code bloat
- 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]
- The generator's
SyntaxProviderwatches everyClassDeclarationSyntaxwhose base referencesIContextorILib. - For each match,
ScriptEmitterwalks the syntax tree with aSemanticModeland produces ManiaScript text following the C# to ManiaScript conversion reference below. BuildSettingsreads compiler-visible MSBuild properties to set the output folder and indentation.- The emitted text is written via
File.WriteAllText, and a tiny marker// generatedC# file is added throughSourceProductionContext.AddSourceso 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/Trackmania —
CMapEditorPlugin/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
- Type mappings
- Variables
- Constants
- Settings
- Host commands
- Operators
- String handling
- Control flow
- Functions
- Collections (lists & arrays)
- LINQ queries
- Structs
- Vectors
- Contexts
- Inheritance (
RequireContext&Extends) - Library inclusions
- Labels
- Timing instructions
- Event handling
- Change detection (
OnChange) - Manialink bindings
- Netwrites & netreads
- Persistent variables
- Metadata variables
- Pattern matching
- Log & assertions
- Quick reference table
- 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/false → True/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;
someVaris aStrongBox<int>— read/write through.Value; the generator strips.Valuesince 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 — noNet_/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 TextLibfor it to resolve. Declare a field of the built-inTextLibtype namedTextLibsomewhere 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 towhile, since ManiaScript'sforonly 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.privateproperties getPrivate_Get/Private_Setprefixes, 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/Tanhexpand to inline ternary/exponential expressions since ManiaScript'sMathLibhas 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#Settingvalues, but not top-leveldeclareglobals. Public properties are exported asGet*/Set*functions. User-defined library constants/settings are emitted withC_/S_prefixes; generated wrappers for official libraries preserve their original names, such asMessage::Version. The[Include]attribute only emits a raw#Includedirective — 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.
Libraries in Manialink scripts (inlining)
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
TextLibor another Nadeo library), that nested dependency's#Includeor 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 scansMain()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 (_score → OldScore).
Manialink bindings
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 = truefor dynamically-built manialinks.
Single-file Razor 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>.Foryields aStrongBox<T>— read/write through.Value(stripped by the generator).Netread<T>.Foryields a plainTinstead — read-only, no.Value, since the client can't write network variables back. Passname: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 condition → and 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 T2 → or 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 | Versions 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. |
-
.NETStandard 2.0
- ManiaScriptSharp (>= 1.0.0-alpha.5)
-
net10.0
- ManiaScriptSharp (>= 1.0.0-alpha.5)
-
net8.0
- ManiaScriptSharp (>= 1.0.0-alpha.5)
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