[Guía] Programación asíncrona con PawnPlus

Aprende lo que no sabes de este lenguaje y encuentra herramientas y códigos útiles.

Moderador: Ayudantes

Reglas del Foro
  • Si tu código es corto, no crees un tema nuevo para liberarlo, publica un mensaje en el tema [Funciones] ¡Publica tu código aquí!.
  • Si creas una guía, debes explicar el código claramente. Evita poner la explicación en forma de comentarios dentro del mismo y no olvides utilizar el BBCode correspondiente: [Pawn]Código aquí[/Pawn].
  • No postear códigos sin probar. Publicar códigos con errores (que impidan compilar el script para el cual está destinado) en este apartado es motivo de sanción.
  • Si tu código necesita plugins, includes u otros códigos adicionales para funcionar, debes mencionarlo en el mensaje.
  • Si has utilizado códigos de otros autores, recuerda colocar los créditos correspondientes. (El plagio es motivo de sanción).
  • Si tu aporte es para un GameMode en específico, debes publicar el tema en el apartado "Guías y Aportes para GameModes específicos".
Responder
Avatar de Usuario
Graber
Ayudante
Ayudante
Mensajes: 194
Registrado: 10 Abr 2016 19:16
Contactar:
Reputación: 22

26 Mar 2019 12:33

La programación en SA-MP esta basada en eventos basicamente, lo que significa que respuestas a todos los eventosson manejadas por callbacks hechos por el usuario. Sin embargo, en muchos casos, el evento es una respuesta a una única acción en el script, y tener que usar una función public en estas condiciones singulares infla el script con muchas funciones public innecesarias, y dividiendo aun más el código.

PawnPlus introduce la programación asíncrona mediante tareas (tasks) a Pawn, inspirada en C# en la parte de sintaxis.

Tarea (Task)
Una tarea, en contraste con un evento (callback), tiene un principio y un final. Inicia con una acción, luego hace algún "trabajo", y luego cambia de estado a "terminada".

El trabajo en sí no tiene que ser hecho por el script, o no tiene que ser un trabajo activo siquiera. Lo unico importante es que una vez que este proceso esté terminado, la tarea estará terminada (con un resultado opcionalmente).

Un script puede "esperar" a cualquier acción para que sea terminada. La espera se hace sin bloqueo de la ejecución, lo que significa que el servidor entero no se congelará mientras espera (a diferencia de, por ejemplo, mysql_query, que es un buen ejemplo de una espera que bloquea la ejecución). La ejecución continuará normalmente.
 Codigo Pawno:
1
2
3
4
5
6
7
8
9
10
11
12
13

new Task:task_fin; // Variable en donde guardaremos nuestra task/tarea

public OnFilterScriptInit() // Cuando inicie el filterscript
{
    task_fin = task_new(); // Crearemos una nueva task/tarea
    await task_fin; // Esperaremos por ella
    print("Adiós!"); // Se ejecutará cuando termine esa tarea
}

public OnFilterScriptExit() // Cuando termine el filterscript
{
    task_set_result(task_fin, 0); // Marcar la task como "terminada", y ejecutar cualquier código que esté esperando por ella
}
  Cantidad de llaves: Abiertas(2)-Cerradas(2) | Lineas Totales: 13
Aquí, una tarea es creada y su ID es asignada a task_fin. Luego, la pseudo-declaración await es usada para pausar la ejecución del script y esperar a la realización de esta task. Esta pseudo-declaración es un alias a la función nativa task_await.

Cuando se empieza a esperar, la ejecución de la función public actual es pausada, y la función es forzada a hacer return. El estado de la memoria actual dentro de esta función se guardará y se asociara con la task, asi que cuando se complete la task (con task_set_result o externamente), se resumirá el código pendiente con todas sus variables intactas (menos las globales/statics que estan guardadas por separado).

Esta función public debe retornar un valor específico, en algunos casos ignorar esto puede llevar a código incorrecto:
 Codigo Pawno:
1
2
3
4
5
6

