Ensamblador Virtual de Aetheria (EVA)

Para qué sirve.

El Aetheria Game Engine, programa que se encarga de poner a funcionar el juego de Aetheria, está escrito en Java, y lo que hace es cargar todos los objetos que compondrán el juego de ficheros que tienen un formato determinado. Estos objetos interactúan entre sí y con el jugador. La mayoría de ellos se pueden especificar dando una serie de datos en su correspondiente fichero: por ejemplo, de una piedra podemos conocer su aspecto, volumen, peso y algunos datos más, y toda esa información aparecerá en el fichero. ¿Pero qué pasa si en vez de una piedra queremos crear la Legendaria Espada de Arlis, por poner un ejemplo, una espada que es capaz de absorber la energía vital de los enemigos y convertirla en energía mágica que transmite a su portador? Todo eso podría estar programado en el engine, y luego se podría especificar en el fichero de ese objeto que "es la Espada de Arlis", y que el programa se encargara de que hiciese lo que tiene que hacer. Pero, ¿acaso no sería mucho más interesante que cada objeto pudiera, por sí solo, mantener propiedades como ésas o incluso mucho más complejas, sin necesidad de que el programa las soportara antes? Si se da a los objetos la suficiente autonomía como para definir sus propias reglas de juego, se puede conseguir una flexibilidad mucho mayor que contando sólo con parámetros y funciones preprogramadas. Se pueden crear objetos realmente únicos, y se puede utilizar el engine de Aetheria para soportar juegos totalmente distintos.

¿Cómo se consigue, entonces, que un objeto pueda poner sus propias reglas? Dejándole ejecutar código. La idea es tener un lenguaje de programación que se pueda introducir en los objetos, y que pueda llamar a las funciones que existen en el engine de Aetheria y hacer operaciones por sí sólo, sobre todo combinando éstas. Por ejemplo, la espada de Arlis podría tener un mini-programa que se ejecutase al dañar a un enemigo, multiplicando el daño hecho por algún factor y añadiéndolo a la energía mágica del jugador mediante alguna función para manejar dicha energía que tenga el engine de Aetheria.

Ese lenguaje es, de momento, el Ensamblador Virtual de Aetheria (EVA). Lo he implementado como un lenguaje de tipo ensamblador sencillamente porque no creo tener los conocimientos suficientes para crear un intérprete de un lenguaje de alto nivel. La idea es soportar un mini-lenguaje de alto nivel más tarde (no hay ningún problema en soportar varios lenguajes a la vez, ya ocuparán bastante memoria los objetos del juego como para preocuparse de la que ocupe ese soporte). Si alguien puede y quiere programar un mini-intérprete de alto nivel en Java, puede dirigirse a la sección de colaboraciones.

Usos: dónde puede haber código EVA.

El código en EVA se puede introducir en el fichero de especificación de cualquier objeto. Basta por incluirlo tras un número que indica que empieza un código EVA. A continuación van todas las funciones EVA que soporte ese objeto. ¿Cuándo se ejecutan dichas funciones? Depende de su nombre: cuando se den ciertas situaciones en el juego, se buscará un código EVA con ese nombre en el objeto y se ejecutará. Así, el código EVA se puede utilizar para:

- Comandos extra: comandos del juego que sólo sean reconocidos en una determinada habitación o con un determinado objeto.

- Eventos: reacciones de objetos ante estímulos externos, como por ejemplo el efecto de la espada descrita antes.

Cabe destacar que los hechizos son también objetos con estado que harán su efecto gracias al código.

He aquí un ejemplo de código EVA:

Fichero completo de una piedra que cuenta hasta diez al cogerla, incluyendo su código EVA. Sí, ya sé que una piedra que cuente hasta diez al cogerla no es muy útil (aunque a ver cuánto dinero se podría hacer con ella en un circo) pero es una prueba...

099 Piedra rojiza, el primer objeto con código.
001 000006
003 0
004 Piedra rojiza
010 0&0&Es una piedra rojiza de prueba.
011 0&0&piedra rojiza
012 0&0&piedras rojizas
013 0
014 Piedra rojiza$Piedra
015 Piedras rojizas$Piedras
016 4
017 4
080 .INIT
      $r0:0
    .DATA
      HOLA:0
    .CODE
      event_get: loadi $a0, ¡Atención! La piedra va a contar hasta 10.
                 function escribir
                 function newline
                 loadi $r0, 0
                 loadi $r1, 10
      bucle:     addi $r0, $r0, 1
                 mov $a0, $r0
                 function escribir
                 bne $r0, $r1, bucle
                 function newline
                 end
