Registrarse

[Otros] FR | Cómo hacer comandos de scripts

Kaiser de Emperana

Called in hand
¡Hola!

Bueno básicamente es lo del título. Este va a ser un tutorial sencillo de como crear nuestros propios comandos de scripts, como lo puede ser el giveitem, lock, release, goto, etc.
Porque el sistema de scripts sinceramente da pena.

Bueno, la verdad es que es bastante sencillo de hacer, así que vamos con la explicación técnica. Yo me basé en una rom de FR 1.0, pero debe ser muy fácil de pasar a otra rom, sólo habría que buscar uno o dos offset.

¿Cómo funcionan los scripts?​
Como muchos sabrán, los scripts tienen una estructura que consta de un byte que indica el comando y una estructura de bytes que serán los parámetros del mismo.
Pero como los comandos pueden tener distintas cantidades y tipos de parámetros, el encargado de cargarlos en memoria es el comando mismo. La ejecución de un script consta de tres partes principales:
- Una rutina que lee los bytes que indican los comandos (que llamaré rutina principal, para simplificar).
- Una tabla que contiene todos los punteros a las rutinas de cada comando.
- Y cada rutina de cada comando.

La función de la rutina principal es únicamente ver el comando que hay que ejecutar, buscar en la tabla su rutina correspondiente y llamarla. Pasándole dos datos relevantes: una dirección de la RAM donde debe escribir el puntero al comando que sigue en la ejecución del script; y un puntero indicando cual es la posición actual de ejecución, que vendría a ser el puntero al byte que contiene el comando que se está ejecutando, es decir, que los parámetros al comando están en los bytes siguientes al mismo.

Después la rutina del comando básicamente tiene que hacer lo que le plazca. Luego debe escribir en la dirección de RAM, que le pasó la función principal, el puntero al comando que se debe ejecutar siguiente (que en general sería el byte siguiente a los parámetros del comando ejecutado, pero podría no serlo). Y finalmente debe cambiar el valor del registro 0 a 0, para indicar que el comando se ejecuto con éxito, (o 1 si no, pero la idea sería que eso no pase...).

¿Entonces qué?​
Bueno, para agregar nuevos comandos sólo debemos repuntear la tabla de rutinas y quitar sus limitadores.
FR tiene en total 214 comandos (aunque algunos no hace nada), que van desde 0x0 a 0xd5 (emerald tiene algunos más y ruby supongo que también).

La tabla que necesitamos repuntear está en: 0x15f9b4 y tiene 0x358 bytes.
Es decir que va desde 0x15f9b4 a 0x15fd0c.
La copiamos a un espacio vacío toca repuntear y quitar los limitadores.

Hay tres estructurtas de punteros, busquen lo siguiente con un editor hexadecimal y los van a encontrar sin problemas:
B4 F9 15 08 08 FD 15 08
El primer puntero sería el puntero a la tabla, cámbielo por la dirección a donde la hayan repunteado.
El segundo es el limitador, funciona como un puntero al último comando válido, en FR es 0x15fd08 que viene a ser...
0xd5 * 4 + 0x15d9b4
(ID ultimo comando) * (bytes que ocupa un puntero) + (puntero a la tabla)

Conclusión, usen un poco la calculadora, pongan el límite que les parezca y al final de la tabla agreguen los punteros a las rutinas de los comandos (el primer comando que agreguén será el 0xd6, el segundo 0xd7 y así pueden seguir hasta 0xff).

¿Y cómo programo el comando?​
Para programar la rutina del comando sólo hay que tener en cuenta 3 cosas.
Cómo dije la rutina principal nos va a pasar dos registros con datos realmente útiles, lo otro no importa. Esos son:
- r2: que contiene el puntero que apunta al comando que se está ejecutando. Pero ojo, con eso me refiero a la instrucción escrita en el script.
o sea, si tenemos un script tal que:
#org 0x81745c
lock
...
Y se está ejecutando la instrucción lock, r2 será igual a 0x0881745c.

