I've seen this happen over and over again - people have a team helping them develop a mode then one of the scripters gets annoyed and leaves, taking the script with them (sometimes releasing it, sometimes setting up their own server). Once this has happened there's nothing you can do about it, you need safeguards in place from the start to ensure that it can't happen*. This tutorial uses the y_master and y_lock systems to develop and deploy full scripts, without any one coder (except you as the lead) ever having access to all the code, but still being able to script and test.
The basic idea is to split the script up in to modules - have each scripter work on one module/part and distribute those modules as compiled filterscripts to the other scripters. Then, when your code is ready to be put on the server, you just compile everything in to one big script (if you want).
- You should also only use people you trust completely.
The basic technology (I say basic, it's one of the most complex libraries I've
ever written) behind this is the YSI y_master library. This system allows you to
write scripts and define functions that are not in the current script, instead
being in another script, but still compile and run everything. The syntax is
based on that of publics - using forward
and public
to define functions, but
instead uses foreign
and global
:
foreign MyFunc(a, string:b[]);
global MyFunc(a, string:b[])
{
printf("%d %s", a, b);
return 1;
}
The foreign
keyword means "this function is defined somewhere, but not here",
and is used in basically the same way as forward
(as seen above). One feature
common to most YSI libraries is the use of the string:
tag on strings - this
MUST be used when an array is a string. When the array is not a string you leave
off the string:
tag but instead you MUST follow the array with the size:
foreign Func2(arr[], size);
Failure to do this will result in some very cryptic error messages (same as with
missing the string:
tag as then the compiler will think that it's just a
normal array that should be followed by its size).
If foreign
appears in a script, you can call that function in that script,
even if it never actually gets defined in the script:
#define MASTER 4
#define YSI_IS_CLIENT
#include <YSI\y_master>
foreign MyFunc(a, string:b[]);
main()
{
MyFunc(56, "hi there");
}
That above is a complete script - I'll come to what the stuff at the top means later.
For this to work, however, you need another script (in this case a filterscript
as that code above is a gamemode), with the corresponding global
declaration
to actually DO something.
global
is like public
- it defines the function itself:
global MyFunc(a, string:b[])
{
printf("%d %d", a, b);
return 1;
}
If global
appears in a script you must also have foreign
in that script (if
you don't you'll get a forward
warning and the script might crash the server).
The filterscript partner to the gamemode above is:
#define MASTER 4
#define YSI_IS_SERVER
#include <YSI\y_master>
foreign MyFunc(a, string:b[]);
global MyFunc(a, string:b[])
{
printf("%d %s", a, b);
return 1;
}
When the gamemode calls MyFunc
THIS is the code that gets run, despite being
in another script. Under the hood this calls "CallRemoteFunction", but has the
added advantage of compile-time parameter checking. If you just did:
CallRemoteFunction("MyFunc", "si", 56, "hi there");
The code will compile fun but will not run. For one thing MyFunc
has been
misspelled as MyFonc
, and the parameter specifiers are backwards (si
instead
of is
). As far as the compiler is concerned this is fine because it doesn't
know WHAT the function does, only that it takes a couple of strings and some
other things - as long as you do that it's happy.
y_master on the other hand, when using foreign
and global
will give relevant
warnings and errors when you misspell function names and get parameters wrong
(as with any other function call).
If a global
function doesn't return something, it must be explicitly marked as such:
global NoReturn()
{
// Wrong.
}
global void:NoReturn()
{
// Right.
}
They can also return strings if they are declared using string:
:
global string:StringReturn()
{
new
ret[YSI_MAX_STRING];
return ret;
}
The return string MUST have size YSI_MAX_STRING
, but you can set how big this
is if you want (default 144):
#define YSI_MAX_STRING 512
#include <YSI\y_master>
You can also return any other tag (Float:
, bool:
etc) as you please.
Both code examples had headers before the function declaration:
#define MASTER 4
#define YSI_IS_CLIENT
#include <YSI\y_master>
#define MASTER 4
#define YSI_IS_SERVER
#include <YSI\y_master>
Lets look at each line individually:
#define MASTER 4
Some scripts may have some code, other scripts might not. For example
filterscript fs_streamer
might contain your streamer and filterscript
fs_users
might hold your user system (admin levels etc). You don't want user
function calls going to the the streamer filterscript where they can't be used,
do you? Each independent library (or code section) has its own MASTER
number
so that the scripts know where to send the relevant calls:
This is using MASTER
number 1.
#define MASTER 1
#include <YSI\y_master>
static stock
gsLoggedIn[MAX_PLAYERS];
foreign bool:IsLoggedIn(playerid);
global bool:IsLoggedIn(playerid)
{
return gsLoggedIn[playerid];
}
This is using MASTER
number 2.
#define MASTER 2
#include <YSI\y_master>
foreign AddObject(model, Float:x, Float:y, Float:z);
global AddObject(model, Float:x, Float:y, Float:z)
{
new
objectid = /* something */;
return objectid;
}
#define YSI_IS_SERVER
#include <funcs_streamer>
#define YSI_IS_SERVER
#include <funcs_users>
#define YSI_IS_CLIENT
#include <funcs_streamer>
#include <funcs_users>
main()
{
}
When the gamemode calls IsLoggedIn
, the code in fs_users
will be called.
When the gamemode calls AddObject
, the code in fs_streamer
will be called
(pad both out as appropriate). The gamemode has BOTH includes, each using a
different MASTER
value, but this is fine (and correct). All functions defined
after MASTER
use that value until MASTER
is redefined (note that you don't
need #undef
):
This is using MASTER
number 1.
#define YSI_IS_CLIENT
#define MASTER 1
#include <YSI\y_master>
static stock
gsLoggedIn[MAX_PLAYERS];
foreign bool:IsLoggedIn(playerid);
global bool:IsLoggedIn(playerid)
{
return gsLoggedIn[playerid];
}
#define MASTER 2
#include <YSI\y_master>
foreign AddObject(model, Float:x, Float:y, Float:z);
global AddObject(model, Float:x, Float:y, Float:z)
{
new
objectid = /* something */;
return objectid;
}
main()
{
}
You also need to include YSI\y_master
every time you reset MASTER
(as the
above expanded code shows).
The code above uses YSI_IS_SERVER
in the filterscripts and YSI_IS_CLIENT
in
the gamemode. This is because the both gamemode and fs_streamer
have
global AddObject
, and both the gamemode and fs_users
have
global IsLoggedIn
. If both scripts define the same function, how do we know
which one to call?
The gamemode is set as YSI_IS_CLIENT
. This means that any global function
definitions are just ignored (in fact they're set to stock
functions so that
the compiler can remove them entirely). The filterscripts are set as
YSI_IS_SERVER
, this means that any calls for a function they have go directly
to them, no other script. They are both YSI_IS_SERVER
because they both have
different bits of code (set by different MASTER
values) to handle.
The other way to handle this is to not define anything. If you remove
YSI_IS_SERVER
from the filterscripts and YSI_IS_CLIENT
from the gamemode the
server will select one script to handle all requests when it starts (usually the
filterscripts as they load first). This means that the compiler will NOT remove
the extra code from the gamemode because it MAY be needed. This style is
internally referred to as "cloud" because the server selects one system to
handle everything from all available processing resources.
There is one more type: YSI_IS_STUB
. In this case it is actually an error to
have any global
declarations in your mode - you can ONLY have foreign
declarations.
This script is fine:
#define MASTER 4
#define YSI_IS_STUB
#include <YSI\y_master>
foreign MyFunc(a, string:b[]);
main()
{
MyFunc(56, "hi there");
}
This script is wrong and will give an error because it contains global
:
#define MASTER 4
#define YSI_IS_STUB
#include <YSI\y_master>
foreign MyFunc(a, string:b[]);
global MyFunc(a, string:b[])
{
printf("%d %s", a, b);
return 1;
}
main()
{
MyFunc(56, "hi there");
}
Using this instead of any of the compile options above entirely disables the YSI master system and compiles everything as a single mode with no remote calls at all.
YSI_IS_SERVER
, YSI_IS_CLIENT
and YSI_IS_STUB
define that setting for the
whole mode. You can modify these settings per module (i.e. every time you
include YSI\y_master
:
#define MASTER 4
#define YSIM_U_DISABLE
#include <YSI\y_master>
As with MASTER
, this will change the setting until you next include
YSI\y_master
. There are six definitions:
- YSIM_S_ENABLE - Turn on
SERVER
mode for the currentYSI\y_master
inclusion. - YSIM_S_DISABLE - Turn off
SERVER
mode. - YSIM_C_ENABLE - Turn on
CLIENT
mode. - YSIM_C_DISABLE - Turn off
CLIENT
mode. - YSIM_U_ENABLE - Turn on
STUB
mode.S
was taken, so this is forstUb
. - YSIM_U_DISABLE - Turn off
STUB
mode. Note that every YSI library that uses y_master has this set. All YSI libraries also have the ability to revert all settings after they have been included, so are transparent to your use of the master system.
YSI\y_master
is the entry point to the master system, and unlike most includes
it can be imported many times with different settings each time:
#define MASTER 4
#include <YSI\y_master>
#define MASTER 5
#include <YSI\y_master>
#define MASTER 6
#include <YSI\y_master>
You can use the default settings, which are MASTER
number 23 (reserved for
default
) and CLOUD
mode simply by including it:
#include <YSI\y_master>
It is recommended to do this after all other includes in your mode so that
things like commands do not inherit the previously set MASTER
and not get
called as a result:
// Uses "MASTER" number 25, but auto-reverts.
#include <YSI\y_commands>
// Uses "MASTER" number 2, but doesn't reset.
#include <funcs_streamer>
// Reset to defaults.
#include <YSI\y_master>
main()
{
}
YCMD:hi(playerid, params[], help)
{
}
The code above will set YCMD:hi
to use MASTER
number 23
. Note that this is
a special case master number designed to allow commands to be called where they
are defined when using y_commands
. You don't want your command to not get
called just because you set YSI_IS_CLIENT
when it is blatantly only defined in
one script (as used to happen, causing problems). If you DO want this to happen,
just set MASTER
to something other than 23
.
Additionally, MASTER
numbers over 15
are reserved for YSI use only.
The section on y_master was quite long as it's a complex system. This system on the other hand is very simple:
#define BIND_IP MAKE_LOCK_IP(127.0.0.1)
#include <YSI\y_lock>
bind 127.0.0.1
That's all you need - YSI\y_lock
as the FIRST include in your script and
bind 127.0.0.1
in server.cfg
or "bind": "127.0.0.1"
in samp.json
(must
match the IP in MAKE_LOCK_IP
). When you do this that script will ONLY run on
localhost, meaning that only one person can connect to it (the person also
playing on that computer). If they don't bind to the correct IP they will get
very strange behaviour.
Now that we've shown how "y_lock" can be used to only run scripts locally (i.e. not online) and how "y_master" can be used to run code in other scripts seamlessly, lets see how they can be combined to protect your mode from theft:
This is using MASTER
number 1.
#define MASTER 1
#include <YSI\y_master>
foreign bool:IsLoggedIn(playerid);
#tryinclude <impl\impl_users>
static stock
gsLoggedIn[MAX_PLAYERS];
global bool:IsLoggedIn(playerid)
{
return gsLoggedIn[playerid];
}
#define MASTER 2
#include <YSI\y_master>
foreign AddObject(model, Float:x, Float:y, Float:z);
#tryinclude <impl\impl_streamer>
global AddObject(model, Float:x, Float:y, Float:z)
{
new
objectid = /* something */;
return objectid;
}
#include <YSI\y_lock>
#define YSIM_S_ENABLE
#include <funcs_streamer>
#include <YSI\y_lock>
#define YSIM_S_ENABLE
#include <funcs_users>
#define YSI_IS_STUB
#include <YSI\y_lock>
#include <funcs_streamer>
#include <funcs_users>
main()
{
}
The code above entirely separates out concerns. The global
declarations of the
users
code are in a separate file to the foreign
declarations, same with the
streamer
code, and they use #tryinclude
instead of #include
to pull in the
full implementations. What's more, the gamemode sets YSI_IS_STUB
, despite the
fact that it includes files that contain global
.
The secret is that not everyone has every file. The person working on the
streamer will have funcs_streamer.inc
, impl\impl_streamer.inc
and
fs_streamer.pwn
. They will also have funcs_users.inc
but will NOT have
impl\impl_users.inc
or fs_users.pwn
If they try and include
funcs_users.inc
to use some of the uses system functions they will need to do:
#define YSIM_U_ENABLE
#include <funcs_users>
#define _YSIM_OVERRIDE 2
#include <YSI\y_master>
That will enable STUB
mode, include funcs_users.inc
to get the foreign
definitions, but not impl\impl_users.inc
because they don't have it. It will
then REVERT to the settings before (for MASTER
number 2) and continue with the
rest of the impl_streamer.inc
code:
#define MASTER 2
#include <YSI\y_master>
foreign AddObject(model, Float:x, Float:y, Float:z);
#tryinclude <impl\impl_streamer>
#define YSIM_U_ENABLE
#include <funcs_users>
#define _YSIM_OVERRIDE 2
#include <YSI\y_master>
global AddObject(model, Float:x, Float:y, Float:z)
{
new
objectid = /* something */;
/* Code now using the users system */;
return objectid;
}
When you, as head scripter, decide to compile the whole mode for use you simply do:
//#define YSI_IS_STUB
//#include <YSI\y_lock>
#define YSI_NO_MASTER
#include <funcs_streamer>
#include <funcs_users>
main()
{
}
And, as you have every file all the code will be pulled together in to one big
mode. Note that YSI_NO_MASTER
overrides every other setting for the master
system (there is YSIM_T_ENABLE
and YSIM_T_DISABLE
as well, but they're
untested).
On the other hand, when your scripters are testing things, you simply compile
all the filterscripts and give them out to people. These will be compiled and
locked, so they can only use them locally, but will contain all the code that
they don't have so they can still test the mode. For this to work your .pwn
files (fs_streamer.pwn
, fs_users.pwn
, gamemode.pwn
etc) will have to be
skeleton files with all other code defined in includes so that you can pull it
all together as required.
This system can be integrated with a content versioning system such as SVN by
limiting file or directory access to different users so they can't download
every file, only the ones they're allowed to work on. This way only you ever
have every all the source code, and only you can run the mode on an IP other
than 127.0.0.1
- meaning that no-one else can steal it.