081 fin codigo
Especificación

Registros:

Como este ensamblador no corresponde a ninguna máquina física, los registros no tienen tamaño fijo, no están limitados a almacenar un valor entero o flotante: almacenan cadenas de caracteres. Esas cadenas de caracteres pueden representar un valor entero o flotante, o una cadena "real", según nuestras necesidades.

Los registros son:

$r0..$r99 Propósito general. Usados para cualquier tipo de operación. En el registro $r0 se solerá poner el valor 0.

$a0..$a9 Parámetros para método. Para pasar argumentos a los métodos que ejecutemos del engine de Aetheria.

$v0..$v9 Valores de retorno. Aquí escribirá el engine de Aetheria los valores de retorno de los métodos.

$pc Contador del programa. Número de instrucción que se está ejecutando. No debería ser necesario conocer su existencia para programar en EVA.

$ra Dirección de retorno de subrutina. Dirección a la que se vuelve al terminar una subrutina con "return". Tampoco debería ser necesario.

$Exc Registro de excepciones. Aquí escribirá el engine de Aetheria si se produce una excepción al ejecutar un método que ha pedido el programador en EVA, y escribirá el programador en EVA si quiere que sea su código el que produzca una excepción y la mande al juego.

$Obj Registro de objeto (para métodos). Para especificar qué objeto ejecuta un método del engine de Aetheria. Los objetos se identifican por su número o ID, esto es lo que hay que poner tanto aquí como en los registros $a0..$a9 si queremos pasar objetos como parámetros.

Segmentos:

El código EVA tiene segmentos de inicialización, de datos y de código.

El formato es el siguiente:

.INIT
(líneas de inicialización)
.DATA
(líneas de datos)
.CODE
(líneas de código)

Puede haber varios segmentos de un mismo tipo, aunque el efecto será exactamente el mismo que si se juntara todo en un solo segmento.

Segmento de inicialización (INIT):

El segmento INIT se utiliza para especificar los valores iniciales de uno o varios registros. Aquellos registros que no aparezcan en INIT (salvo el PC) contendrán un valor indefinido al ejecutar el código.

Ejemplo:

.INIT
   $r0:0
   $r1:27
   $r5:Hola, Mundo.

Los registros $r0, $r1 y $r5 tienen los valores iniciales que se especifican tras los dos puntos cada vez que Aetheria llama a cualquier rutina del código. El mismo efecto lo conseguiremos si al principio de todas las subrutinas que puedan ser llamadas directamente por el juego colocamos instrucciones de carga para inicializar esos registros.

Segmento de datos (DATA):

El segmento DATA se utiliza para inicializar las líneas de memoria. Como no estamos en una máquina real, no tenemos realmente una unidad de direccionamiento de memoria, así que cualquier cadena de caracteres se puede utilizar como dirección. Esto nos permite nombrar las direcciones según la variable asociada.

Ejemplo:

.DATA
   id_hechizo:0047
   habitacion:24
   jugador:Hegel el Enano.

Aquí, por ejemplo, la dirección de memoria "jugador" contiene el dato "Hegel el Enano".

Importante: Además de las líneas de memoria que inicialicemos nosotros en el segmento DATA, y las que podamos inicializar después mediante instrucciones de almacenamiento en memoria, pueden estar presentes otras líneas de memoria al empezar a ejecutarse el código. Son líneas que añade Aetheria para que desde el código EVA podamos acceder a la información que necesitaremos para interactuar con los objetos del juego. (se puede observar que con lo que hemos visto sólo tenemos datos constantes).
Estas líneas de información que añade Aetheria dependen de la rutina que se esté ejecutando, y para saber a cuáles se tiene acceso hay que consultar la especificación de la subrutina.
Ejemplo: Al ejecutar la subrutina "event_get" de un item, que se ejecuta cuando alguien lo coge, contamos con la línea "this", que contiene la ID del propio objeto. Podemos usar esa información para llamar a métodos del objeto. (si el objeto tuviera como ID 23, sería como si una línea de .DATA pusiera this:23, a efectos de event_get)

Segmento de código (CODE):

El segmento de código es el que contiene las instrucciones ejecutables del Ensamblador Virtual de Aetheria.

El formato de un trozo de código podría ser:

etiqueta1: instruccion1
			instruccion2
			instruccion3