- r4: que contiene un puntero a una dirección de la RAM (en FR es 0x03000eb0, pero usen el contenido del registro). Ah ese puntero le tienen que sumar 0x8 y en esa dirección deben escribir el puntero al comando siguiente al que se está ejecutando.

Concluyendo:
- Su comando puede o no recibir parámetros, eso depende de ustedes y cargarlos a memoria también. Usen el contenido de r2 para saber en donde estará los parámetros.
- Pueden programar la rutina que se les plazca, pero en algún momento tienen que escribir la instrucción siguiente al script en r4 + 0x8, de lo contrario el script va a entrar en un bucle infinito. Pueden usar el r2 para saber a donde deberían continuar.
- Cuando terminen su rutina, antes de hacer el return, deben cambiar el valor de r0 a 0.

Y con eso termina el tutorial. Pero para completar un poco hago una demostración práctica:

La idea
A mí se me ocurrió hacer la estructura de control case. Lo primero que tengo que hacer es pensar como va a estar estructurada. A mí lo que se me ocurrió fue:
byte(case) word(número de variable) byte(cantidad de posibilidades)
tabla {
byte(valor 1) dword(puntero a script 1)
byte(valor 2) dword(puntero a script 2)
...​
}

Mi idea es que el comando funcione similar a call, llamando a otro script según corresponda y que una vez termine pueda volver al script original con un return.

Por otro lado decidí poner el numero de posibilidades para no quitar valores posibles de las ramas del case.

Repunteando
Bueno ahora, repunteemos la tabla, tengo fe en que puedo programar esto luego...
Me voy a 0x15f9b4 y copio la tabla. En mi caso la voy a repuntear a 0x900000. Y ya que estoy repunteando, agrandemos la tabla al máximo:
0xff - 0xd5 = 0x19 = 25.
Habria que marcar de alguna forma estas direcciones para que ninguna herramienta haga algo indebido ahí. Yo por mi parte voy a agregar 25 punteros a la rutina del comando "nop", para que si por algún error ejecuto algo que no está programado, el juego no haga nada en lugar de clavarse.
(El primero puntero de la tabla es el del comando nop).
Bueno mi tabla está en 0x900000 y mi tabla llega hasta el comando 0xff. Pero cada puntero a comando ocupa 4 bytes, así que calculo:
0x900000 + 0xff * 4 = 0x9003b8
Anoto ese número y controlo es dirección. Allí debería estar el ultimo puntero de la tabla. Por lo que en 0x9003bc (<--0x9003b8 + 4) debería estar el primer byte cualquiera que no está relacionado a mi tabla.
Cosa que en mi caso se cumple, así que hice bien las cosas. Pasamos a lo siguiente.

Con esto copiamos los datos de la tabla, pero el rom sigue apuntando a la tabla vieja. Así que, como dije antes, buscamos:
B4 F9 15 08 08 FD 15 08
En las tres coincidencias tenemos que cambiar el primer puntero, por la nueva dirección de la tabla y el segundo por el número que les dije que anoten hace un momento (el puntero al puntero de la rutina del último comando).
Así que en mi caso remplazo los tres casos por:
00 00 90 08 B8 03 90 08
Con esto casi estamos.

Programando el comando
Bueno en esta parte no los puedo ayudar, cada uno programará lo que se le ocurra. Yo por mi parte programé lo siguiente:
Código:
.align 2
.thumb

