Proyecto Open-Source

WarScript - embeddable scripting language

WarScript

Un lenguaje de scripting ligero con una VM de bytecode, escrito en C#. Disenado para embeber en juegos.

Que es WarScript?

Scripting que Compila a Bytecode

WarScript es un lenguaje de scripting con un interprete y compilador de bytecode escrito en C#. El codigo fuente se parsea, se compila a bytecode y se ejecuta en una maquina virtual basada en pila. El AST se descarta despues de la compilacion, asi que solo el bytecode permanece en memoria.

El lenguaje tiene su propia sintaxis limpia con clases, herencia, corrutinas, manejo de excepciones y un sistema de importacion. Pero el verdadero poder esta en la capa de bindings nativos: tu codigo C# del juego y WarScript se comunican directamente, sin serializacion ni traduccion de por medio.

Born out del desarrollo de Warman y probado en un juego publicado, WarScript maneja todo, desde hooks de eventos y logica de gameplay hasta generar unidades y aplicar modificadores.

game-logic.ws
# Classes with properties and methods
class Entity [name, hp]
    fun take_damage [amount]
        this :: hp = this :: hp - amount
    end
    fun is_alive []
        return this :: hp > 0
    end
end

hero = new Entity ["Hero", 100]
hero :: take_damage [30]
assert hero :: hp == 70

# Exception handling
begin
    risky_operation []
rescue err
    print err :: message
ensure
    print "cleanup done"
end
patrol.ws
# Import shared libraries
import "lib/vectors.ws"

# Coroutine with yield
fun patrol_loop [unit, waypoints]
    loop wp in waypoints
        Unit_move_to [unit, wp]
        yield wait 2.0
    end
end

# Start a looping coroutine
start_coroutine_loop ["patrol_loop",
    {guard, {pos_a, pos_b, pos_c}}]

# Called each tick by the host
fun tick [dt]
    players = get_players []
    loop player in players
        if Unit_get_hp [player] < 20
            Unit_apply_modifier [player, "Regen"]
        end
    end
end

El Lenguaje

Sintaxis Limpia, Funciones Reales

WarScript usa bloques fun/end, argumentos entre corchetes, iteracion loop/in y comentarios #. La sintaxis es deliberadamente simple para que los modders la aprendan rapido.

Pero no es un lenguaje de juguete. WarScript tiene clases con herencia, operadores is y as para comprobacion y conversion de tipos, manejo de excepciones begin/rescue/ensure, un sistema import con deteccion de dependencias circulares y cache, corrutinas con yield y yield wait, mas un conjunto completo de operadores aritmeticos, de comparacion y logicos.

La capa de bindings nativos permite a los scripts llamar directamente a tu codigo C# del juego y viceversa. Funciones como Unit_apply_modifier y spawn_unit son metodos C# que los scripts invocan nativamente. Tu API del juego se convierte en la API de scripting.

Arquitectura

Del Codigo Fuente al Bytecode

Compilador de Bytecode

El codigo fuente se tokeniza, se parsea en un AST y luego se compila a bytecode. El AST se descarta despues de la compilacion para liberar memoria. El bytecode se almacena en cache y se reutiliza para cada llamada posterior.

VM Basada en Pila

Una maquina virtual construida a medida ejecuta el bytecode. Incluye superinstrucciones (pares de opcodes fusionados como comparar-y-saltar) para reducir el overhead de despacho en caminos calientes.

Recarga en Caliente

Intercambia codigo fuente en tiempo de ejecucion preservando todo el estado de variables globales. Las definiciones de funciones se actualizan inmediatamente. Las instancias de clases existentes siguen funcionando. Las corrutinas activas se detienen limpiamente.

Serializacion de Bytecode

Compila una vez, carga instantaneamente. Guarda el bytecode compilado en un flujo binario en tiempo de compilacion, luego cargalo en tiempo de ejecucion para saltar el lexer, parser y compilador completamente.

Sandboxing

Establece limites por llamada en instrucciones de bytecode y asignaciones de heap. Cuando se excede un presupuesto, la VM lanza una excepcion capturable y se detiene limpiamente. Esencial para scripts de mods no confiables.

Depurador con Mapa de Codigo

Establece puntos de interrupcion por numero de linea, recorre la ejecucion paso a paso e inspecciona variables locales. El hook de depuracion recibe el nombre de funcion, linea y todas las variables locales en cada punto de pausa. Cero overhead cuando esta desactivado.

Bindings Nativos

El Generador de Codigo Hace el Cableado

WarScript incluye un generador de codigo Roslyn. Marca una clase C# con [WsModule] y sus metodos con [WsFunction], y el generador produce todo el codigo de marshalling automaticamente. Los tipos de parametros (double, int, string, bool, WarValue) se convierten en la frontera. Los tipos de retorno se envuelven. Sin codigo de pegamento manual.