etiqueta2: instruccion4
			instruccion5
		end

La ejecución de las instrucciones es secuencial: si Aetheria llamara a la subrutina llamada "etiqueta2" se ejecutarían las instrucciones 4 y 5, y si llamara a la llamada "etiqueta1" se ejecutarían 1, 2 y 3 (salvo en el caso de que las instrucciones contengan saltos). Cada instrucción debe estar en una línea diferente.

La instrucción end es necesaria para devolver el control al juego, y si no se pone provocará errores.

Nota: Como el "parser" es bastante limitado, al menos de momento no es posible dejar ninguna línea del segmento de código sin instrucción. Es decir, sería ilegal una sintaxis del tipo:

etiqueta1:
etiqueta2:
etiqueta3: instruccion1
		 instruccion2 
etiqueta4: instruccion3
		end

Sin embargo, si se quiere disponer de un código que haga lo mismo que éste (en ocasiones puede ser útil tener varias etiquetas en la misma instrucción, para que un mismo código se ejecute frente a varios eventos, por ejemplo) se puede conseguir así:

etiqueta1: nop
etiqueta2: nop
etiqueta3: instruccion1  
		  instruccion2 
etiqueta4: instruccion3 
		end

(la instrucción nop no hace nada)

Instrucciones:

El juego de instrucciones se parece mucho a los de procesadores RISC con arquitectura de carga/almacenamiento tipo MIPS o DLX. A los que conozcan el repertorio de instrucciones de algún procesador de este tipo, especialmente el MIPS, les resultará familiar, aunque el EVA es mucho más simple al no tener nada que ver con una máquina física.

Instrucción nop o noop:

No hace nada.

Formato: nop

Instrucción load o ld:

Carga datos de las líneas de memoria a un registro.

Formato: ld registro, etiqueta / load registro, etiqueta

Ejemplo: load $r4, this
(carga en el registro $r4 lo que hay en la línea de memoria etiquetada "this").

Instrucción loadi o li:

Carga un dato dado en la instrucción a un registro.

Formato: li registro, imm / loadi registro, imm

Ejemplo: loadi $r3, 47
(pone en el registro $r3 el valor 47)

Instrucción store o sd:

Guarda el dato de un registro a una línea de memoria.

Formato: sd registro, etiqueta / store registro, etiqueta

Ejemplo: store $r3, dato
(en la línea de memoria "dato" copia el dato del registro $r3)

Nota: la línea de memoria no tiene que existir (estar inicializada en el segmento de datos o por el juego): si no existe, se crea automáticamente.

Instrucción mov o move:

Mueve datos de un registro a otro.

Formato: mov rt, rs / move rt, rs (copia el dato de rs a rt)

Ejemplo: move $a0, $r2
(pone en $a0 el mismo dato que hay en $a2)

Instrucciones add, sub, mult y div:

Sumar, restar, multiplicar y dividir (respectivamente) los datos ENTEROS que hay en los dos registros fuente y poner el resultado en el registro resultado.

Formato: add (sub/mult/div) rd, rs, rt (suma los enteros que hay en rs y en rt y coloca el resultado en rd)

Ejemplo: div $a0, $r5, $r6
(hace dato[$a0] = dato[$r5] / dato[$r6] )

Nota: si los datos de alguno de los registros fuente no son enteros, no se escribirá el resultado en el registro destino rd, y el juego nos transmite una excepción, colocando en el registro de excepción $Exc el valor "NotANumber".
Si intentamos dividir por cero, sucede lo mismo, siendo la excepción "DivisionByZero".

Instrucciones addi, subi, multi y divi:

Sumar, restar, multiplicar y dividir (respectivamente) el dato ENTERO que hay en el registro fuente y el entero que se especifica en la instrucción.

Formato: addi (subi/multi/divi) rd, rs, imm (suma el entero que hay en rs con el entero imm y coloca el resultado en rd)

Ejemplo: addi $r1, $r1, 1
(hace dato[$r1] = dato[$r1] + 1)

Del mismo modo que en el caso anterior, si el registro no contiene un entero o el dato inmediato no lo es se producirá la excepción "NotANumber", y si intentamos dividir por cero la excepción "DivisionByZero".

Instrucciones fadd, fsub, fmult, fdiv, faddi, fsubi, fmulti y fdivi:

Hacen exactamente lo mismo que add, sub, mult, div, addi, subi, multi y divi, respectivamente; pero con números en punto flotante en lugar de enteros. Cualquier representación como strings de números en punto flotante que sea soportada por Double.valueOf(elString).doubleValue() en Java es válida.