main:
	push {r5,r6,lr}
	ldrb r1, [r2, #0x3]		@table_size
	
	ldrb r3, [r2, #0x1]
	ldrb r0, [r2, #0x2]
	lsl r0, r0, #0x8
	add r0, r3
	lsl r0, r0, #0x1
	ldr r3, .VARIABLE_TABLE 
	ldrh r3, [r3, r0]		@var_value

	add r2, #0x4			@current_table_offset
	mov r0, #0x5
	mul r0, r0, r1
	add r5, r2, r0			@callback_offset

	mov r0, #0x0			@table_position

loop:	
	cmp r0, r1
	beq no_coincidences
	ldrb r6, [r2]
	cmp r6, r3
	beq get_goto_pointer
	add r2, #0x5
	add r0, #0x1
	b loop


no_coincidences:
	mov r3, r5
	b end


get_goto_pointer:
	mov r0, #0x4
	ldrb r3, [r2, r0]

get_pointer_loop:
	cmp r0, #0x1
	beq set_callback
	sub r0, #0x1
	ldrb r1, [r2, r0]
	lsl r3, r3, #0x8
	add r3, r1
	b get_pointer_loop
	

set_callback:
	ldrb r0, [r4]
	cmp r0, #0x12
	bgt end
	add r0, #0x1
	strb r0, [r4]
	str r5, [r4, #0xc]

end:
	str r3, [r4, #0x8]
	mov r0, #0x0
	pop {r5,r6, pc}


.align 2
.VARIABLE_TABLE:
	.word 0x020270B8
Lo que hago en el main es cargar los distintos parametros: el número de variable, que lo transformo a su dirección en la ram y posteriormente leo su valor, y la cantidad de filas de la tabla (ramas del case). Sabiendo los bytes que ocupan los parámetros y la tabla, y la cantidad de filas de la misma, puedo calcular cual es la siguiente instrucción correspondiente del script (que guardo en r5).
Investigando el comando call (al que estoy imitando) me di cuenta que el puntero al callback al script inicial se escribe justamente después del puntero de la siguiente instrucción.

Despúes el comando entra en un bucle y comienza a leer todas las filas de la tabla buscando coincidencias con el valor de la variable. Si no hay, la rutina se va a no_coincidences, cambia el valor de r3 al puntero a la instrucción siguiente al case y finaliza en end.

Si encuentra una coincidencia, la rutina pasa a get_goto_pointer y pasa a cargar en memoria el puntero de la tabla.
Continúa en set_callback donde básicamente ejecuto un versión abreviada de la rutina de callback (podría haber llamado a la original pero entre el ldr, bl y el puntero, literalmente ocupa el mismo espacio lol, y así no complico la lectura con punteros). No la voy a detallar tanto, es básicamente lo que dije arriba del puntero y que al parecer los call están limitados a 0x12 (no lo mire mucho, ni se porque, pero es lo que parece).

Finalmente en end se procede a escribir el valor de r3 en la parte teórica del tutorial, r4 + 0x8. Nótese que en r3 se tiene el puntero a un script, en caso de que se hayan encontrado coincidencias en la tabla, o el puntero a la instrucción siguiente del script principal, en caso de que no. Luego se cambia el valor de r0 a 0 y se retorna.

Insertando la rutina
Compilamos lo de arriba, lo insertamos en donde se nos cante la gana, siempre y cuando sea múltiplo de 4 (en mi caso 0x9003bc).
Ahora hay que decidir que comando va a ser, yo elijo el primero, o sea 0xd6.
Y escribo en: 0x900000 + 0xd6 * 4 =
El puntero a mi rutina más 1, o sea: BD 03 90 08

¡A scriptear!
Bueno, mi comando funciona con variables, así que vamos a hacer unos scripts de prueba:
#dynamic 0x800000

#org @cero
lock
setvar 0x8000 0x0
msgbox @m0 0x6
release
end

#org @m0
= Cambiado a 0

#org @uno
lock
setvar 0x8000 0x1
msgbox @m1 0x6
release
end

#org @m1
= Cambiado a 1

#org @dos
lock
setvar 0x8000 0x2
msgbox @m2 0x6
release
end

#org @m2
= Cambiado a 2
Y ahora finalmente el script con el comando:
#define case 0xd6

#dynamic 0x800000
#org @main
lock
#raw case
#raw word 0x8000
#raw 0x2

#raw 0x1
#raw pointer @var=1

#raw 0x2
#raw pointer @var=2

msgbox @finalmsg 0x6
release
end

#org @finalmsg
= El script terminó.

#org @var=1
msgbox @msg1 0x6
return

#org @msg1
= ¡Uno!

#org @var=2
msgbox @msg2 0x6
return

#org @msg2
= ¡Dos!
Ya se que se ve horrible, pero en XSE más no puedo hacer... Habría que ir cambiando de compilador de scripts.

Bueno, no creo que nadie tenga problemas para entender como funciona lo que está aparte de los raw, son un montón de script simples aparentemente sin relacionar.
Ahi es donde entra el case:
Por si alguien no sabe raw es una utilidad de XSE que nos permite escribir bytes directamente (para más información lean la guía de XSE).

Primero esto:
#define case 0xd6
Por que no creo que nadie se quiera aprender los número de cada comando. Vendría bien hacer un header e importarlo cuando haga falta.

Después esto:
#raw case
#raw word 0x8000
#raw 0x2
Bueno esa cosa fea es la estructura del comando que definí en el primer paso de este ejemplo. El primer byte es de 0xd6 del id del comando, la word que sigue es la variable que le estamos pasando a la estructura para comparar y el 0x2 del final es la cantidad de filas de la tabla.

Por último, esto:
#raw 0x1
#raw pointer @var=1

#raw 0x2
#raw pointer @var=2
Sería la tabla. Una estructura de un byte y un puntero por fila. Como se puede observar, hay dos como lo especifiqué arriba y los punteros apuntan a los scripts que parecían inutilizados.

Si todo anda bien, los scripts segundarios tendrían que ser llamados si la variable es 1 o 2 (nótese que los mismos deben terminar en return, o no regresarán al script inicial).

Bueno, ahora queda compilar.
Como dato, guarden los scripts en un documento de texto porque XSE no los va a reconocer una vez compilados.

¡A probar!
Dejando en claro:
Si la variable es igual a...
1 el script debería decir: "¡Uno!"
2 el script debería decir: "¡Dos!"
otro "el script no haría nada

Y finalmente el script debería finalizar diciendo: "El script terminó.".

Así que, a probar...


Parece que no rompí nada.
¡Yeii!

Por si alguien no entendió alguna parte dejo un ips para que puedan comparar con un rom limpio.
Parche

Créditos:
Hakcmew y DavidJCobb por una documentación precisa de la estructura de los comandos, que hizo la investigación más fácil de lo que ya era.
Cosarara por tener en su web el mirror que encontré de la investigación de DavidJCobb.

Bueno, eso es todo.

¡Saludos!
 

cosarara97

Dejad de cambiar de nick
Miembro de honor
Respuesta: [FR] Cómo hacer comandos de scripts

¡Excelente tutorial! Añado que es muy útil leer el código de los comandos que ya están en el juego para entender como funciona la cosa.

Ya se que se ve horrible, pero en XSE más no puedo hacer... Habría que ir cambiando de compilador de scripts.
¡Exacto! Y voy a aprovechar la ocasión para hacer publicidad de Red Alien, si se me permite ;)
(Todo lo siguiente, usando la nightly que he sacado hoy)

Así quedaría el script, con mínimo esfuerzo:
Código:
#include "stdlib/std.rbh"

#dynamic 0x800000

#define switch #raw 0xd6; #hword $1; #raw $2
#define case #raw $1; #word $2

#org @main
lock
switch 0x8000 0x2
case 1 @var=1
case 2 @var=2
msgbox @finalmsg 0x6
release
end

#org @finalmsg
= El script terminó.$$

#org @var=1
msgbox @msg1 0x6
return

#org @msg1
= ¡Uno!$$

#org @var=2
msgbox @msg2 0x6
return

#org @msg2
= ¡Dos!$$
El siguiente paso sería añadir switch como comando en commands.txt:
Código:
    "switch": {"hex": 0xd6, "args": ("var, n", (2, 1))}
Y como deberes para casa, añadir verdadera sintaxis y soporte para decompilación.

Código:
#include "stdlib/std.rbh"

#dynamic 0x800000

#define switch #raw 0xd6; #hword $1; #raw $2
#define case #raw $1; #word $2

#org @main
lock
switch 0x8000 0x2
case 1 @var=1
case 2 @var=2
callstd 6
msgbox @finalmsg 0x6
release
end

#org @finalmsg
= El script terminó.$$

#org @var=1
loadpointer @msg1
return

#org @msg1
= ¡Uno!$$

#org @var=2
loadpointer @msg2
return

#org @msg2
= ¡Dos!$$
 
Arriba