SplitScript reference / SplitScript for C# authors
SplitScript for C# authors
SplitScript keeps C#'s static predictability, familiar braces, fixed-width
numbers, and as casts, but it is a small autosplitter language rather than a
general .NET environment. Its lifecycle, process access, error values, and
settings are language features, so translating only the spelling is not
enough.
Keep these common spelling changes in mind:
- C#
intbecomes SplitScripti32. - C#
stringbecomes SplitScriptString. - C#
TimeSpanbecomes SplitScriptDuration.
Declarations and types
Use let for every local and global variable and fn for functions. Bindings
are mutable unless the surrounding value is read-only. Types are normally
inferred in both directions; annotate process-memory boundaries when the
target layout does not already determine the type.
C# names map to explicit widths: byte becomes u8, int becomes i32,
long becomes i64, and their unsigned counterparts retain the same width.
Use f32 or f64, String, and Duration instead of float, double,
string, and TimeSpan.
state "game.exe" {
health: u32 at 0x1000
}
fn healthText(health) {
return `Health: {health}`
}
whileAttached {
print(healthText(current.health))
}
Backtick strings interpolate with {expression} without C#'s leading $.
Use arrays as [T], fixed memory arrays as [T; N], records for named product
types, and enums with exhaustive match.
There is no class override or interface declaration for textual display.
Records and enums receive a readable multiline Display representation by
default. Define fn Type.toString() -> String only to override it; the compiler
can infer the result. Use lower-camel toString, not C#'s ToString.
record Position {
x: i32,
y: i32,
}
fn Position.toString() { return `({self.x}, {self.y})` }
print(Position { x: 3, y: 5 })
Value-producing blocks
A block used in an expression can declare local values and then yield its final
expression. This avoids a temporary helper or immediately invoked delegate when
an if branch, match arm, fallback, or argument needs multiple steps:
fn levelLabel(isBoss: bool) -> String {
let label = if isBoss {
let kind = "Boss"
`{kind} level`
} else {
"Level"
}
return label
}
The final expression yields from the nested block; it does not return from the
function. Function, method, and lifecycle bodies continue to use explicit
return. A block without a final expression yields None. A trailing
semicolon on the final value is accepted for familiarity, but the compiler
warns because it does not discard that value, and the formatter removes it.
Unconditional and value-producing loops
C# while (true) maps to loop when repetition is intentionally
unconditional. Unlike C#, a loop can itself produce a value through
break value; all break values infer one result type. A loop without a break
has type Never.
fn choose(flag: bool) -> i32 {
return loop {
if flag { break 7 }
break -1
}
}
Keep while for a real condition. Only loop accepts a value-carrying
break; bare break and continue work in all runtime loops. Function
results still require the explicit return shown above.
Optional and fallible values
T? is an optional value and T! is a value or an error. None is both the
unit value and the absent option case. Plain T values promote into either
wrapper, so construction does not need Some or Ok; those names exist for
pattern matching. Recover a result with else, propagate it from a T!
function with postfix ?, or inspect it with match.
fn parseLives(text: String) -> u32! {
return text.parse()
}
fn livesOrZero(text: String) -> u32 {
return parseLives(text) else 0u32
}
print(livesOrZero("3"))
These are typed values, not CLR exceptions. throw constructs and transfers
the same error value; there is no general try/catch construct.
Retrying synchronous fallible work
Use retry when attachment should re-run a synchronous read transaction on
later ticks until every required value is available. A value block is an
ordinary expression, so several reads can share one local failure boundary:
onAttach {
let health = retry {
let player = process.read<address>(0x1000)?
process.read<i32>(player)?
}
print(health)
}
This is not a C# exception retry loop. A T! error, ?, or throw ends
the current attempt, and the complete operand begins again on the next attached
update. return still returns from the enclosing function; break and
continue keep their lexical loop targets. The attempt must be synchronous
and bounded, so await and nested retry are rejected inside it. Await
asynchronous discovery before entering the retry block.
Autosplitter lifecycle
Declare one state block. It owns attachment and polls its fields into the
transactional old and current snapshots. Use onAttach for discovery that
may suspend, onStateReady for work after the first complete snapshot,
whileAttached for per-tick bookkeeping, and onDetach for process-lifetime
cleanup. start, split, and reset return booleans; isLoading and
gameTime may return None when there is no new observation.
state "game.exe" {
completed: bool at 0x2000
}
split {
return !old.completed && current.completed
}
There is no explicit async modifier on lifecycle blocks. Awaiting is allowed
where the lifecycle permits suspension, and process closure cancels pending
attachment-owned work.
Process reads and settings
Memory access is fallible. State pointer paths propagate read failure at the
field boundary; direct process.read<T>(address) calls return T!. Prefer a
state field for values polled every tick and direct reads for attachment-time
discovery.
Settings are declared statically and become typed members. Documentation
comments become tooltips, and the settings and oldSettings views reflect
the current and previous tick.
settings {
/// Enables the completion split.
"Split on completion" => completionSplit: true
}
state "game.exe" {
completed: bool at 0x2000
}
split {
return settings.completionSplit && !old.completed && current.completed
}
Next step
Open Getting started from the documentation index and build its first autosplitter workflow. When a particular C# API does not translate directly, use Search Documentation with the original spelling; migration results link to the canonical SplitScript symbol or focused porting recipe.