Skip to content

Host API

ffredyk edited this page Jul 23, 2026 · 1 revision

Host API & Embedding

How to embed SQ# scripting into your .NET application or game.

NuGet Packages

Package Purpose Dependencies
SQSharp.Core SqValue, SqType, SqArray, bytecode types None
SQSharp.VM Stack VM + runtime Core
SQSharp.Compiler Lexer + parser + bytecode compiler Core, Language
SQSharp.Scheduler Fiber scheduler Core, VM
SQSharp.Host High-level host API + 100+ StdLib commands All above
SQSharp.CLI Command-line tool (dotnet sqf) All above

Most apps only need SQSharp.Host:

dotnet add package SQSharp.Hosting

Quick Start

using SQSharp.Host;

// Create the scripting host
var host = new SqHost();

// Capture script output
host.OnPrint += msg => Console.WriteLine(msg);

// Run a script
host.ExecuteString(@"
    _arr = [1, 2, 3, 4, 5];
    _arr pushBack 6;
    print format ['Array has %1 elements', count _arr];
");

// Pump the scheduler (in your game loop)
host.TickMain();

Host Lifecycle

var host = new SqHost();

// 1. Register custom commands (before spawning scripts)
host.RegisterNular("getPlayerCount", () => new SqValue((double)_players.Count));
host.RegisterUnary("getPlayerName", id => new SqValue(_players[(int)id.AsNumber()].Name));
host.RegisterBinary("damagePlayer", (id, amount) => {
    _players[(int)id.AsNumber()].Health -= amount.AsNumber();
    return SqValue.Nil;
}, precedence: 4, threadSafety: ThreadSafety.Exclusive);

// 2. Execute scripts
host.ExecuteString(@"
    _count = getPlayerCount;
    for '_i' from 0 to (_count - 1) do {
        _name = getPlayerName _i;
        print format ['Player %1: %2', _i, _name];
    };
");

// 3. Game loop — tick every frame
while (_running)
{
    host.TickMain();           // Run scheduled scripts (3ms budget per tick)
    UpdateGameLogic();         // Your game logic
    Render();                  // Your rendering
}

Command Registration

By Arity

// Nular — no arguments, returns value
host.RegisterNular("getFPS", () => new SqValue(1.0 / deltaTime));

// Unary — one argument on the RIGHT
host.RegisterUnary("spawnEnemy", type => {
    SpawnEnemy(type.AsString());
    return SqValue.Nil;
});

// Binary — left and right operands
host.RegisterBinary("damageEntity", (entity, amount) => {
    var id = (int)entity.AsNumber();
    _entities[id].Damage(amount.AsNumber());
    return SqValue.Nil;
}, precedence: 4);

Thread Safety Declaration

// ReadOnly — safe from any scheduler (pure computation)
host.RegisterUnary("getPosition", entity => {
    return PackVector3(_entities[(int)entity.AsNumber()].Position);
}, ThreadSafety.ReadOnly);

// Exclusive — not thread-safe, host must serialize access
host.RegisterBinary("modifyWorld", (target, action) => {
    _world.Modify(target.AsNumber(), action.AsString());
    return SqValue.Nil;
}, threadSafety: ThreadSafety.Exclusive);

Overriding Built-in Commands

// Override print to use your logging system
host.RegisterUnary("print", msg => {
    Logger.Log(msg.AsString());
    return SqValue.Nil;
});

Command Registration Options

Method What When
RegisterCoreCommands() Math, string, array, logic, type checks Called automatically by new SqHost()
DeclareArmaCompatCommands() hint, systemChat, diag_log Opt-in. Skip for non-Arma hosts
DeclareSchedulerCommands() currentScheduler, allSchedulers, etc. Called automatically
DeclareMultiplayerCommands() remoteExec, publicVariable, etc. Opt-in. Host must call explicitly
// Minimal host (core only, no Arma compat):
var host = new SqHost(includeArmaCompat: false);
host.RegisterUnary("log", msg => { Logger.Log(msg.AsString()); return SqValue.Nil; });

// Full Arma compat (default):
var host = new SqHost();  // or new SqHost(includeArmaCompat: true)

Multiple Schedulers

var host = new SqHost();

// Create additional schedulers
host.CreateScheduler("AI", budgetMs: 3.0);
host.CreateScheduler("Physics", budgetMs: 5.0);

// Register multiplayer commands (makes scheduler=machine)
host.DeclareMultiplayerCommands();

// Scripts can now use spawnOn:
host.ExecuteString(@"
    spawnOn ['AI', { heavyPathfinding(); }];
    spawnOn ['Physics', { simulate(_this); }];
");

// Tick all schedulers in game loop:
while (_running)
{
    host.TickAll();            // Pumps Main + any auto-pumped schedulers
    Render();
}

Output Handling

// Capture all script output
host.OnPrint += msg => {
    // msg is prefixed: "[HINT] ...", "[CHAT] ...", "[DIAG] ...", or raw
    if (msg.StartsWith("[HINT] "))
        UIManager.ShowHint(msg.Substring(7));
    else if (msg.StartsWith("[CHAT] "))
        ChatPanel.AddMessage(msg.Substring(7));
    else if (msg.StartsWith("[DIAG] "))
        File.AppendAllText("debug.log", msg.Substring(7) + "\n");
    else
        Console.WriteLine(msg);
};

Executing Scripts

// From string
host.ExecuteString("print 'Hello';");

// From file
var handle = host.ExecuteFile("scripts/init.sqf");

// Await completion (C# side)
var result = await handle.ToTask();
Console.WriteLine($"Script returned: {result.AsString()}");

Custom Types

Register opaque host types:

// Register an Entity type
host.RegisterObjectType("Entity", entityId => new SqValue((double)entityId));

// Now scripts can receive and pass around Entity references
host.RegisterUnary("getEntityHealth", entity => {
    var id = (int)entity.AsNumber();
    return new SqValue(_entities[id].Health);
});

host.RegisterUnary("damageEntity", (entity, amount) => {
    var id = (int)entity.AsNumber();
    _entities[id].Damage(amount.AsNumber());
    return SqValue.Nil;
});

Error Handling

try
{
    host.ExecuteString("_undefinedVar;");  // throws
}
catch (SqRuntimeException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    Console.WriteLine($"  at {ex.File}:{ex.Line}:{ex.Col}");
}

Complete Example: Game Host

public class GameHost
{
    private SqHost _sq;
    private List<Entity> _entities = new();

    public void Initialize()
    {
        _sq = new SqHost();
        
        // Capture output
        _sq.OnPrint += msg => Debug.Log(msg);
        
        // Register game commands
        _sq.RegisterNular("entityCount", 
            () => new SqValue((double)_entities.Count));
        
        _sq.RegisterUnary("getHealth", entity => {
            var e = _entities[(int)entity.AsNumber()];
            return new SqValue(e.Health);
        }, ThreadSafety.ReadOnly);
        
        _sq.RegisterBinary("damage", (entity, amount) => {
            var e = _entities[(int)entity.AsNumber()];
            e.Health -= amount.AsNumber();
            return SqValue.Nil;
        }, precedence: 4, threadSafety: ThreadSafety.Exclusive);
        
        // Create AI scheduler
        _sq.CreateScheduler("AI", budgetMs: 4.0);
        _sq.DeclareMultiplayerCommands();
        
        // Load mission scripts
        _sq.ExecuteFile("scripts/init.sqf");
    }

    public void Update(float deltaTime)
    {
        // Tick all schedulers
        _sq.TickAll();
        
        // Game logic
        UpdateEntities(deltaTime);
    }
}

See Also

SQ# Wiki

Home

Engine Docs

Migration

Commands

Value Constructors

Arithmetic

Comparison

Logic

Array

String

Math

Random

Type & Introspection

HashMap

Code Execution

Concurrency

Scheduler

Thread Safety

Error

Output

Time

Multiplayer

Compiler

Clone this wiki locally