public OnPlayerUpdate()
{
    await task_ms(1000); // task_ms es una función que crea una task que automáticamente se completa en el numero específicado de millisegundos.
    //...
    return true;
}
  Cantidad de llaves: Abiertas(1)-Cerradas(1) | Lineas Totales: 6
Ya que la función debe retornar obligadamente, retorna 0 ya que el valor de retorno actual aun no fue especificado en ese momento. En este caso hará que todos los jugadores salgan pauseados. Hay dos maneras de solucionar esto:

La pseudo-declaración yield (alias a task_yield) guarda un valor de retorno en una ubicación especial, que luego se usa cuando la función se ve obligada a retornar.
 Codigo Pawno:
1
2
3
4
5
6
7

public OnPlayerUpdate(playerid)
{
    yield true; // Setear el valor de retorno a true
    await task_ms(1000); // Esperar 1 segundo, obliga a retornar el public con el ultimo valor en yield.
    //...
    return false; // Este valor será ignorado.
}
  Cantidad de llaves: Abiertas(1)-Cerradas(1) | Lineas Totales: 7
En ese lugar, yield no hace ninguna espera, y la función public usará el último valor con que yield especificó cuando deba retornar obligadamente. Este comportamiento puede cambiar asi que es mejor solo usar yield una sola vez y antes de cualquier await.

La otra solución es llamar a otra función public indirectamente:
 Codigo Pawno:
1
2
3
4
5
6
7
8
9
10
11
12

public OnPlayerUpdate(playerid)
{
    CallLocalFunction("PostPlayerUpdate", "d", playerid); // llamar a otra función public indirectamente
    return true; // se retornará como se espera
}

forward PostPlayerUpdate(playerid);
public PostPlayerUpdate(playerid)
{
    await task_ms(1000); // la función que se pausa y es obligada a retornar ahora es PostPlayerUpdate, pero no OnPlayerUpdate
    //...
}
  Cantidad de llaves: Abiertas(2)-Cerradas(2) | Lineas Totales: 12
Las esperas a tasks pausan la ejecución de la función public (mas no las normales) mas externa, la que más recientemente se usó, asi que la función pausada en este caso es PostPlayerUpdate, mientras que OnPlayerUpdate sigue su ejecución como si nada hubiese pasado.

Timers
Hay dos maneras en las que un script puede esperar por un tiempo determinado. Puedes esperar por una cantidad específica de millisegundos, o por una cantidad específica de server ticks (la frecuencia de estos ticks se puede configurar con la opción sleep de server.cfg). Esta última es muy útil para demorar mensajes o acciones por la menor cantidad de tiempo posible.
 Codigo Pawno:
1
2
3
4
5
6

public OnPlayerConnect(playerid)
{
    SendClientMessage(playerid, -1, "Conectado");
    wait_ticks(1); // esperar 1 tick
    SendClientMessage(playerid, -1, "Hola!");
}
  Cantidad de llaves: Abiertas(1)-Cerradas(1) | Lineas Totales: 6
 Codigo Pawno:
1
2
3

SendClientMessage(playerid, -1, "Adios!");
wait_ticks(1); // esperar 1 tick
Kick(playerid);
  Cantidad de llaves: Abiertas(0)-Cerradas(0) | Lineas Totales: 3
El primer ejemplo manda el segundo mensaje justo despues de que aparezca el mensaje de "Connected to [server]" en el cliente. En el segundo ejemplo, el mensaje aparece antes de ser kickeados, evadiendo el ya conocido bug de SA-MP.

Nótese que, en contraste con la función SetTimer, un intervalo de 0 no hace ninguna espera. Para esperar 1 tick (o sea, el siguiente tick), un valor de 1 es necesario.

Estas funciones especiales, wait_ms y wait_ticks, son equivalentes a `await task_ms` y `await task_ticks` pero no necesariamente crean una task por separado, asi que no tendrán ningun valor de retorno específico. Estas automáticamente esperarán sin tener que usar await. Si usas un numero negativo, el código que sigue nunca se ejecutará y será descartado.

