[Tutorial] Writing libraries with YSI
#1

Introduction

YSI is not just a powerful library for writing gamemodes and filterscripts, it also has a lot of features which can make your own libraries much better. This tutorial will introduce four of them: y_hooks, y_master, y_playerarray and y_groups.

This tutorial will follow the development of a very simple library which allows you to create a text draw and show it to only certain players. This is a contrived example, but will do. The end result will have code like:

pawn Code:
new
    Group:g = Group_Create("tds"),
    Text:t = TextDrawCreate(20.0, 20.0, "Hello there");
Group_SetTextDraw(g, _:t, true);
Group_SetGlobalTextDraw(_:t, false);
TextDrawShow(t);
That code creates a YSI group and a text draw, then sets only a single group to be able to see the text draw and shows the text draw to everyone allowed to see it (the single group).

y_playerarray

y_playerarray creates a bit array of players to save on memory. It is slightly different to a regular bit array as the first full slot is reserved. The reasons for this are not relevant to this tutorial, but it's to do with future unreleased YSI libraries.

pawn Code:
#include <a_samp>
#include <YSI\y_playerarray>

new
    PlayerArray:g_players[Text:MAX_TEXT_DRAWS]<MAX_PLAYERS>;
That will declare an array large enough to hold data on who can see what text draws (or who is allowed to). To add a player do:

pawn Code:
PA+(g_players[td], playerid);
To remove one do:

pawn Code:
PA-(g_players[td], playerid);
y_hooks

What is y_hooks? It's a method for hooking callbacks in your library. When a new player connects to your server they aren't allowed to see any text draws. This requires hooking OnPlayerConnect to reset the data:

pawn Code:
#include <a_samp>
#include <YSI\y_playerarray>
#include <YSI\y_hooks>

new
    PlayerArray:g_players[Text:MAX_TEXT_DRAWS]<MAX_PLAYERS>;

Hook:TextDraw_OnPlayerConnect(playerid)
{
    for (new i = 0; i != MAX_TEXT_DRAWS; ++i)
    {
        PA-(g_players[Text:i], playerid);
    }
}
Simple. This is the sort of thing common to libraries - resetting a player when they connect, this just makes it slightly easier.

Extra Code

At this point I'll just define some extra code to make the library do something:

pawn Code:
#include <a_samp>
#include <YSI\y_playerarray>
#include <YSI\y_hooks>
#include <YSI\y_iterate>

new
    PlayerArray:g_players[Text:MAX_TEXT_DRAWS]<MAX_PLAYERS>;

Hook:TextDraw_OnPlayerConnect(playerid)
{
    for (new i = 0; i != MAX_TEXT_DRAWS; ++i)
    {
        PA-(g_players[Text:i], playerid);
    }
}

stock Text:TextDraw_Create(Float:x, Float:y, str[])
{
    new
        Text:td = TextDrawCreate(x, y, str);
    PA_Init(g_players[td], false);
    return td;
}

#define TextDrawCreate TextDraw_Create

stock TextDrawShow(Text:td)
{
    foreach (Player, playerid)
    {
        if (PA=(g_players[td], playerid))
        {
            TextDrawShowForPlayer(playerid, td);
        }
    }
}

stock TextDraw_SetPlayer(td, playerid, bool:set)
{
    if (set) PA+(g_players[Text:td], playerid);
    else PA-(g_players[Text:td], playerid);
}
In the code above "TextDraw_Create" is out custom creation function which makes a new text draw and sets all players to false. It then redefines "TextDrawCreate" to call our custom function. The second function loops through all players and displays the text draw to only the selected players. The final function either adds or removes a player from a text draw.

y_master

y_master is a distribution system. What this means is that if you include a library using the YSI master system in multiple scripts, they will all talk to each other. We want to be able to add and remove players from text draws in any script, so we need to use the master system:

pawn Code:
#include <a_samp>
#include <YSI\y_playerarray>
#include <YSI\y_hooks>
#include <YSI\y_iterate>