Instrucciones bne, beq, bge, bgt, ble y blt:

Son las instrucciones de salto condicional: saltan a una dirección dada si los datos de los dos registros también dados cumplen una determinada condición.

Formato: beq (bne/bge/bgt/ble/blt) rs, rt, label

Ejemplo: beq $r1, $r2, subrutina1
(si el dato de $r1 es igual al de $r2, la siguiente instrucción que se ejecuta es la etiquetada "subrutina1", si no, se sigue la ejecución secuencialmente)

beq: salto si los datos de los dos registros son iguales.
bne: si son distintos.
bge: si dato[rs] >= dato[rt].
bgt: si dato[rs] > dato[rt].
ble: si dato[rs] <= dato[rt].
blt: si dato[rs] < dato[rt].

Nota: las comparaciones de igualdad, beq y bne, son comparaciones de cadenas, es decir, funcionarán tanto para números enteros como para cadenas genéricas. El resto de comparaciones son para números enteros, y provocarán la excepción NotANumber ante cualquier valor que no sea un entero.

Instrucción j:

Es el salto incondicional (goto). Salta a la dirección dada.

Formato: j label

Ejemplo: j bucle
(salta a la instrucción etiquetada "bucle")

Instrucción jal:

Es la llamada a subrutina (jump and link). Salta a la dirección dada, y guarda la dirección de la instrucción siguiente a jal en $ra para poder volver más tarde de la subrutina con la instrucción return. (el programador no se tiene por qué preocupar de $ra si todas las subrutinas son subrutinas hoja y no hay recursividad, pues jal y return lo manejan implícitamente)

Formato: jal label

Ejemplo: j subrutina1
(llama a subrutina1, y podremos hacer "return" para volver a donde estamos)

Instrucción return:

Es la instrucción de retorno de subrutina. Vuelve al punto donde invocamos la subrutina, es decir, a la dirección guardada en $ra por la última instrucción jal.

Formato: return

Instrucción push:

Pone el contenido del registro dado en la pila.

Formato: push registro

Ejemplo: push $ra
(pone el dato del registro $ra en la pila)

Instrucción pop:

Pone en el registro dado el dato más recientemente insertado en la pila de los que en ella hay, borrándolo de la pila.

Formato: pop registro

Ejemplo: pop $ra
(pone en $ra el dato de la cima de la pila)

Instrucción fun o function:

Llama a una función de Aetheria. Es la instrucción principal para comunicarse con el juego.

- El nombre del método llamada es el que se especifica en la instrucción.
- El objeto que invoca el método es aquél cuya ID está guardada en $Obj.
- Los parámetros para el método, si los tiene, se han de guardar en $a0, $a1, ..., por orden.
- Los valores de retorno, si los hay, aparecerán en $v0, $v1... y las posibles excepciones en $Exc.

Formato: fun método / function método

Ejemplo: fun getState
(pondrá en $v0 el valor del estado del objeto cuya ID hemos almacenado previamente en $Obj)

Instrucción exc o exception:

Lanza al juego una excepción de tipo EVASemanticException con el mensaje guardado en $Exc.

Formato: exc / exception

Métodos llamables por function:

La instrucción "function" nos permite utilizar métodos de Aetheria. He aquí una lista de los que de momento están soportados de momento.

La sintaxis que uso para denotar los métodos es:

clase :: retorno nombre_metodo ( arg1, arg2, ... ) throws excepcion1, excepcion2 ...

El nombre de las excepciones es la cadena que aparecerá en $Exc si se dan. Si en lugar de una clase aparece static, quiere decir que no nos tenemos que preocupar de a qué objeto corresponde el método, no necesitamos poner nada en $Obj (no quiere decir que sea un método estático en el programa Java)

[static] :: void escribir ( String s )

Saca por pantalla, o cualquiera que sea la salida de Aetheria, la cadena guardada en $a0.

[static] :: void newline ( void )

Saca por pantalla, o cualquiera que sea la salida de Aetheria, un salto de línea.

Entity :: int getstate ( void ) throws NotANumber, NotAnIDNumber

Devuelve en $v0 el estado del objeto cuya ID está en $Obj.

Entity :: void setstate ( int newstate , long tuleft ) throws NotANumber , NotAnIDNumber

Cambia el estado del objeto al nuevo estado dado durante las unidades de tiempo dadas.


Vade retro