Conversión de programación basada en eventos a basada en tasks
Esperar una cantidad de tiempo es útil, pero algo aun más útil es esperar a que pase una acción específica. Digamos que queremos esperar hasta que algun player se conecte al server. Esto se podría ver inicialmente así:
 Codigo Pawno:
1
2
3
4
5
6
7
8
9
10

new Task:player_connect;
stock Task:CuandoSeConecteUnJugador()
{
    player_connect = task_new();
}

public OnPlayerConnect(playerid)
{
    task_set_result(player_connect, playerid);
}
  Cantidad de llaves: Abiertas(2)-Cerradas(2) | Lineas Totales: 10
Luego podrías hacer `await CuandoSeConecteUnJugador();` (o usar `new playerid = await CuandoSeConecteUnJugador();` si quieres el playerid), pero hay un par de problemas con este código. Principalmente, una task nueva es creada cada vez que llamas a CuandoSeConecteUnJugador, pero solo una es completada en OnPlayerConnect. Podrías crear un array de estas tasks, pero tener que crearla por cada evento convertido en tarea es muy impráctico.

Para este problema, una técnica conocida como "additional callback handlers" (receptores de callback adicionales) se puede usar. Desde un punto de vista abstracto, un callback es una manera de interactuar entre el servidor y el script, pero de la misma manera en la que un callback puede tener un receptor de callback en cada script, cada script podría tener mas de un receptor para un callback.
 Codigo Pawno:
1
2
3
4
5
6
7
8
9
10

public OnFilterScriptInit()
{
    pawn_register_callback("OnPlayerConnect", "MiPlayerConnect", "");
}

forward MiPlayerConnect(playerid);
public MiPlayerConnect(playerid)
{
    //...
}
  Cantidad de llaves: Abiertas(2)-Cerradas(2) | Lineas Totales: 10
MiPlayerConnect ahora es llamado cada vez que OnPlayerConnect deba ser llamado en el script, incluso cuando dicha función no está definida. Esto por sí mismo no es tán impresionante, pero parámetros adicionales pueden ser especificados para aparecer en el receptor, creando lo que se conoce como un "delegado" (pero con diferentes valores)
 Codigo Pawno:
1
2
3
4
5
6
7
8
9
10

public OnFilterScriptInit()
{
    pawn_register_callback("OnPlayerConnect", "MiPlayerConnect ", "d", 12);
}

forward MiPlayerConnect (parametro, playerid);
public MiPlayerConnect (parametro, playerid)
{
    //...
}
  Cantidad de llaves: Abiertas(2)-Cerradas(2) | Lineas Totales: 10
parametro se puede identificar con la instacia de dicho receptor, pero aun esta a un paso de la perfección. Usar el especificador "e" es mucho mejor. Se usa para referirse a la instacia actual de evento-delegado, y puedes usarlo para de-registrarlo:
 Codigo Pawno:
1
2
3
4
5
6
7
8
9
10
11

public OnFilterScriptInit()
{
    pawn_register_callback("OnPlayerConnect", "MiPlayerConnect", "e");
}

forward MiPlayerConnect(CallbackHandler:id, playerid);
public MiPlayerConnect(CallbackHandler:id, playerid)
{
    pawn_unregister_callback(id);
    //...
}
  Cantidad de llaves: Abiertas(2)-Cerradas(2) | Lineas Totales: 11

EN CONSTRUCCIÓN

Pueden preguntarme cosas por aqui, espero que aprecien este tutorial y la llegada de este nuevo paradigma de programación a Pawn.

Muchos agradecimientos a IllidanS4 por este tutorial (originalmente en inglés) y por crear este impresionante plugin.
Imagen

No doy soporte por privado y no estoy disponible para ningún trabajo/proyecto/lo que sea. Eviten MPs de ese tema

Qarper
Aprendiz
Aprendiz
Mensajes: 8
Registrado: 25 Dic 2015 22:52
Ubicación: Viviendo en Argentina
Reputación: 0

27 Mar 2019 09:45

Genial,
ya era hora de que alguien lo hiciera. PawnPlus debería ser más conocido en la comunidad hispana.
Responder