Para metodos de instancia, el generador captura this en la lambda. Para funciones variadicas, marca un parametro con [WsRawArgs] para recibir la lista de argumentos cruda. El codigo generado alimenta directamente al registro de librerias, que el exportador de documentacion tambien lee.

Puedes seguir registrando funciones nativas manualmente si lo prefieres. La API NativeFunctionDefinition toma un nombre, lista de argumentos y una lambda. Ambos enfoques coexisten.

MathModule.cs
// C# with source generator attributes
[WsModule("math",
    Description = "Math functions")]
public static partial class MathModule
{
    [WsFunction("pow")]
    public static double Pow(
        double @base, double exp)
        => Math.Pow(@base, exp);

    [WsFunction("lerp")]
    public static double Lerp(
        double a, double b, double t)
        => a + (b - a) * t;

    [WsFunction("clamp")]
    public static double Clamp(
        double n, double lo, double hi)
        => Math.Max(lo, Math.Min(hi, n));
}

Math

pow, sqrt, floor, ceil, round, abs, min, max, clamp, sign, lerp.

Array

length, contains, index_of, remove, remove_at, pop, insert, copy, clear, append.

Coroutine

start_coroutine, start_coroutine_loop, stop_coroutine, stop_all_coroutines. Yield con duraciones de espera.

Utility

is_null y ayudantes de comprobacion de tipo. El registro esta disenado para que anadir nuevas librerias sea sencillo.

Librerias Integradas

Baterias Incluidas

WarScript incluye cuatro librerias estandar registradas automaticamente a traves del WarScriptLibraryRegistry. Cada libreria expone sus funciones con descripciones completas y anotaciones de tipo de retorno, para que el exportador de documentacion pueda generar documentacion de referencia para ellas junto con tus bindings especificos del juego.

La libreria de corrutinas merece mencion especial. Las corrutinas en WarScript usan suspension y reanudacion a nivel de bytecode. Una corrutina puede hacer yield para pausar hasta el siguiente tick, yield wait 2.0 para pausar durante una duracion, o ejecutarse como corrutina en bucle que se reinicia tras cada finalizacion. El host llama a TickCoroutines(dt) cada frame para avanzarlas.

Anadir tu propia libreria sigue el mismo patron: crea una clase estatica con un metodo Register(script, scope) y anadela al registro. O usa el generador de codigo [WsModule] y se maneja por ti.

Integracion

Como Tu Juego Usa WarScript

Crea una instancia de WarScriptLanguage con tu codigo fuente, un resolvedor de archivos para importaciones y un logger opcional. Llama a Run() para parsear, compilar y ejecutar el codigo de nivel superior. Luego usa GetFunction() y Call() para invocar funciones de script desde tu bucle de juego.

La clase base ScriptRunner maneja la configuracion comun: registrar librerias estandar, ejecutar el script y proporcionar un metodo CallDynamic() para llamar funciones por nombre. Sobreescribela para anadir tus bindings nativos especificos del juego y logica de importacion.

Para produccion, compila en tiempo de build con SaveBytecode() y carga en tiempo de ejecucion con LoadBytecode(). Esto salta el parseo y la compilacion completamente. Para desarrollo, usa Reload() para intercambiar codigo fuente manteniendo todo el estado global vivo.

GameSetup.cs
// Create and run a script
var script = new WarScriptLanguage(
    "patrol", source, ImportFile, Log);
script.InstructionBudget = 1_000_000;
script.Run();

// Get a function handle (cached)
var tick = script.GetFunction("tick", 1);

// Game loop — call every frame
script.Call(tick, WarValue.FromNumeric(dt));
script.TickCoroutines(dt);

// Hot reload during development
script.Reload(newSource);
tick = script.GetFunction("tick", 1);

Instalacion

Anade WarScript a Tu Proyecto

Para proyectos de Unity, abre el Package Manager y anade WarScript desde una URL de git. El paquete apunta a Unity 6000.3+ y no tiene dependencias externas.

Unity Package Manager
https://github.com/nine9123/WarScript.git#upm

Para proyectos C# standalone, clona el repositorio o anadelo como submodulo. El runtime no tiene dependencias de Unity y puede usarse en cualquier aplicacion .NET.

Apoya el Proyecto

Ayuda a WarScript a Crecer

WarScript es gratuito y open-source. Si lo encuentras util y quieres apoyar el desarrollo, considera convertirte en patron. Pero lo mas importante: simplemente usalo. Prueba WarScript en tu juego, reporta problemas, sugiere funciones y ayuda a construir la comunidad.

Empezar

Explora la API, lee la documentacion y empieza a scriptear.