#define MASTER 7
#include <YSI\y_master>

new
    PlayerArray:g_players[Text:MAX_TEXT_DRAWS]<MAX_PLAYERS>;

RH:TextDraw_OnPlayerConnect[i](playerid)
{
    for (new i = 0; i != MAX_TEXT_DRAWS; ++i)
    {
        PA-(g_players[Text:i], playerid);
    }
}

RF@pt:Text:TextDraw_Create[ffs](Float:x, Float:y, str[])<x, y, str>
{
    new
        Text:td = TextDrawCreate(x, y, str);
    PA_Init(g_players[td], false);
    return td;
}

#define TextDrawCreate TextDraw_Create

RF:TextDrawShow[i](Text:td)
{
    foreach (Player, playerid)
    {
        if (PA=(g_players[td], playerid))
        {
            TextDrawShowForPlayer(playerid, td);
        }
    }
}

RF:TextDraw_SetPlayer[iii](td, playerid, bool:set)
{
    if (set) PA+(g_players[Text:td], playerid);
    else PA-(g_players[Text:td], playerid);
}
It should be fairly clear looking at the code above that not a lot has changed - but a lot of important changes have happened.

pawn Code:
#define MASTER 7
#include <YSI\y_master>
That includes the master system and sets a unique master value for it. If you have two scripts running and they both have a checkpoint streamer and an object streamer, you need a way for the cp streamers to talk and the object streamers to talk, but to not get confused between each other. This is what the master ID is for. In this case we are using 7, so all scripts which include your library should use 7 to uniquely identify your library.

pawn Code:
RH:TextDraw_OnPlayerConnect[i](playerid)
Here "Hook:" has changed to "RH:" - that's more or less the only change here. Because mutliple scripts include your library, multiple scripts have all the code for your library. When the server starts all these scripts talk to each other and decide who is going to be in charge so that all the code only gets done once. Using "RH:" ensures that this hook only gets called for the script in charge.

pawn Code:
RF@pt:Text:TextDraw_Create[ffs](Float:x, Float:y, str[])<x, y, str>
This is the most complex part. "RF" defines a "Remote Function" - as I just said multiple scripts can have the same code, but only one should be run. This means that all the other scripts should forward their calls on to the main one and return whatever it says. A basic example of this is:

pawn Code:
RF:PrintNumber[i](n)
{
    printf("Number: %d", n);
}
If all your scripts include that they can all call this function, but only one script will run it, which does mean that local data can be used. The format is "RF:FUNCTION_NAME[PARAMETER_TYPES](PARAMETERS}" - the parameter types are the letters which would appear in CallRemoteFunction (as that's exactly what they are).

Some functions however are slightly more complicated, especially ones with strings. If you have a string your parameters contain "[]" - however that't not valid in "CallRemoteFunction" so you need to use the "parameterised" version of "RF", which is "RF@p":

pawn Code:
RF@p:PrintString[s](str[])<str>
{
    printf("String: %s", str);
}
That adds an extra part to the end of the function definition with the variable name in. All the other functions do not use "RF@p" but they could do to remove the tag data on the variables:

pawn Code:
RF@p:TextDraw_SetPlayer[iii](td, playerid, bool:set)<td, playerid, set>
"RF@t" is a function which returns a tag, as "TextDraw_Create" does, you can also mix modifiers, which is where "RF@pt" (or "RF@tp") comes from. The other main modifier is "c", which defines a shorter version of the macro incase your generated code is too long (usually results in a compiler crash). You could therefore end up with "RF@pct" for very long function names.

y_groups

If you write a custom library you can use the group system to define access contols to your features. The group system is actually a wrapper around normal player permissions so you must provide a function called "Library_SetPlayer". The following is a minimal implementation of a library which can now use the groups system. The library uses the prefix "Custom" and can hold 50 of whatever it does:

pawn Code:
#include <a_samp>
#include <YSI\y_playerarray>
#include <YSI\y_hooks>
#include <YSI\y_iterate>

#define MASTER 7
#include <YSI\y_master>

#define _GROUP_MAKE_NAME_INTTEST<%0...%1>       %0TextDraw%1
#define _GROUP_MAKE_LIMIT_INTTEST               MAX_TEXT_DRAWS
#include <YSI\y_groups>

new
    PlayerArray:g_players[Text:MAX_TEXT_DRAWS]<MAX_PLAYERS>;

RH:TextDraw_OnPlayerConnect[i](playerid)
{
    NO_GROUPS()
    {
        for (new i = 0; i != MAX_TEXT_DRAWS; ++i)
        {
            PA-(g_players[Text:i], playerid);
        }
    }
}

RF@pt:Text:TextDraw_Create[ffs](Float:x, Float:y, str[])<x, y, str>
{
    new
        Text:td = TextDrawCreate(x, y, str);
    NO_GROUPS(_:td)
    {
        PA_Init(g_players[td], false);
    }
    return td;
}

#define TextDrawCreate TextDraw_Create

RF:TextDrawShow[i](Text:td)
{
    foreach (Player, playerid)
    {
        if (PA=(g_players[td], playerid))
        {
            TextDrawShowForPlayer(playerid, td);
        }
    }
}

RF:TextDraw_SetPlayer[iii](td, playerid, bool:set)
{
    if (set) PA+(g_players[Text:td], playerid);
    else PA-(g_players[Text:td], playerid);
}

#undef _GROUP_MAKE_NAME_INTTEST
#undef _GROUP_MAKE_LIMIT_INTTEST
Really not a lot has changed here:

pawn Code:
#define _GROUP_MAKE_NAME_INTTEST<%0...%1>       %0TextDraw%1
#define _GROUP_MAKE_LIMIT_INTTEST               MAX_TEXT_DRAWS
#include <YSI\y_groups>
That includes the groups library and defines the pattern that the functions are to take. Those three lines alone define the following functions:

pawn Code:
Group_SetTextDraw(Group:group, element, bool:set);
Group_SetGlobalTextDraw(element, bool:set);
Group_SetTextDrawDefault(Group:group, bool:set);
Group_SetGlobalTextDrawDefault(bool:set);
Note that there is no "Text:" tag, but that's another matter and a current limitation of the groups library. You can now use all the standard group functions to define who can use your system.

pawn Code:
NO_GROUPS()
    {
        for (new i = 0; i != MAX_TEXT_DRAWS; ++i)
        {
            PA-(g_players[Text:i], playerid);
        }
    }
Within YSI the groups system is entirely optional. If you use this method the groups system will just be included entirely, but the "NO_GROUPS" pattern is still valid. You could just remove your own initialisation code entirely here - it will only be called if groups aren't defined, but they are.

pawn Code:
NO_GROUPS(_:td)
    {
        PA_Init(g_players[td], false);
    }
The same applies here - if there are no groups use custom initialisation code, otherwise initialise this new text draw (in "td") using the groups library code.

Note that the y_groups system uses your "TextDraw_SetPlayer" function to call in to your basic per-player access code - so it is very important that that function exists and has those exact parameters (namely item, playerid, set).

Conclusions

This was a very brief introduction to using YSI for writing dynamic libraries, and using these features gives you direct access to features such as automatic group saving in the YSI user library etc. This tutorial was mostly a simple example built up, but shows the basics regardless. Any more information just ask here or read through the relevant includes (I dare you to look in y_groups and y_master).
Reply
#2

There are some examples already of other ready-made libraries?
Reply
#3

I love YSI lib, i can't imagine working without y_hooks, y_timers and y_groups/iterators.
But i have a question, what is the limit of y_hooks, i know that there is one because i found topic about it a year or so ago, but i can't find it again now. And how much does it affect server preformance ? ( i think that it only affects mod compile time, am I right ? )
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)