NimbyScript
Purpose
NimbyScript is an imperative, statically typed, AOT native compiled language for scripting gameplay extensions for NIMBY Rails. It compiles down to native machine code, just like C. It has zero calling and data marshalling overhead with C/C++: a NimbyScript function is a C function. NimbyScript data types are C types. This capability makes its compiled code much faster than the usual scripting languages used in game development, both interpreted or JIT compiled. NimbyScript is AOT compiled, but the compiler is bundled as part of NIMBY Rails, and scripts are automatically compiled on (re)load from their source text, making the development experience as seamless as interpreted scripts.
NimbyScript syntax and semantics are inspired by Rust, C and C++. But NimbyScript is not designed to be a stand alone language, and as such its bare bones standard library lacks most features found in general purpose languages and runtimes, like file I/O, networking or threads. It is an explicit goal of the language and its runtime environment to crash as little as possible. Crashing and hanging should only happen due to bugs in the NimbyScript compiler, its runtime or NIMBY Rails C++ code, never due to errors in NimbyScript code. To accomplish this the language itself lacks some common features like general purpose iteration, integer division or arbitrary setting of pointers and references, and most data exposed to scripts is read-only, strictly enforced by the language. Any expression result which could result in an out of bounds or invalid memory access must be checked for validity, and the API design together with the type system enforces it.
This documents assumes some familiarity with compiled, native code languages like C++ or Rust. Experience with other languages can also apply, since the basic building blocks like functions and structs/objects are similar to popular languages like Javascript or Python, but other concepts like references, pointers and values will be less clear. For this reason I won’t really explain what a function or a struct is, and instead I will focus on what makes NimbyScript different from C++/Rust in concepts like references.
Syntax
The description of each syntax category starts with a PEG (Parsing Expression Grammar) description, which is copy pasted directly from the source code of the NimbyScript compiler. Its syntax is recognized by the cpp-peglib library.
Comments are introduced by // and end at the end of the current line. Whitespace is not relevant (like C++ or Rust or Javascript, unlike Python).
Top level
TopLevel <- (TopMeta / ConstDecl / FnDecl / StructDef / EnumDef)*
The top level of a NimbyScript source file is composed of a sequence of script meta, const definitions, function definitions, struct definitions and enum definitions.
Example:
script meta {
lang: nimbyscript.v1,
api: nimbyrails.v1,
}
pub struct SpeedTrap extend Signal {
max_speed: f64,
}
const ms_to_kmh: f64 = 3.6;
pub fn SpeedTrap::event_signal_check(
self: &SpeedTrap,
ctx: &EventCtx,
train: &Train,
motion: &Motion,
signal: &Signal
): SignalCheck {
let sc &= ctx.db.view<SpeedTrap>(signal) else { return SignalCheck::Pass; }
if let presence &= motion.presence.get() {
if presence.speed * ms_to_kmh > sc.max_speed {
log("speedy @!", motion.train_id);
return SignalCheck::Stop;
}
}
return SignalCheck::Pass;
}
script meta
TopMeta <- 'script' Meta
The top level script meta declares some meta information for the scripting runtime about your script. In the current scripting runtime it must declare the language version as lang: nimbyscript.v1,, and the API version as api: nimbyrails.v1,. This meta declaration is mandatory and exactly one should be specified per source file.
Example:
script meta {
lang: nimbyscript.v1,
api: nimbyrails.v1,
}
const
ConstDecl <- 'const' NAME ':' Type '=' Constant ';'
const definitions associate a name with a (typed) numeric constant. They are similar to Rust const or C++ constexpr, but much more limited in capabilities. They are not equivalent to C #define since they are typed. Constants are not variables: they cannot be assigned to, and their address cannot be referenced. They can only be used in place of a literal numeric constant.
Example:
const pi: f64 = 3.14159265358979323846; const ms_to_kmh: f64 = 3.6;
struct
StructDef <- VisMod? 'struct' NAME StructExtend? '{' (!'}' (StructField / (Meta ',')) )* '}'
StructExtend <- 'extend' NAME
StructField <- NAME ':' Type Meta? ','
struct definitions introduce new data types within the script. Structs are a data type which contains a sequence of fields, each with a name and a type. When expressions access a value or reference of the type of the struct, it can access the individual fields with a struct.field notation.
Structs can extend one of the following game types:
SignalStationTrainLineScheduleTagScript(with restrictions)
Structs extending game types for player extensions must be declared as pub. When a struct extends one of these types, and its script is in use in the game session, it will be offered as an extension in the game editor UI, for each corresponding editor. If the player enables the extension for a particular object of a game type, a new object of the script struct is created, and the player is then able to give values to the fields from the game UI. This is the only way to create objects of extension structs. Only one object of a given script struct type is allowed per game object.
To read struct objects from script code they must be looked up using the DB::view<ScriptStruct>(bj: &GameObject): *ScriptStruct API. Therefore only functions provided with DB object (or a context object with contains DB) can look up these objects. But you can pass around DB& as a parameter without any lifetime restrictions, since it’s a purely read only API. This API also works for non-pub structs.
Example:
pub struct Example extend Signal {
v: i64,
}
fn f(ctx: &EventCtx, signal: &Signal) {
let config &= ctx.db.view<Example>(signal) else { return; }
log("v = @", config.v);
}
pub structs
As explained earlier, pub structs are the user interface of your scripts. They are exposed in the UI as input forms, and their metas can give some control over this rendering. Conceptually they are owned by the player of the game as part of the game object database, by attaching them to a game model object in the database, not the script. Scripts do not have any capability to create, edit or delete objects of pub structs. It is always the player who decides when to create a new object of these structs by interacting in the UI. Scripts can then read, and only read, the content of these objects, by using the DB::view<ScriptStruct>(bj: &GameObject): *ScriptStruct API presented earlier.
pub structs have a very powerful capability: they have limited support for forward schema evolution. This means that the script developer can add to, and reorder, fields in pub structs, and the game runtime will be able keep the contents of the other fields intact. This is very important to respect the input data by the player. If you rename pub structs or their fields, or change their types, the player input data will be lost.
Non-pub (or private) structs
Non-pub structs, also called private structs, are conceptually owned by the script as a part of the simulation, not the player. Their contents are assumed to be a black box by the rest of the game. Scripts are allowed to create and modify objects of these structs during their execution, which are automatically deleted at the end of the simulation frame. It is also possible to attach these structs to existing game model objects to extend their lifetime beyond the current frame. In this case their lifetime becomes the same as the game object, and they can be retrieved in later frames for reading, deleted or overwritten as desired.
Private structs do not support schema evolution. Changing field types, field names, reordering of fields, even just adding new fields at the end, will always result in losing all existing attached objects of the given struct. Some operations like resetting the simulation will also erase attached objects, because they are considered simulation state, not player created objects.
Private structs support dynamic memory allocation. All private struct definitions automatically create a new and a clone method for the struct:
struct PrivData {
v: i64,
}
fn f(ctx: &EventCtx, signal: &Signal) {
let ro &= ctx.db.view<PrivData>(signal) else { return; }
let rw &mut= PrivData::clone(ro);
rw.v = 100;
let empty &mut= PrivData::new();
empty.v = 200;
}
The objects allocated by new and clone are always deleted at the end of the current simulation frame. Their lifetime can be extended beyond the current frame by attaching them to a game object, like Train or Signal, so their lifetime becomes the same as the attached object. The SimController API has methods for attaching and erasing these object.
Host structs
Many of the exposed game APIs are also based around structs. These structs do not follow any of the previous rules. Some have special capabilities, for example simple structs like Pos or ID<Signal> are value objects: they can be copied and passed around like any i64 would. Some others are strictly locked down and serve as very little beyond a way to keep a pointer safe. In general, when working with game API structs, assume they are what would be called move only in C++. And NimbyScript has no move operator or semantics, so in essence assume they are immutable, immobile references, except when documented otherwise (like Pos).
As an aside, all script declared struct (pub or not) are also always move only, and always “owned” by some external entity. This is why the APIs for their manipulation are always based on pointers and references, even for private structs. It is not possible to create plain, stack allocated values of script structs, or to use them as fields in structs.
Field types
Only a subset of field types are accepted with proper editing experience in the current language version:
- Boolean values:
bool - Integer values:
i64 - Floating point values:
f64 - Enum values: any script-defined
enum(but not game defined enums) - For pub structs:
ID<Line>,ID<Train>,ID<Schedule>,ID<Signal>,ID<Tag> - For private structs:
ID<Track>,ID<Building>,ID<Station>,ID<Line>,ID<Train>,ID<Schedule>,ID<Signal>,ID<Tag>
In pub structs these types display the proper UI for editing them, including the ID<Object> types.
struct meta
struct definitions can contain a struct-level meta:
pub struct ProbeCheck extend Signal {
meta {
label: "Probe a track position",
},
// ...
}
The label meta field will replace the name of the struct in UI for its content.
It is also possible to define a meta for each field:
pub struct ProbeCheck extend Signal {
probe: ID<Signal> meta {
label: "Probe",
},
}
- The
labelmeta field is accepted for all field types. - The
defaultmeta field is accepted forbool,i64,f64andenumfield types. - The
minandmaxmeta field is accepted fori64andf64field types.
enum
EnumDef <- 'enum' NAME '{' (!'}' EnumOption)* '}'
EnumOption <- NAME Meta? ','
Enums are a scoped set of names, with no semantics or values associated to them (unlike C enums). It is possible to pass and create values of enum types, and to check if these types match one of the enum options, but the values themselves have no meaning and cannot be expressed as any other type. Enums as a type can be referred by their name, and each option can be referred like EnumName::OptionName.
Enums are most often used to represent abstract states or settings. It is also a good idea to use them in place of booleans, since having to write out the full name of the enum and the option makes the code self-documenting.
Example:
enum Color { Red, Green, Blue, }
fn pick_color(v: i64, c: Color): Color {
if v < 100 {
return Color::Red;
}
if c == Color::Green {
return Color::Blue;
}
return Color::Blue;
}
When using enums in structs, they will be displayed as a drop down menu in the game UI, and you can modify the option labels with meta:
enum Speed {
Slow meta { label: "Allow slow trains" },
Fast meta { label: "Allow fast trains" },
}
fn definition
FnDecl <- VisMod? 'fn' ColonNAME '(' FnArgs? ')' (':' Type)? BlockStmt
FnArgs <- FnArg (',' FnArg)*
FnArg <- NAME ':' Type
VisMod <- 'pub'
Functions, introduced by fn, define a series of parameters and contain a sequence of executable statements. All executable code is contained in one or more functions, there is no top level statements. Functions are always invoked from the game runtime in response to certain events, and there is no way to independently execute code by scripts. The pub qualifier is required for functions which are meant to be called from the game runtime, and it can be omitted for functions which are only called from within the script.
Example:
fn add(a: i64, b: i64): i64 {
return a + b;
}
Some functions are called “methods”, when said functions are meant to be called with a reference of a specific object type as its first parameter, or for collecting them under the namespace of a certain object. The game extension scripting interface is based on naming certain pub script functions as methods of extended structs. As an analogy, if defining structs as struct extend is used to extend game object data, defining functions as methods with a certain name is used to extend the game behavior (code).
meta
Meta <- 'meta' MetaMap
MetaVal <- MetaMap / MetaVec / MetaStr / MetaName / NumberText
MetaMap <- '{' MetaKV? ( ',' MetaKV )* ',' '}'
MetaVec <- '[' MetaVal? ( ',' MetaVal )* ',' ']'
MetaKV <- MetaName ':' MetaVal
MetaStr <- '"' [^"]* '"'
MetaName <- < [a-zA-Z_][a-zA-Z_0-9.]* >
meta is a structured comment, which can appear associated to certain syntax elements. It has no effect in the compilation or semantics of NimbyScript source code. Its purpose is to decorate some elements with extra information for the game runtime, like UI presentation hints, which have no relevance for compiled code. Metas are similar in semantics and expressiveness to JSON. Root meta information is a map of of key-value pairs. Keys are always a name complying with the identifier rules of the language. Meta values are one of maps, vectors, strings, names (which are just strings complying with identifier rules, not requiring quotes) and numbers.
Statements
BlockStmt <- '{' (!'}' Stmt)* '}'
Stmt <- IfBindStmt / IfStmt / ForStmt / BindElseStmt / BorrowStmt / BindStmt / AssignStmt / BlockStmt / RetStmt / BreakStmt / ContinueStmt / LogStmt / ExpStmt
The body of a function is composed of a sequence of statements. Statements are the executable instructions of code; if types (like struct and enum) define the “shape of data”, statements define how to process that data. Instructions are executed in order, starting from the first statement in a block of statements.
Variables and assignment
BindElseStmt <- 'let' BindBody 'else' BlockStmt BindStmt <- 'let' BindBody ';' BindBody <- NAME BindTypeSep? TypePattern? '=' Exp BindTypeSep <- ':' AssignStmt <- DotExp '=' Exp ';'
New variables are introduced as parameters to a function, and by using the let syntax. Some syntax elements of let are optional, in particular the type signature does not need to be fully specified (this is called type inference in other languages, but I do not claim have a formal, consistent type inference capability in NimbyScript). All of the following variable definitions are equivalent:
fn (a1: i64) {
let a2 = 1;
let a3: i64 = 1;
}
NimbyScript variables cannot be reassigned in the sense they can in C or Rust. This distinction is not important for simple, value-based types like i64, but it is important for struct types and references. In particular it means a reference variable has exactly one chance to take the address of an object: at initialization. All other assignment operations on references always reassign into the original object. This is identical to C++ references, and unlike Rust borrows.
let a = 1;
let r &= a; // the type of r is &mut i64 and it points to a memory address; it is an alias of a, forever
log("a: ", a); // output is "1".
let else is a special case of let which expects an expression evaluating into a pointer on the right side of the equals, but it will create a reference rather than a pointer. Its goal is to be able to transform pointers into references safely, by explicitly handling the invalid case inside the else block statement. The else statement must always return.
// ctx.db.view<SpeedTrap>(signal) returns *SpeedTrap, which is a pointer
// pointers can be invalid, therefore to use them as references your
// code has to prove to the compiler it is handling the invalid case
// one way of doing so is using let-else as this:
let sc &= ctx.db.view<SpeedTrap>(signal) else { return SignalCheck::Pass; }
Assignment of variables is possible when the type of the variable is mut. The left side of an assignment is just a variable name or struct field expression, and the right side is any expression which is compatible with the left side type:
let a mut= 1;
a = 10;
log("a: ", a); // 10
Flow control
IfBindStmt <- 'if' 'let' BindBody BlockStmt ('else' (IfStmt / IfBindStmt / BlockStmt))?
IfStmt <- 'if' Exp BlockStmt ('else' (IfStmt / IfBindStmt / BlockStmt))?
ForStmt <- 'for' NAME 'in' Exp BlockStmt
BreakStmt <- 'break' ';'
ContinueStmt <- 'continue' ';'
RetStmt <- 'return' Exp? ';'
NimbyScript supports a subset of the usual control flow statements found in C++. if statements are almost full featured, with a Rust-like syntax:
if condition() {
}
else if a != b {
}
else {
}
A special, limited case of Rust if let or C++ if (auto* v = ...) is also supported, only for expressions evaluating into pointers. It is similar and has the same motivation as let else:
// another way of handling pointers safely is by proving to the compiler
// the code using the pointer as a reference is inside an if branch
// which has checked it for validity:
if let sc &= ctx.db.view<SpeedTrap>(signal) {
let v = sc.max_speed;
}
for statements are very limited in the current version of NimbyScript. This is a deliberate design choice. Currently it is only possible to iterate using iterator types defined in the game host runtime. Free loops and loops with arbitrary conditions are not possible. continue and break are supported.
for hit in extrapolator.reservation_probe(pos) {
if !hit.train.id.equals(motion.train_id) {
return SignalCheck::Stop;
}
}
return ends the execution of the current function and returns to the caller. Its value is required when the function returns a value.
Expressions
ExpStmt <- Exp ';'
Exp <- EqExp (BoolOp EqExp)?
BoolOp <- '&&' / '||'
EqExp <- RelExp (EqOp RelExp)?
EqOp <- '==' / '!='
RelExp <- AddExp (CmpOp AddExp)?
CmpOp <- '<' / '>' / '>=' / '<='
AddExp <- MulExp (AddOp MulExp)*
AddOp <- '+' / '-'
MulExp <- DotExp (MulOp DotExp)*
MulOp <- '*' / '/' / '%'
DotExp <- AtomExp ('.' NAME)* MethodArgsExp?
MethodArgsExp <- '(' CallArgs? ')'
CallExp <- ColonNAME '(' CallArgs? ')'
CallArgs <- Exp (',' Exp)*
UnaryExp <- UnaryOp Exp
UnaryOp <- '!' / '-'
AtomExp <- AtomPar / CallExp / Constant / UnaryExp / ColonNAME
AtomPar <- '(' Exp ')'
Expression support is similar to classic C, with some quirks around operator precedence. You might need to use parentheses more often than in other languages to ensure the semantics of your expression, but in practice it does not happen often.
“Dot expressions” to address struct fields support chaining like variable.a.b.c, but method calls like variable.method() only support one level of chaining. This can be worked around with parentheses like (variable1.method1()).method2() but it’s usually a better idea to use some intermediate variables.
As explained elsewhere, pointer types cannot be deferenced, therefore cannot be used in dot expressions. But built-in functions is_null(v): bool and is_valid(v): bool are usable with any pointer type and value.
Type matching in expressions is very strict. You cannot subtract an i64 from an f64, for example, but you can compare them. To convert types into other types there’s a family of built-in functions called as_xxx(), where xxx is the desired return type. For example as_i64(v: f64): i64 is one of them. This is equivalent to the C++ cast of int64_t(v). The standard library also has some rounding functions for the specific case of float to integer conversion.
Integer division and remainder operators trigger a compilation error, to avoid division by 0 errors at runtime. Use the zdiv and zmod library functions to do integer division, which return 0 when the divisor is 0.
Usage of variables in expressions is regulated by a set of borrow checking rules, which are a very simplified version of the Rust borrow checking rules. The design of the game APIs has been carefully done to avoid introducing difficult borrowing situations: virtually all data offered to scripts is read-only, making borrow checking trivial and unrestricted (this also enables seamless multithreading support for scripts). For more information on the borrow checker see this post.
Types
Type <- TypeStore? TypeMut? TypeName
TypeName <- NAME ('<' TypeTplArgs '>')? ('::' TypeName)*
TypeTplArgs <- TypeName (',' TypeName)*
TypePattern <- TypeStore? TypeMut? TypeName?
TypeMut <- 'mut'
TypeStore <- '&' / '*'
A NimbyScript type, as used in parameters and variables, is composed of 3 elements: the storage type, the mutability flag and the value type.
The storage of a variable type indicates if it’s storing the full value of a type, or its address. Address types are references & and pointers *. References are very much like C++ references. Pointers are like a “unusable” reference: you cannot dereference it, but if you can prove to the compiler you are checking it for validity, it will allow you to assign it as a reference.
Value storage variables do not have any special character. You will see some types, specially types from the game host API, cannot be used as value variables, forcing you to always use pointers or references to them.
Native NimbyScript value types are very limited: integer ('i64' / 'u64' / 'i32' / 'u32' / 'i16' / 'u16' / 'i8' / 'u8'), floating point ('f64' / 'f32'), booleans (bool), and enum values. Under restricted situations it is also possible to create variables of simple C-like types from the game host, like the Pos type, or iterator types. Despite this simplicity the previous type definition grammar has support for the entire set of C++ type naming. This is because although NimbyScript cannot create new objects of these types, it can access them as struct references, therefore it needs to be able to fully express these type names.
The mutability flag is specified with mut and it indicates mutating operations can be invoked on the variable. For simple types like i64 this can mean overwriting it with any other value, like v = 2;. For structs this usually means you are allowed to call functions which take a &mut to the variable, or to directly assign values to their fields like v.f = 1;. Only a few simple struct types are allowed to be fully reassigned, like Pos.
Constants
Constant <- Number / Time / BoolConst Number <- NumberText NumberType? NumberText <- < '-'? [0-9.]+ > NumberType <- 'i64' / 'u64' / 'i32' / 'u32' / 'i16' / 'u16' / 'i8' / 'u8' / 'f64' / 'f32' Time <- TimeNumber TimeOp (TimeNumber? TimeOp)? (TimeNumber? TimeOp)? TimeNumber? TimeNumber <- < [0-9]+ > TimeOp <- ':' BoolConst <- 'true' / 'false'
Constant expressions are terminal syntax tokes which represent a typed value. For integer numbers, given the very strict type matching rules, you might need to use suffix annotations, like: let v = 1i32; let w = 1i32 + v;.
Identifiers
NAME <- < [a-zA-Z_][a-zA-Z_0-9]* > ColonNAME <- < [a-zA-Z_][a-zA-Z_0-9:]* >
The names you can use for your types, functions and variables are limited to the usual plain C restrictions. An exception is made for function names, since some C++ APIs expect you implement functions as a method of a struct, so the colon character is also allowed.
Debug logs
LogStmt <- 'log' '(' '"' LogStr '"' (',' CallArgs+)? ')' ';'
LogStr <- [^"]*
The built-in log() generic function is used for debug logging. log() takes a text string (this is one and only allowed use of strings in NimbyScript), and then zero to N arguments. It will automatically serialize these arguments as text, and it has some smarts for some game objects, like automatically printing train names if the argument is a reference to a train. Logging has a large CPU impact and it is always disabled by default; explicit manual action in the game UI by the player is required to enable it on each game session.
Extensions
Extending game database struct data with extend can also be used to extend the game code. The mechanism to do so is to first declare a struct as extending a game database object (with or without fields). Then create a specifically named and parametrized function which will be called by the game under certain situations, for certain kinds of objects.
All extension functions take a self parameter which is a reference to the struct which it is method of, called Example in the following sections. Replace it with your struct name. The rest of the parameters are fixed and must be reproduced exactly as-is.
Events
Event functions are called as part of the core simulation engine. They are meant to be focused in features and quick to execute. These functions might be called thousands of times per simulation frame, including multiple redundant times. Their capability to change the simulation behavior is usually very limited.
Signal event_signal_check
pub struct Example extends Signal { ... }
pub fn Example::event_signal_check(
self: &Example,
ctx: &EventCtx,
train: &Train,
motion: &Motion,
signal: &Signal
): SignalCheck { ... }
event_signal_check script functions must be defined as a method of a struct which extends the game object Signal. They must have the exact same parameters and return type as in the previous example.
event_signal_check are called when the player enables their respective struct as an extension for a given signal in the track editor. From that moment the game simulation system which advances the train motion simulation (the Extrapolator) will call your event_signal_check function whenever said signal is checked as part of a path check, and it is a path signal.
This function can be called multiple times per frame per train, redundantly. Your code must return a SignalCheck enum value of SignalCheck::Pass or SignalCheck::Stop. Your code can only make the signal more restrictive, not less. In other words, your Pass will be ignored if the default path check fails, or if another script call returns Stop. But your Stop won’t be ignored if the patch check was successful, and it will stop the train.
Signal event_signal_lookahead
pub struct Example extends Signal { ... }
pub fn Example::event_signal_lookahead(
self: &Example,
ctx: &EventCtx,
train: &Train,
motion: &Motion,
signal: &Signal,
train_distance: f64,
check: SignalCheck,
result: &mut SignalLookaheadResult
) { ... }
event_signal_lookahead script functions must be defined as a method of a struct which extends the game object Signal. They must have the exact same parameters and return type as in the previous example.
event_signal_lookahead are called when the player enables their respective struct as an extension for a given signal in the track editor. From that moment the game simulation system which advances the train motion simulation (the Extrapolator) will call your event_signal_lookahead function at regular intervals for each signal 15km ahead of the train path. It is called for all signals, until one of these conditions hold: - One of the signals path checks as Stop - Another train is found - The train stop is found - 15km have been checked
For both Pass and Stop signals, your event_signal_lookahead will be called, and you can check the path find result in check, and the distance from the train in train_distance.
result.max_speed allows to set a maximum speed for the train, which goes into effect immediately. This works from any distance and past any amount of signals between the train and the caller signal. If multiple signals in the lookahead path return a max_speed, the slowest speed is picked. If no max_speed is set, or it is 0, the result is ignored.
result.max_speed default value is 0, which is considered invalid and will be ignored. To stop the train at the signal implement event_signal_check.
Signal event_signal_change_path
pub struct Example extends Signal { ... }
pub fn Example::event_signal_change_path(
self: &Example,
ctx: &EventCtx,
train: &Train,
motion: &Motion,
signal: &Signal,
check: SignalCheck,
result: &mut SignalChangePathResult
): SignalChangePath { ... }
event_signal_change_path script functions must be defined as a method of a struct which extends the game object Signal. They must have the exact same parameters and return type as in the previous example.
event_signal_change_path are called when the player enables their respective struct as an extension for a given signal in the track editor. From that moment the game simulation system which advances the train motion simulation (the Extrapolator) will call your event_signal_change_path function whenever said signal is allowed to change the path of the train, and it is a path signal.
Under some circumstances path signals are given a chance to change the train path, by allowing them to introduce an intermediate path goal that it is not the currently scheduled stop goal. An example of this mechanic is the stop selecting signal behavior. Starting in 1.18 this capability has been extended to every path signal, but it can only be triggered by scripts implementing event_signal_change_path.
The current path check result of this signal for the passed train is passed as check. This already includes any results from event_signal_check.
Your code must return a SignalChangePath enum value of SignalChangePath::Change or SignalChangePath::Keep. Keep keeps the current train path as-is. Change will try to change the current train path to a new path that ends at the Pos specified in result.pos (check the API reference in the bottom of this document for the full definition of SignalChangePathResult). After the train reaches that position it will automatically find a path to resume its trip to its currently scheduled stop. The train path can be altered an unlimited number of times before reaching its scheduled stop, including cutting short any previously issued path by a event_signal_change_path script.
Signal event_signal_pass_by
pub struct Example extends Signal { ... }
pub fn Example::event_signal_pass_by(
self: &Example,
ctx: &EventCtx,
train: &Train,
motion: &Motion,
signal: &Signal,
sc: &mut SimController
) { ... }
event_signal_pass_by script functions must be defined as a method of a struct which extends the game object Signal. They must have the exact same parameters and return type as in the previous example.
event_signal_pass_by are called when the player enables their respective struct as an extension for a given signal in the track editor. From that moment the game simulation system which advances the train motion simulation (the Extrapolator) will call your event_signal_pass_by function on the exact simulation frame a train passes by the exact point of the signal on the track.
This function has no return result. Instead, since it has a predictably low call rate compared to the other events, it is the only event function with access to the powerful SimController API.
Signal event_signal_marker_reserved
pub struct Example extends Signal { ... }
pub fn Example::event_signal_marker_reserved(
self: &Example,
db: &DB,
signal: &Signal
): ID<Train> { ... }
event_signal_marker_reserved script functions must be defined as a method of a struct which extends the game object Signal. They must have the exact same parameters and return type as in the previous example.
event_signal_marker_reserved are called when the player enables their respective struct as an extension for a given marker signal in the track editor. From that moment the game simulation system which advances the train motion simulation (the Extrapolator) will call your event_signal_marker_reserved any time the reservation check algorithm is tracing a path which goes over the marker signal, on any direction, giving it the chance to report an arbitrary train as the reserver of that point on the track.
This signal has a potential huge call rate, and for this reason it is given very little access to the APIs. The intention is that all the state required by this signal is calculated somewhere else and attached to the signal as a private struct object, or to some object reachable from it. Don’t worry about filtering for the “same train” cases, this is automatically done.
If no reservation is desired, use the empty global method available in all ID<Class> types: return ID<Train>::empty();
pub struct Example extends Signal {}
struct Owner {
train_id: ID<Train>,
}
pub fn Example::event_signal_marker_reserved(
self: &Example,
db: &DB,
signal: &Signal
): ID<Train> {
// some other, more full featured script function has attached Owner to this signal,
// or not, so we need to check first
if let owner &= db.view<Owner>(signal) {
if is_valid(db.view(owner.train_id)) {
return owner.train_id;
}
}
return ID<Train>::empty();
}
Signal event_signal_texture_state
pub struct Example extends Signal { ... }
pub fn Example::event_signal_texture_state(
self: &Example,
db: &DB,
signal: &Signal
): i64 { ... }
event_signal_texture_state script functions must be defined as a method of a struct which extends the game object Signal. They must have the exact same parameters and return type as in the previous example.
event_signal_texture_state are called when the player enables their respective struct as an extension for a given signal in the track editor. From that moment, when the game UI needs to query what is the current signal texture state index, it will call your event_signal_texture_state, with some potential caching to keep the call rate lower. In any case the call rate of this function is potentially very large when the user zooms out the map, so make sure to keep its code as efficient as possible. For this same reason it is given very little access to the APIs.
The return value indicates which signal texture state is to be used. If it’s -1 or lower, the default algorithm will be used to select the texture index. If it’s 0 or larger, it will select the texture in the same order as the state lines in the SignalTextures. Larger values than the number of states will be clamped to the last state.
Controllers
Controller functions are called at a low frame rate compared to the overall simulation, with an (unchangeable) rate of once in 6 sim-seconds. In exchange they can invoke powerful commands as their execution result, and do not need any specific event or trigger to run, other than their enforced timer
Train control_train
pub struct HateDepots extend Train {}
pub fn HateDepots::control_train(
self: &HateDepots,
ctx: &ControlCtx,
train: &Train,
motion: &Motion,
sc: &mut SimController
) {
// train is already removed from the tracks, don't interfere with potential attempts at shift assignation
if is_null(motion.presence.get()) {
return;
}
// train is running a schedule, don't interfere
if is_valid(motion.schedule_dispatch.get()) {
return;
}
// train is present, but it is not running a schedule, so despawn it
sc.queue_train_intervention(train);
}
The train motion engine will call train controllers after the entire simulation for a given frame is done, and all threads are finished. Command execution will only happen after all train controllers for a given frame are done. In other words, train controllers are given a consistent, read-only snapshot of the train sim, and queue up work to be executed by the sim. They never directly change the sim in any way, allowing train controller execution to be fully parallel.
After all controllers are run, and all their non-task commands are executed, tasks are executed. Read more about tasks later on. Any tasks with a deadline which expired in this frame a run at this time too.
SimController command queue
The SimController object implements an API to control the simulation, including modification of some parameters and behaviors. It is also responsible for attaching and erasing private struct data from game objects, on behalf of scripts. The execution of SimController is based on a command queue. Every call queues a command and returns immediately to the script code, without making any changes to the game state. Therefore the game state never changes while the script code is running.
For control scripts, commands are executed asynchronously, there is no guaranteed order of execution! You must prepare your code and data for this design. For tasks scripts commands are executed in order and in isolation. You can queue up tasks from control scripts when you need to guarantee data consistency beyond “pick a random winner”.
SimController::queue_train_intervention
fn SimController::queue_train_intervention(
self: &mut SimController,
train: &Train
)
Removes the train from the tracks, releases its schedule shift (if any), and deletes and refunds its pax (if any). No other cost other than pax refunds is issued. This function can be used as a general “despawn the train” feature, not just for exceptional circumstances.
SimController::queue_train_warp
fn SimController::queue_train_warp(
self: &mut SimController,
train: &Train,
pos: &Pos
)
Removes the train from the tracks and repositions it at pos, re-pathing it to its currently scheduled destination (if any), and setting its speed to 0. This is basically a train wormhole. The train keeps its shift and pax, if any.
SimController::queue_train_drive_to
fn SimController::queue_train_drive_to(
self: &mut SimController,
train: &Train,
pos: &Pos
)
Re-paths the train path to end at pos, without any changes to its shift or pax (if any). It is possible for any future queue_train_drive_to or event_signal_change_path to reset this path at any point, losing the destination. “Queue” here is used as part of the SimController API semantics, it does not mean the train keeps a queue of positions to reach. If the path is not interrupted and the train reaches pos it will re-path itself towards its current scheduled stop, if it has any, or brake to a stop if unassigned. If pos was also in the stop area set for its current scheduled stop it will also brake to stop.
SimController::queue_attach
fn SimController::queue_attach(
self: &mut SimController,
model_obj: <Generic>,
struct_obj: <Generic>
)
Queues the attachment of the struct_obj (allocated with Example::new(), or copied from an existing object with Example::clone(existing)) to the model_obj, which must be of one of the following types: Train, Signal, Line, Schedule or Station.
Attached objects can be accessed with DB::view<Example>(model_obj) at some point in the future. If called from a controller function, the attached object will be accessible after all controller functions are done for a given frame. If called from a task, he attached object will be accessible immediately after the task returns, and before any other task is executed. In other words, tasks are transaction-like, in the sense they view an immutable snapshot of the game state, and any changes they queue are executed before any other task, and then visible from these other tasks.
Attaching an object of a given private struct type when it already exists attached to a model_obj will overwrite it. This is how private data is updated from scripting: by using clone to copy any existing data, modifying the copy, then calling queue_attach to replace the existing data. Therefore it is a good idea to keep your private structs small. Only one instance of a given private struct type can be attached to any particular model object, but you can attach as many different private struct types as you want.
SimController::queue_erase
fn SimController::queue_erase(
self: &mut SimController,
struct_obj: <Generic>
)
Queues the erasure of struct_obj, which must obtained using with DB::view<Example>(model_obj). Trying to erase temporary objects (allocated with Example::new(), or copied from an existing object with Example::clone(existing)) is ignored. The execution of the queued erase commands is analog to the queue_attach case. In particular a sequences of attach and erase queued from controllers will produce undefined results (impossible to predict which attach and/or erase wins). For example multiple attach from a controller for the same model_obj and private struct type will implicitly pick a singular “winner” attach, but the script does not have any control over which one. Sequences of attach and erase queued from tasks are strictly ordered and isolated, so they produce consistent results, in the same order they were queued in the task script, without any concurrent changes.
SimController::queue_task and SimController::queue_task_at
These functions are documented in the next section.
Tasks
Tasks are a very special kind of command which allows scripts to dynamically queue “future work”. Scripts with access to SimController can call this function to queue up a task for future script execution:
fn SimController::queue_task(
self: &mut SimController,
struct_obj: <Generic>
)
struct_obj must be of a private struct type which implements task_run as this:
struct Example { ... }
pub fn Example::task_run(
self: &Example,
ctx: &ControlCtx,
sc: &mut SimController
) { ... }
For example, a train controller script could decide it needs to delay some decision into a task for data consistency reasons (other controllers are running at the same time), and delegate into a task rather than directly calling queue_attach:
pub struct ControlTrain extends Train { ... }
pub fn ControlTrain::control_train(
self: &ControlTrain,
ctx: &ControlCtx,
train: &Train,
motion: &Motion,
sc: &mut SimController
) {
...
let task = Task::new();
task.train_id = train.id;
task.v = some_data_from_somewhere;
task.prio = 42; // or something smarter, like from ControlTrain, other objects, etc.
sc.queue_task(task);
...
}
struct Task {
train_id: ID<Train>,
v: i64,
prio: i64,
}
struct SomeData {
v: i64,
prio: i64,
}
pub fn Task::task_run(
self: &Task,
ctx: &ControlCtx,
sc: &mut SimController
) {
let train &= ctx.db.view(self.train_id) else { return; }
if let existing &= ctx.db.view<SomeData>(train) {
if existing.prio >= self.prio {
return;
}
}
let data = SomeData::new();
data.v = self.v;
data.prio = self.prio;
sc.queue_attach(train, data);
}
In the previous example, maybe task_run will be called multiple times because multiple ControlTrain::control_train() instances queued objects of Task, but each execution will be single threaded, and all its queued queue_attach commands will be executed just after the call is done and before any other task_run call (for Task or any other struct type), therefore the algorithm to pick a winner based on an explicit priority works.
As mentioned, task execution is single threaded. The game does not feature a real concurrent database, instead all concurrent access is bespoke and very carefully designed to avoid deadlocks and inconsistencies (this is much faster than a general purpose concurrent database). Scripts also run in this parallel system, but their access to the game state is carefully restricted as explained earlier. But in the end, scripts also need to modify data, and do so in a consistent way. Tasks solve this issue, but do so in the most pessimistic possible way, by being single threaded. Therefore script developers should use them in moderation. Consider the “random winner” semantics of control script queuing first, which are completely free performance wise, before deciding you need a task. It is often the case a concurrent problem reduces to “pick 0 or 1 winners, any winner, but never 2 or more” and the default queue semantics already provide that guarantee for attaching a specific model/private pair.
task_run is provided with SimController, therefore it can queue tasks itself. But unlike controller queuing tasks, which are guaranteed to run on the same frame, tasks queued from tasks are guaranteed to not run on the same frame, on purpose. They are deliberately stored away and forced a delay of 6 sim-seconds. This is to avoid misuse of tasks, and infinite looping.
This behavior might even be desirable in certain scripts, for example if you are developing some stateful signal system and you need some kind of “cleanup” task that makes sure some signal being owned by some train is still the focus of said train. You can deliberately queue tasks for future simulation frames with queue_task_at:
fn SimController::queue_task_at(
self: &mut SimController,
struct_obj: <Generic>,
deadline: i64
)
deadline is expressed as microseconds since the start of the current save world. You can obtain the current simulation frame clock_us value with Extrapolator::clock_us(), like in this example:
// schedule task to run 10 sim-s from now
sc.queue_task_at(task, ctx.extrapolator.clock_us() + 10000000);
Standard library
Top level
fn abs(a: f32): f32
fn abs(a: f64): f64
fn abs(a: i16): i16
fn abs(a: i32): i32
fn abs(a: i64): i64
fn abs(a: i8): i8
fn ceil(a: f32): f32
fn ceil(a: f64): f64
fn exp(a: f32): f32
fn exp(a: f64): f64
fn floor(a: f32): f32
fn floor(a: f64): f64
fn iround(a: f32): i64
fn iround(a: f64): i64
fn is_inf(a: f32): bool
fn is_inf(a: f64): bool
fn is_nan(a: f32): bool
fn is_nan(a: f64): bool
fn is_normal(a: f32): bool
fn is_normal(a: f64): bool
fn log10(a: f32): f32
fn log10(a: f64): f64
fn loge(a: f32): f32
fn loge(a: f64): f64
fn max(a: f32, b: f32): f32
fn max(a: f64, b: f64): f64
fn max(a: i16, b: i16): i16
fn max(a: i32, b: i32): i32
fn max(a: i64, b: i64): i64
fn max(a: i8, b: i8): i8
fn max(a: u16, b: u16): u16
fn max(a: u32, b: u32): u32
fn max(a: u64, b: u64): u64
fn max(a: u8, b: u8): u8
fn min(a: f32, b: f32): f32
fn min(a: f64, b: f64): f64
fn min(a: i16, b: i16): i16
fn min(a: i32, b: i32): i32
fn min(a: i64, b: i64): i64
fn min(a: i8, b: i8): i8
fn min(a: u16, b: u16): u16
fn min(a: u32, b: u32): u32
fn min(a: u64, b: u64): u64
fn min(a: u8, b: u8): u8
fn pow(a: f32, b: f32): f32
fn pow(a: f64, b: f64): f64
fn round(a: f32): f32
fn round(a: f64): f64
fn sqrt(a: f32): f32
fn sqrt(a: f64): f64
fn zdiv(a: i16, b: i16): i16
fn zdiv(a: i32, b: i32): i32
fn zdiv(a: i64, b: i64): i64
fn zdiv(a: i8, b: i8): i8
fn zdiv(a: u16, b: u16): u16
fn zdiv(a: u32, b: u32): u32
fn zdiv(a: u64, b: u64): u64
fn zdiv(a: u8, b: u8): u8
fn zmod(a: i16, b: i16): i16
fn zmod(a: i32, b: i32): i32
fn zmod(a: i64, b: i64): i64
fn zmod(a: i8, b: i8): i8
fn zmod(a: u16, b: u16): u16
fn zmod(a: u32, b: u32): u32
fn zmod(a: u64, b: u64): u64
fn zmod(a: u8, b: u8): u8
Database model
Type Building
struct Building {
id: ID<Building>,
}
Type ControlCtx
struct ControlCtx {
db: &DB,
sim: &Sim,
extrapolator: &Extrapolator,
}
Accesible game state for sim controller scripts.
Type DB
struct DB {}
The database of all player-created objects, and associated script stuct objects.
fn DB::view(self: &DB, id: ID<Building>): *Building
fn DB::view(self: &DB, id: ID<Line>): *Line
fn DB::view(self: &DB, id: ID<Schedule>): *Schedule
fn DB::view(self: &DB, id: ID<Signal>): *Signal
fn DB::view(self: &DB, id: ID<Station>): *Station
fn DB::view(self: &DB, id: ID<Tag>): *Tag
fn DB::view(self: &DB, id: ID<Track>): *Track
fn DB::view(self: &DB, id: ID<Train>): *Train
fn DB::view(self: &DB): &Script
Type EventCtx
struct EventCtx {
db: &DB,
extrapolator: &Extrapolator,
}
Accesible game state for sim event handler scripts.
Type Line
struct Line {
id: ID<Line>,
tags: Tags,
}
Type Motion
struct Motion {
train_id: ID<Train>,
presence: std::optional<Motion::Presence>,
drive: std::optional<Motion::Drive>,
timed_stop: std::optional<Motion::TimedStop>,
schedule_stop: std::optional<Motion::ScheduleStop>,
schedule_station_event: std::optional<Motion::ScheduleStationEvent>,
schedule_dispatch: std::optional<Motion::ScheduleDispatch>,
}
Simulation state of a train.
Type Motion::Drive
struct Motion::Drive {
waiting_signal_id: ID<Signal>,
waiting_signal_last_check: i64,
path: Path,
goal_trace: TapeTrace,
}
Simulation state of a train being driven along a path.
Type Motion::Presence
struct Motion::Presence {
pos: Pos,
speed: f64,
}
Simulation state of a train being present at a certain position on a track.
Type Motion::ScheduleDispatch
struct Motion::ScheduleDispatch {
sched_id: ID<Schedule>,
run: Run,
run_epoch_start: i64,
}
Simulation state of a train currently assigned to a schedule shift.
Type Motion::ScheduleStationEvent
struct Motion::ScheduleStationEvent {}
Simulation state of a train performing scheduled stop related to pax and auxiliary station data.
Type Motion::ScheduleStop
struct Motion::ScheduleStop {
sched_id: ID<Schedule>,
line_id: ID<Line>,
station_id: ID<Station>,
}
Simulation state of a train performing a stop due to its schedule.
Type Motion::TimedStop
struct Motion::TimedStop {
departure_epoch_s: i64,
}
Simulation state of a train performing a timed stop, with a departure time.
Type Path
struct Path {
found: bool,
goal_has_stop_time: bool,
force_goal_reservation: bool,
pose_start: Pose,
pose_goal: Pose,
found_start: Pos,
found_goal: Pos,
}
A complex path over tracks, represented a sequence of track segments, subsegments, branchs, merges and reversals, from a start Pos to a goal Pos.
Type Pos
struct Pos {
track_id: ID<Track>,
}
A position on a track, with a direction.
fn Pos::flip(self: &Pos): mut Pos
Type Pose
struct Pose {
head: Pos,
tail: Pos,
}
Two positions on a track, with opposite orientations. Usually used as part of station stop data.
Type Run
struct Run {
line_id: ID<Line>,
}
A projection into real time of a sequence of stops belonging to a line, with possible modified timing.
Type Schedule
struct Schedule {
id: ID<Schedule>,
tags: Tags,
}
Type Script
struct Script {
id: ID<Script>,
}
Type Signal
struct Signal {
id: ID<Signal>,
match_block_facing: bool,
check_beyond_stops: bool,
}
fn Signal::backward(self: &Signal): mut Pos
Returns the position of a train facing backwards from the signal.
fn Signal::forward(self: &Signal): mut Pos
Returns the position of a train facing forward from the signal.
fn Signal::kind(self: &Signal): mut SignalKind
Returns the signal kind.
Type SignalKind
enum SignalKind {
Path,
Balise,
Marker,
OneWay,
NoWay,
PlatformStop,
}
Type Station
struct Station {
id: ID<Station>,
tags: Tags,
}
Type Tag
struct Tag {
id: ID<Tag>,
parent_id: ID<Tag>,
}
Type Tags
struct Tags {}
A set of tags.
fn Tags::contains(self: &Tags, id: ID<Tag>): mut bool
Returns true if the given tag ID is contained in the set.
fn Tags::contains(self: &Tags, tag: &Tag): mut bool
Returns true if the given tag is contained in the set.
Type TapeTrace
struct TapeTrace {}
A simplified path (with no branching) over tracks, which represents a footprint of track segments and subsegments.
fn TapeTrace::contains(self: &TapeTrace, other: &Pos): mut bool
Returns true if the given Pos is contained inside this TapeTrace.
fn TapeTrace::contains(self: &TapeTrace, other: &TapeTrace): mut bool
Returns true if the given TapeTrace is fully contained inside this TapeTrace.
fn TapeTrace::hits(self: &TapeTrace, other: &TapeTrace): mut bool
Returns true if the given TapeTrace hits this TapeTrace.
Type Track
struct Track {
id: ID<Track>,
}
Type Train
struct Train {
id: ID<Train>,
tags: Tags,
}
Type std::optional<Motion::Drive>
struct std::optional<Motion::Drive> {}
fn std::optional<Motion::Drive>::get(self: &std::optional<Motion::Drive>): *Motion::Drive
Type std::optional<Motion::Presence>
struct std::optional<Motion::Presence> {}
fn std::optional<Motion::Presence>::get(self: &std::optional<Motion::Presence>): *Motion::Presence
Type std::optional<Motion::ScheduleDispatch>
struct std::optional<Motion::ScheduleDispatch> {}
fn std::optional<Motion::ScheduleDispatch>::get(
self: &std::optional<Motion::ScheduleDispatch>
): *Motion::ScheduleDispatch
Type std::optional<Motion::ScheduleStationEvent>
struct std::optional<Motion::ScheduleStationEvent> {}
fn std::optional<Motion::ScheduleStationEvent>::get(
self: &std::optional<Motion::ScheduleStationEvent>
): *Motion::ScheduleStationEvent
Type std::optional<Motion::ScheduleStop>
struct std::optional<Motion::ScheduleStop> {}
fn std::optional<Motion::ScheduleStop>::get(
self: &std::optional<Motion::ScheduleStop>
): *Motion::ScheduleStop
Type std::optional<Motion::TimedStop>
struct std::optional<Motion::TimedStop> {}
fn std::optional<Motion::TimedStop>::get(self: &std::optional<Motion::TimedStop>): *Motion::TimedStop
Type ID<Building>
struct ID<Building> {}
fn ID<Building>::empty(): mut ID<Building>
fn ID<Building>::equals(self: &ID<Building>, other: &ID<Building>): mut bool
Type ID<Line>
struct ID<Line> {}
fn ID<Line>::empty(): mut ID<Line>
fn ID<Line>::equals(self: &ID<Line>, other: &ID<Line>): mut bool
Type ID<Schedule>
struct ID<Schedule> {}
fn ID<Schedule>::empty(): mut ID<Schedule>
fn ID<Schedule>::equals(self: &ID<Schedule>, other: &ID<Schedule>): mut bool
- Type ID
<Script>
struct ID<Script> {}
fn ID<Script>::empty(): mut ID<Script>
fn ID<Script>::equals(self: &ID<Script>, other: &ID<Script>): mut bool
Type ID<Signal>
struct ID<Signal> {}
fn ID<Signal>::empty(): mut ID<Signal>
fn ID<Signal>::equals(self: &ID<Signal>, other: &ID<Signal>): mut bool
Type ID<Station>
struct ID<Station> {}
fn ID<Station>::empty(): mut ID<Station>
fn ID<Station>::equals(self: &ID<Station>, other: &ID<Station>): mut bool
Type ID<Tag>
struct ID<Tag> {}
fn ID<Tag>::empty(): mut ID<Tag>
fn ID<Tag>::equals(self: &ID<Tag>, other: &ID<Tag>): mut bool
Type ID<Track>
struct ID<Track> {}
fn ID<Track>::empty(): mut ID<Track>
fn ID<Track>::equals(self: &ID<Track>, other: &ID<Track>): mut bool
Type ID<Train>
struct ID<Train> {}
fn ID<Train>::empty(): mut ID<Train>
fn ID<Train>::equals(self: &ID<Train>, other: &ID<Train>): mut bool
Simulation
Type Extrapolator
struct Extrapolator {}
The simulation system tasked with train motion and scheduling.
fn Extrapolator::clock_us(self: &Extrapolator): mut i64
Current microseconds since the creation of this saved game. This is used as the timing root of the simulation.
fn Extrapolator::epoch_s(self: &Extrapolator): mut i64
Current second since 1970-01-01:00:00:00.
fn Extrapolator::reservation_probe(self: &Extrapolator, pos: &Pos): mut ScResvProbeIt
Probe a track position for train reservations and occupations.
fn Extrapolator::week_monday0000_epoch_s(self: &Extrapolator): mut i64
Seconds since 1970-01-01:00:00:00 for Monday 00:00:00 of the current week.
fn Extrapolator::week_s(self: &Extrapolator): mut i64
Current second of the week.
Type ScResvProbeIt
struct ScResvProbeIt {}
Iterator to query the reservations at a certain track position.
fn ScResvProbeIt::next(self: &mut ScResvProbeIt): *ScResvProbeIt::Result
Advance the iterator.
Type ScResvProbeIt::Result
struct ScResvProbeIt::Result {
is_occ: bool,
train: &Train,
}
Type SignalChangePath
enum SignalChangePath {
Keep,
Change,
}
Type SignalChangePathResult
struct SignalChangePathResult {
pos: Pos,
goal_has_stop_time: bool,
force_goal_reservation: bool,
}
If a signal_change_path function wants to change the train path, it must set the pos field of this struct with the goal position of the new train path.
Type SignalCheck
enum SignalCheck {
Pass,
Stop,
}
Type SignalLookaheadResult
struct SignalLookaheadResult {
max_speed: f64,
}
Stores extra information associated with the result of the signal lookahead.
Type Sim
struct Sim {}
The database of the simulation state.
fn Sim::view<Motion>(self: &Sim, train_id: ID<Train>): *Motion
Return the Motion object associated to the train ID.
Type SimController
struct SimController {}
A proxy mutable API to the simulation engine.
fn SimController::queue_attach(
self: &mut SimController,
model_obj: <Generic>,
struct_obj: <Generic>
): void
fn SimController::queue_erase(
self: &mut SimController,
struct_obj: <Generic>
): void
fn SimController::queue_task(
self: &mut SimController,
struct_obj: <Generic>
): void
fn SimController::queue_task_at(
self: &mut SimController,
struct_obj: <Generic>,
deadline: i64
): void
fn SimController::queue_train_drive_to(
self: &mut SimController,
train: &Train,
pos: &Pos
): void
fn SimController::queue_train_intervention(self: &mut SimController, train: &Train): void
fn SimController::queue_train_warp(
self: &mut SimController,
train: &Train,
pos: &Pos
): void