[CHANGELOG] Noteworthy changes in the plugin.

Started by ., Aug 24, 2016, 09:25 PM

Previous topic - Next topic

.

This topic will exist here to point out noteworthy additions and changes in the plug-in. Don't expect these changes to exist in the binaries immediately. There should be an edit with a note at the end of a post when binaries caught up with the changes.
.

.

Dynamic dispatching for meta-methods on base types:

local v = Vector3(3, 3, 3);
/* These are equivalent to
v.x + value
v.y + value
v.z + value
*/
print(v + Vector3(2, 2, 2));
print(v + 2);
print(v + 2.0);
print(v + true);
print(v + false);
print(v + null);

print("----------------");

local n = SLongInt(23);

print(n + SLongInt(2));
print(n + ULongInt(2));
print(n + 2);
print(n + 2.0);
print(n + true);
print(n + false);
print(n + null);

Output:
[USR] 5.000000,5.000000,5.000000
[USR] 5.000000,5.000000,5.000000
[USR] 5.000000,5.000000,5.000000
[USR] 4.000000,4.000000,4.000000
[USR] 3.000000,3.000000,3.000000
[USR] 3.000000,3.000000,3.000000
[USR] ----------------
[USR] 25
[USR] 25
[USR] 25
[USR] 25
[USR] 24
[USR] 23
[USR] 23



Binary Status: available
.

.

More then often people share snippets of code that require to initialize and/or cleanup various things required for them to run. Usually, people put the code required for such things in plain sight and tell the user to copy that code and place it in their initialization function.

The only problem with that code is that it introduces possible unknown behavior if the user places that initialization code before or after doing something that the snippet could not anticipate. There's simply nothing to guarantee that their initialization code will be executed before or after the user initialization code.

Not to mention that it looks awful from a design perspective.

For that purpose I've introduce 3 possible stages that accept any number of functions to be bound to them in order to be called (in the same order they were added) when that particular stage is reached. The stages are:

  • PreLoad: Functions bound to this stage are called before the main ScriptLoaded event and allow the snippets to initialize stuff that the user will probably need once the ScriptLoaded event is triggered. They only received argument is the specified payload, if any.
  • PostLoad: Functions bound to this stage are called after the main ScriptLoaded event and allow the snippets to collect information that the user probably generated when the ScriptLoaded event was triggered. They only received argument is the specified payload, if any.
  • Unload: Functions bound to this stage are called when all scripts are about to be unloaded. This includes both a server shutdown and a reload of scripts. This allows the snippets to cleanup what they've initialized and prepare for a shutdown. The received arguments are the specified payload, if any and a boolean value specifying if this is a shutdown or just a reload.

Here is a basic example:
SqCore.BindPreLoad(this, function(payload) {
    print("preload 1");
});
SqCore.BindPreLoadEx(this, function(payload) {
    print("preload " + payload);
}, 500);
SqCore.BindPreLoad(this, function(payload) {
    print("preload 3");
});


SqCore.BindPostLoad(this, function(payload) {
    print("postload 1");
});
SqCore.BindPostLoadEx(this, function(payload) {
    print("postload " + payload);
}, 100.0);
SqCore.BindPostLoad(this, function(payload) {
    print("postload 3");
});


SqCore.BindUnload(this, function(payload, shutdown) {
    print("unload 1 " + shutdown);
});
SqCore.BindUnload(this, function(payload, shutdown) {
    print("unload 2 " + shutdown);
});
SqCore.BindUnloadEx(this, function(payload, shutdown) {
    print("unload " + payload + " " + shutdown);
}, "last");


function onScriptLoaded()
{
    print("Scripts loaded");
}
 
SqCore.Bind(SqEvent.ScriptLoaded, this, onScriptLoaded);

function onScriptReload(header, payload)
{
    print("Requesting to reload scripts");
}
 
SqCore.Bind(SqEvent.ScriptReload, this, onScriptReload);

Output:
[DBG] Completed execution of stage (0) scripts
[USR] preload 1
[USR] preload 500
[USR] preload 3
[USR] Scripts loaded
[USR] postload 1
[USR] postload 100
[USR] postload 3
[SCS] The Squirrel plug-in was loaded successfully
[USR] unload 1 true
[USR] unload 2 true
[USR] unload last true
[DBG] Signaling outside plug-ins to release their resources

References to the bound functions are only kept until that particular stage is reached and cleared after. You cannot bind any more functions to a particular stage once it was reached.



Binary Status: available
.

.

Recently I've implemented a built-in system for creating custom events. The implementation is based on the signals and slots design. Normally, this system can be found in high level GUI libraries because it makes it easy to deal to events from the GUI widgets. However, it can be quite useful in other situation as well.

The purpose of this implementation is to help improve the isolation of shared snippets and/or sub components of a script. And at the same time improve interaction between them. Without any of them having to know about each other or having to touch their code. So how does this work?



Well, the whole thing starts with you creating a Signal.
// Named signal which can be accessed from anywhere by name
SqCreateSignal("MySignal");

// Anonymous signal, must have access to the instance to use it
MySignal = SqCreateSignal();

As you can see I can make my signal globally available by giving it a name. To access a named signal anywhere within the code I can search it by name with the `SqSignal` function which returns the instance or throws an error if it doesn't exist:
SqSignal("MySignal").Something();
The signal can also be anonymous, which means that you can use it as a member of a class and therefore act as a localized event:
class Test
{
    onMySignal = null

    function constructor()
    {
        onMySignal = SqCreateSignal();
    }
}

Now when I create instances of `Test`, each signal will be unique to each instance of that class.

To remove a named signal you ca use the `SqRemoveSignal` function:
SqRemoveSignal("MySignal");
To remove an anonymous signal you simply release all references to it and cleans itself up:
MySignal = SqCreateSignal();
//...
MySignal = null; // Cleans itself up if this was the last reference



Alright, so I've created my signal (synonymous to creating your own event). But how do I tell it which functions to call? Well, you connect them to the signal. Simply using the `Connect` method from the signal.

We can do it for the named signals:
function A()
{
    print("A");
}

SqSignal("MySignal").Connect(this, A);

That anonymous signal we made:
function B()
{
    print("B");
}

MySignal.Connect(this, B);

And also that signal from the class we made, after we make some instances:
class Test
{
    onMySignal = null

    function constructor()
    {
        onMySignal = SqCreateSignal();
    }
}

local test1 = Test();
local test2 = Test();

function C()
{
    print("C");
}

test1.onMySignal.Connect(this, C);
test2.onMySignal.Connect(this, C);

As you can see, you must specify an environment in which the function will be called. Can be your current environment by using `this`, can be a class instance, a class declaration, and basically anything that can be used as a table which means it can be a table as well. If you want the default environment, then specify `null`. And it'll default to the root table.

NOTE: You can connect as many functions as you want to a single Signal. Once connected to a signal, a function cannot be connected again with the same environment. Meaning all connected functions are unique. You can connect a function multiple times but it must have a different environment. Same goes with the environments, you can connect them multiple times, but with different functions.

INFO: Once connected to the signal, a function is called a slot. A slot is actually a unique combination between a function and an environment. That combination will always point to the same slot. A slot that will receive signals. And that's how I'll refer to them from now on.

Let me put it in a simple example which you can relate to. You know how table's work, right? You have a key/index which points to a value.

Well, let's say that you have some strings "X", "Y", "Z". And let's imagine they're environments. And now let's have some other strings "A", "B", "C". And let's imagine they're functions. You got that so far?

local x = "X", y = "Y", z = "Z";

local a = "A", b = "B", c = "C";

Alright, now let's combine environment "Y" with function "C". This will give us a string "YC". And let's think of that as our slot and try to set a value to it into a table.

local signal = {};

signal[y+c] = 32;

Now, whenever I use that combination. It'll always refer to the same element in the table.

print( signal[y+c] ); // Output: 32

signal[y+c] = 88;

print( signal[y+c] ); // Output: 88

That combination between "Y" and "C" will always yield the same value. And that's why a slot will always be unique and cannot exist multiple times in the same signal.

As soon as I combine the function or environment with something else. They'll become and entirely different slot. I hope this explains why the combination between a function and an environment is called a slot and will always be unique.

Internally, this uses the address in memory (which will always be unique) where the function and environment is stored. That's why they'll always be unique.



Alright, so I've told the signal which functions to call. But how do I tell the signal which functions to not call anymore? Same as connecting them. Except you use the `Disconnect` method.

We can do it for the named signals:
function A()
{
    print("A");
}

SqSignal("MySignal").Disconnect(this, A);

That anonymous signal we made:
function B()
{
    print("B");
}

MySignal.Disconnect(this, B);

And also that signal from the class we made, after we make some instances:
class Test
{
    onMySignal = null

    function constructor()
    {
        onMySignal = SqCreateSignal();
    }
}

local test1 = Test();
local test2 = Test();

function C()
{
    print("C");
}

test1.onMySignal.Disconnect(this, C);
test2.onMySignal.Disconnect(this, C);

NOTE: You must specify the exact environment that you've used to connect it. Remember, all connected functions become unique in the signal and the environment contributes to that uniqueness.

To disconnect a all functions that use a particular environment we can use the `EliminateThis` method:
function A()
{
    print(" A ");
}

function B()
{
    print(" B ");
}

function C()
{
    print(" C ");
}

SqSignal("MySignal").Connect(this, A);
SqSignal("MySignal").Connect(this, B);
SqSignal("MySignal").Connect(this, C);


SqSignal("MySignal").EliminateThis(this);

Now all three functions were disconnected because they all used the same environment.

To disconnect all occurrences of a function regardless of the environment we can use the `EliminateFunc` method:
class Test
{
    function A()
    {
        print("A");
    }
}

local test1 = Test();
local test2 = Test();
local test3 = Test();

SqSignal("MySignal").Connect(test1, Test.A);
SqSignal("MySignal").Connect(test2, Test.A);
SqSignal("MySignal").Connect(test3, Test.A);


SqSignal("MySignal").EliminateFunc(Test.A);

Now all occurrences of the `A` functions were removed. It didn't matter they all used a different instance as the environment.

To remove all slots connected to a signal you can use the `Clear` method.



To see how many occurrences of an environment exist in a slot regardless of the function you can use the `CountThis` method. Has the same syntax as the `EliminateThis` method except it only returns the number of occurrences and does not remove anything.

To see how many occurrences of a function exist in a slot regardless of the environment you can use the `CountFunc` method. Has the same syntax as the `EliminateFunc` method except it only returns the number of occurrences and does not remove anything.

To find out how many slots were connected in total to a signal you can use the `Slots` property:
function A()
{
    print("A");
}

function B()
{
    print("B");
}

function C()
{
    print("C");
}

SqSignal("MySignal").Connect(this, A);
SqSignal("MySignal").Connect(this, B);
SqSignal("MySignal").Connect(this, C);

print(SqSignal("MySignal").Slots); // Output: 3



Alright, so I can connect them, I can disconnect them, I can count them. But, how do I call them? Well there are several ways of doing it.

The most simple way is the `Emit` method. You simply call the `Emit` with the parameters that you want to send to the slots:
function A(a, b)
{
    print(a + " A " + b);
}

function B(a, b)
{
    print(a + " B " + b);
}

function C(a, b)
{
    print(a + " C " + b);
}

SqSignal("MySignal").Connect(this, A);
SqSignal("MySignal").Connect(this, B);
SqSignal("MySignal").Connect(this, C);

SqSignal("MySignal").Emit("letter", "is nice");

This should output:
letter C is nice
letter B is nice
letter A is nice

There's an alias for the `Emit` method which is called `Broadcast`. If that makes more sense in the code. Meaning you broadcast a signal. Pretty much the same thing.

But what if the slots return some values and you want to do something with them? Well, for that situation we have the `Query` method. Basically that means you want to query/interrogate the slots and they must reply with something:

function A(a, b)
{
    return a + b;
}

function B(a, b)
{
    return a * b;
}

function C(a, b)
{
    return a - b;
}

function D(a, b)
{
    return a / b.tofloat();
}

SqSignal("MySignal").Connect(this, A);
SqSignal("MySignal").Connect(this, B);
SqSignal("MySignal").Connect(this, C);
SqSignal("MySignal").Connect(this, D);

SqSignal("MySignal").Query(this, function(result) {
    print(result);
}, 3, 7);

We specified a function which will receive the returned value of each slot and an environment in which to run that function. Then we specified the parameters we want to forward like we did with the `Emit` function.

The output should be:
0.428571
-4
21
10

The next way of invoking the slots allows slots to prevent other slots from receiving the signal. This is achieved through the `Consume` method. Basically, allows one slot to consume the event while the remaining ones are ignored:
function A(a, b)
{
    if (a != b)
    {
        print("A consumed the event");
        return true;
    }
    else print("A ignored the event");
}

function B(a, b)
{
    if (a == b)
    {
        print("B consumed the event");
        return true;
    }
    else print("B ignored the event");
}

function C(a, b)
{
    if (a < b)
    {
        print("C consumed the event");
        return true;
    }
    else print("C ignored the event");
}

function D(a, b)
{
    if (a > b)
    {
        print("D consumed the event");
        return true;
    }
    else print("D ignored the event");
}

SqSignal("MySignal").Connect(this, A);
SqSignal("MySignal").Connect(this, B);
SqSignal("MySignal").Connect(this, C);
SqSignal("MySignal").Connect(this, D);

if (SqSignal("MySignal").Consume(5, 5))
{
    print("The signal was consumed");
}
else
{
    print("The signal was not consumed");
}

This works the same as the `Emit` method. Except the return value from the slots is no longer ignored. This time, the first slot that returns a value which can be evaluated to true (not necessarily a boolean) is considered to have consumed the signal and therefore ignore the others.

If the event was consumed, the `Consume` method returns true otherwise it returns `false`. So you know whether your signal was consumed or not.

The output should be:
D ignored the event
C ignored the event
B consumed the event
The signal was consumed



All slots are called in the reverse order in which you add them. Meaning, the last connected slot is called first.

Accompanying the `Connect` method there is the `Head` and `Tail` methods. They work very similarly to the `Connect` method. With a few differences.

The `Head` method tries to connect a slot to the front of the list (same as `Connect`). If the slot does not exist, then it will be created at the top of the list. If the slot already exists, then it will be moved to the top of the list. Something which the `Connect` method does not do. When the slot is at the top of the list, it will be the first to receive the signal.

The `Tail` method has the same behavior as the `Head` method. But like the name implies, it creates or moves the slot to the back of the list. Meaning it'll be the last one to receive the signal.

Here's an example:
SqCreateSignal("MySignal");

function A(a, b)
{
    print(a + " A " + b);
}

function B(a, b)
{
    print(a + " B " + b);
}

function C(a, b)
{
    print(a + " C " + b);
}

function D(a, b)
{
    print(a + " D " + b);
}

SqSignal("MySignal").Connect(this, A);
SqSignal("MySignal").Connect(this, B);
SqSignal("MySignal").Connect(this, C);
SqSignal("MySignal").Connect(this, D);

// Move B to the front/hed
SqSignal("MySignal").Head(this, B);
// Move C to the back/tail
SqSignal("MySignal").Tail(this, C);

SqSignal("MySignal").Emit("letter", "is nice");

In this example, even though `B` and `C`are in the middle. We moved `B` to the front and `C` to the back.

The output should be:
letter B is nice
letter D is nice
letter A is nice
letter C is nice

The order in which the signals are received can be important sometimes. Which is why these methods can come in handy.



Anyway, this is a very efficient and very flexible way of introducing custom events in your scripts without having to deal with managing them or worrying about performance. The implementation is still fresh but in my tests it came out to be quite stable. There are still some things that I'd like to implement but so far it does a good job at what it's supposed to do.



Binary Status: available
.

.

Finally finished the module for the modern Maxmind database files. This can be used to obtain information about IP addresses from the modern database format from Maxmind. This is not the legacy file format but their new one.

This should make improve banning mechanisms by providing a more accurate location of where the player is located. Such that if multiple offenders are connecting from a similar location, you can probably ban that entire region.

NOTE: For the purpose of this demonstration, I'll use the IP address from XE European server.



Everything starts with opening a database:
local mmdb = SqMMDB.Database("GeoLite2-City.mmdb");
Now that you have the database open, you can search for information about a certain IP address:
local result = mmdb.LookupString("51.255.193.118");
This returns a LookupResult instance. To check if something was found, we can use the `FoundEntry` property.
if (!result.FoundEntry)
{
    throw "no information for this address";
}

The lookup results only contains the location of whatever results are stored in it. To obtain the actual data, you can either retrieve the data for all entries at once with the `EntryDataList`property or a specific entry that interest you with the `GetValue` function.

The data from the database has a JSON(ish) style. The lookup result contains those values resulted from the lookup encoded as a list of entries. You could loop these entries manually if you want. But I wouldn't recommend such thing.

To have an idea of what was returned in the lookup result let's dump it to a file. First we obtain the data for all the entries then dump the entire list to a file:
result.EntryDataList.DumpTo("lookup.txt");
In this particular example with the database that I'm using, the file contains:
{
  "continent":
    {
      "code":
        "EU" <utf8_string>
      "geoname_id":
        6255148 <uint32>
      "names":
        {
          "de":
            "Europa" <utf8_string>
          "en":
            "Europe" <utf8_string>
          "es":
            "Europa" <utf8_string>
          "fr":
            "Europe" <utf8_string>
          "ja":
            "ヨーロッパ" <utf8_string>
          "pt-BR":
            "Europa" <utf8_string>
          "ru":
            "Европа" <utf8_string>
          "zh-CN":
            "欧洲" <utf8_string>
        }
    }
  "country":
    {
      "geoname_id":
        3017382 <uint32>
      "iso_code":
        "FR" <utf8_string>
      "names":
        {
          "de":
            "Frankreich" <utf8_string>
          "en":
            "France" <utf8_string>
          "es":
            "Francia" <utf8_string>
          "fr":
            "France" <utf8_string>
          "ja":
            "フランス共和国" <utf8_string>
          "pt-BR":
            "França" <utf8_string>
          "ru":
            "Франция" <utf8_string>
          "zh-CN":
            "法国" <utf8_string>
        }
    }
  "location":
    {
      "accuracy_radius":
        500 <uint16>
      "latitude":
        48.858200 <double>
      "longitude":
        2.338700 <double>
      "time_zone":
        "Europe/Paris" <utf8_string>
    }
  "registered_country":
    {
      "geoname_id":
        3017382 <uint32>
      "iso_code":
        "FR" <utf8_string>
      "names":
        {
          "de":
            "Frankreich" <utf8_string>
          "en":
            "France" <utf8_string>
          "es":
            "Francia" <utf8_string>
          "fr":
            "France" <utf8_string>
          "ja":
            "フランス共和国" <utf8_string>
          "pt-BR":
            "França" <utf8_string>
          "ru":
            "Франция" <utf8_string>
          "zh-CN":
            "法国" <utf8_string>
        }
    }
}

As you can see, it looks a lot like JSON. And that's why I said looping through all those values and processing them manually would be a waste of time.

For example, this is a table named "hello":
"hello":
{
}

These are some elements inside that "hello" table:
"hello":
{
    "world":
        "yum yum" <utf8_string>
    "there":
        3243 <int32>
}

If you notice the type of the value is in those arrow brackets <...> on the right of the value.

Let's see how it looks to have another table inside our "hello" table:
"hello":
{
    "world":
        "yum yum" <utf8_string>
    "there":
        3243 <int32>
    "what":
    {
        "yes":
            221 <int32>
    }
}

This should feel familiar. Now let's have an array. Arrays use the square brackets [].
"hello":
{
    "world":
        "yum yum" <utf8_string>
    "there":
        3243 <int32>
    "what":
    {
        "yes":
            221 <int32>
    }
    "why":
    [
        {
            "foo":
                212 <int32>
        }
        {
            "bar":
                "what?" <utf8_string>
        }
        {
            "baz":
                2.23 <float>
        }
    ]
}

You get the idea now? Because understanding this is crucial to you using this library. For example, to select a value, you form a path. Let's say that we want to access the "there" element. The path to that element would be "hello/there" because it's in the "hello" table.

Let's say that we want to access the "bar" value from the anonymous table in the second element of the "why" array. The path would be "hello/why/1/bar". Array indexing starts from 0, so we use 1 for the second element. And since the the "bar" value is in an anonymous table, we don't have to specify anything to get to it.

Anyway, if you've worked with XML/HTML and XPath then this way of making paths should feel familiar. Except here is more like a JSON style path.

So how do we access a particular value from there? Well, like I mentioned, we use the `GetValue` function. Let's take the continent for example:
  "continent":
    {
      "code":
        "EU" <utf8_string>
      "geoname_id":
        6255148 <uint32>
      "names":
        {
          "de":
            "Europa" <utf8_string>
          "en":
            "Europe" <utf8_string>
          "es":
            "Europa" <utf8_string>
          "fr":
            "Europe" <utf8_string>
          "ja":
            "ヨーロッパ" <utf8_string>
          "pt-BR":
            "Europa" <utf8_string>
          "ru":
            "Европа" <utf8_string>
          "zh-CN":
            "欧洲" <utf8_string>
        }
    }

The `GetValue` expects you to specify a series of strings (can be any value because internally they're converted to a string anyway) which will form the path to what you need.

So let's say we want the continent code. Since the continent code is in the continent table, we can simply use:
local continent_code = result.GetValue("continent", "code");
The `GetValue` method returns an instance of an EntryData type. Which contains the actual data at the entry which you selected.

Too see if there's actually any data in there you can use the `HasData` property which is true in case it does have some data which you can retrieve.

To actually retrieve the data in a format which you can work with you can use the `Bool`, `Integer`, `Float`, `Long`, `String` and `Bytes` properties. Their names should be self explanatory. If you paid close attention, the type of data was in those arrow brackets < ... >. So we know in this example, the continent code was a string. We could also use the `Type` property to find out the type. But that's for later.

So now let's print the value from the obtained continent_code EntryData instance:
print(continent_code.String);
If a certain entry that you requested for does not exist, you'll receive an error. So it's best to wrap these requests in a try/catch block:
try
{
    print(result.GetValue("continent", "code").String);
}
catch (...)
{
    print("there's no continent code in this lookup result");
}

Using this brief introduction, let's print some information about that IP.

City name:
try
{
    print("City name: " + result.GetValue("city", "names", "en").String);
}
catch (...)
{
    print("City name: unknown");
}

There was none for this IP, so it'll throw an exception which we catch and that tells us we did not find a city. Also note that city names can be in multiple languages. That 'en' tells it we want the English one. Again, look at what was dumped to get some idea of what you can retrieve.

Continent code:
try
{
    print("Continent code: " + result.GetValue("continent", "code").String);
}
catch (...)
{
    print("Continent code: unknown");
}

Continent name:
try
{
    print("Continent name: " + result.GetValue("continent", "names", "en").String);
}
catch (...)
{
    print("Continent name: unknown");
}

Country code:
try
{
    print("Country code: " + result.GetValue("country", "iso_code").String);
}
catch (...)
{
    print("Country code: unknown");
}

Country name:
try
{
    print("Country name: " + result.GetValue("country", "names", "en").String);
}
catch (...)
{
    print("Country name: unknown");
}

The location on the globe:
try
{
    print("Longitude: " + result.GetValue("location", "longitude").Float);
}
catch (...)
{
    print("Longitude: unknown");
}

try
{
    print("Latitude: " + result.GetValue("location", "latitude").Float);
}
catch (...)
{
    print("Latitude: unknown");
}

The accuracy of the location:
try
{
    print("Location accuracy: " + result.GetValue("location", "accuracy_radius").Integer);
}
catch (...)
{
    print("Location accuracy: unknown");
}

The state code:
try
{
    print("State code: " + result.GetValue("subdivisions", 0, "iso_code").String);
}
catch (...)
{
    print("State code: unknown");
}

That 0 there means that "subdivisions" is an array and we selected the first element (which is a table). And then the "iso_code" element from that table.

The state name:
try
{
    print("State name: " + result.GetValue("subdivisions", 0, "names", "en").String);
}
catch (...)
{
    print("State name: unknown");
}

The postal code:
try
{
    print("Postal code: " + result.GetValue("postal", "code").String);
}
catch (...)
{
    print("Postal code: unknown");
}

And a bunch of information which can be found for a certain IP address.



If you reached this point you probably have a WTF expression on your face. I did too. Imagine my pain having to go through the low level stuff and putting the pieces together in a way that it shouldn't be too hard for you to use. Yes it takes a while to get used to it. But eventually you'll get it.



Binary Status: available
.

.

Dedicated entity tasks. What does that mean? Well, have you ever scripted something then created a timer to... dunno, do something with a player/vehicle/object  etc. at certain interval? Well, you probably remember that if the player/vehicle/object etc. gets disconnected/destroyed before your timer finishes, then your timer will end up working with an invalid player/vehicle/object etc.

So that means you have to do a bunch of housekeeping to make sure you don't reach that kind of situation. Which can get annoying sometimes.

Well, to get around issues like that I've implemented a way to create timers that can be dedicated only to a particular entity. And the nice thing about them is that they're cleaned up automatically when the entity that they're associated with get's destroyed as well.



So how do I use them? Well, you basically have 3 functions to worry about `MakeTask`, `DropTask` and `DoesTask`.

The `MakeTask` method takes a function to call, an interval to wait between calls and the number of times to call (0 for infinite). After these parameters you can specify up to 8 values that will be forwarded to the function. Only the function is mandatory, the rest are optional.

The `DropTask` and `DoesTask` have the same signature except different behaviors. They take a function to search and optionally and interval  and the number of iterations if you want to be more explicit about which task you're referring. The `DropTask` stops the task if found, regardless of how much times it has left to be called. It basically removes it. And the `DoesTask` returns true if indeed the entity was assigned to do that task.



How about some examples? Well, let's start with the most basic one, shall we?
function onPlayerCreated(player, reason, payload)
{
    player.MakeTask(function(did) {
        printf("%s did %s", this.Entity.Name, did);
    }, 1000, 6, "nothing");
}

SqCore.Bind(SqEvent.PlayerCreated, this, onPlayerCreated);

When I connected to the server I had the following over a period of 6 seconds:
SLC did nothing
SLC did nothing
SLC did nothing
SLC did nothing
SLC did nothing
SLC did nothing

The task instance will pass itself as the environment. This allows you to access and/or modify it's properties from withing the function itself. But also to terminate the task in mid execution.

In the previous example you can see I'm accessing the instance of the entity to which the task belongs with the `.Entity` property (there's also an alias for it called `.Inst`).

There are a few more properties:
  • `.Func` Which is the function itself. If maybe you want to re-bind this function to something else.
  • `.Data` So you can store your own values in it and be able to retrieve them back in the next call (like a persistent storage).
  • `.Interval` So you can access/modify the task interval.
  • `.Iterations` So you can access/modify the number of task iterations.
  • `.Arguments` So you can retrieve the number of arguments that the task will forward to the function.

And a few functions:
  • `Terminate()` Calling this function will release everything from the task. Accessing anything from the task after this point will yield null or default values. Which means that you should probably return after calling this as well.
  • `GetArgument(index)` Allows you to retrieve the value of an argument that will be forwarded to you. Rather useless right now since they're already forwarded to you as parameters. But I might include a `set` method in the future so you can change them as well.

Here's an example which uses the internal persistent storage to store a counter and terminate before finishing all the iterations:
function onPlayerCreated(player, reason, payload)
{
    player.MakeTask(function(did) {
        if (this.Data == null)
        {
            this.Data = 1;
        }
        else if (++this.Data > 3)
        {
            this.Terminate();
            return; // the task has been cleaned of all values
        }
        printf("%s did %s", this.Entity.Name, did);
    }, 1000, 6, "nothing");
}

SqCore.Bind(SqEvent.PlayerCreated, this, onPlayerCreated);

As you can see we had to return because `Terminate()` will release all values from the task. Which means when we'd try to access the entity in the next line we'd get an error since it'll return `null` and null doesn't have a `.Name` property.

That should output:
SLC did nothing
SLC did nothing
SLC did nothing

If I still want to finish the execution and then terminate I could take advantage of the `.Iterations` property and set that to `1` which would cause the task to clean itself once it returns:
function onPlayerCreated(player, reason, payload)
{
    player.MakeTask(function(did) {
        if (this.Data == null)
        {
            this.Data = 1;
        }
        else if (++this.Data > 3)
        {
            this.Iterations = 1;
        }
        printf("%s did %s", this.Entity.Name, did);
    }, 1000, 6, "nothing");
}

SqCore.Bind(SqEvent.PlayerCreated, this, onPlayerCreated);

That should output:
SLC did nothing
SLC did nothing
SLC did nothing
SLC did nothing

As you can see the tasks can be quite flexible and very adaptive.



So what fun things can we do with this? Hmm, let's see. How about if we teleport a player to the center of the map after 5 seconds:
player.MakeTask(function(pos) {
    this.Entity.Position = pos;
}, 5000, 1, Vector3(0, 0, 0));

Let's make a vehicle with a time-bomb which the player must drive away from the city :D
vehicle.MakeTask(function() {
    this.Entity.Explode();
}, 180000, 1);

That should make for some interesting mod.

How about if we re-spawn a vehicle after a certain time:
vehicle.MakeTask(function() {
    this.Entity.Respawn();
}, 60000, 1);

I think you noticed something so far. You can't stop a task from within itself. And with anonymous functions that problem becomes even more apparent. Well, This is something that I'm still thinking about. So far it's just a prototype. A functioning and fully implemented prototype but still a prototype. This limitation was removed and the tutorial was updated to reflect the changes.


Binary Status: available
.

.

#6
The previous event system was replaced by a signals and slots implementation to allow the scripter to bind more than one listener/callback to a certain event. So what's changed in the syntax compared to the previous ones?

Well, If previously you would've listened to the `player message` event like this:
player.Bind(SqEvent.PlayerMessage, this, function(mesage) {
    print(message);
});

Now you can do it like this:
player.On.Message.Connect(this, function(mesage) {
    print(message);
});

The `this` parameter is now optional. If you don't specify it then it defaults to the root table.

And this is not just some syntax sugar. This implementation allows you to connect multiple functions to the same event. For example:
SqCore.On().PlayerCreated.Connect(function(player, header, payload) {

    player.On.Message.Connect(player, function(message) {
        printf("%s said '%s'", this.Name, message);
    });

    player.On.Message.Connect(function(message) {
        printf("are you sure he said said '%s' ?", message);
    });

    player.On.Message.Connect(function(message) {
        printf("yes, he said '%s'", message);
    });

});

Outputting something like this https://s21.postimg.org/jwyr3cp53/Untitled.png
[USR] SLC said 'ftw biatch'
[USR] are you sure he said said 'ftw biatch' ?
[USR] yes, he said 'ftw biatch'

When you want to listen to an event globally, you use it like this:
SqCore.On().PlayerMessage.Connect(function(player, message) {
    printf("%s said '%s'", this.Name, message);
});

Please note the parentheses after `On`. That's because the events are returned by a function. When you're using the events directly on the entity itself. You don't need them as they act like a property.

In fact, that's just a table of `SqSignal` instances. That means in order to get a list of them you can use a for loop.
foreach (name, signal in SqCore.On()) {
    print(name);
}

Which outputs a list of all the signals/events that can be accessed. And the same applies to the entities:
foreach (name, signal in someplayer.On) {
    print(name);
}

And because that's just a table. that means you can insert your own events/signals and make them look like they're a part of the module itself.

For example, to create an event/signal for a certain player:
player.On.MyEventName <- SqCreateSignal();
And now you can connect to it like any normal event. Except you're the one that has to emit that event/signal manually (obviously):
player.On.MyEventName.Emit(parameters, go, here);
And the same works with global signals:
SqCore.On().MyEventName <- SqCreateSignal();
This works perfectly fine because it's just a table. However, you should be careful not to remove or modify the built in events from that table. Because there's nothing preventing you from doing that. So you'll find your self unable to receive that particular event. Unless you back it up to some other place.



The routines implementation was also changed. Previously you would create routines like:
SqRoutine.Create(...);
Now you can do it just with:
SqRoutine(...);
There are also some other changes and improvements but I won't get into them right now.



Binary Status: available
.

.

Several builds have been added lately which introduced some minor features and behavior changes.

First one is the ability to define custom names for about 100 custom vehicle IDs. Basically, the ID range of 6400 (including 6400) up to 6499 (including 6499). So now when you call SqServer.SetAutomobileName(id, name, ...); and the ID is a number between 6400 and 6499. It will store that string and later if you call SqServer.GetAutomobileName(id) with the same id, you should get that name back. Previous functionality still works where you can retrieve or modify names of standard vehicles with IDs between 130 and 235. And if the given ID is not in any of those ranges. The function will throw an error with the cause of the issue.

A simple Whirlpool implementation was added to the hashing algorithms. Only SqHash.GetWhirlpool() function was implemented. Incremental hashing like the other algorithms is currently not implemented. Probably later if necessary.

Two simple base64 encoding/decoding functions have been added that work with strings (no raw memory such as blobs or buffers). Which can be found in SqHash.EncodeBase64(text, ...) and SqHash.DecodeBase64(text, ...). To be removed from the SqHash namespace at some point because they're not technically meant for hashing. But I was too tired to realize that and by the time I did, I had already compiled and uploaded everything. So I left it as it was for the moment.

These updates were not planned but upon request I had to implement them so thought I should mention about them in case someone downloads the latest binaries and some things don't behave like they used to. Especially the vehicle ID feature since it changes plugin behavior and people should know about it.

Binary Status: available
.

.

Please note that this feature was not extensively tested. Be sure to do a test run in your development environment before using it in production.

The plugin finally gets an implementation of the much requested area system. Before I get to the examples, let's talk implementation details so that you know what you're dealing with and what are the limitations.

First thing first. Each defined area (also known as a polygon) will have a bounding box that roughly estimates the size of the area. Example:


When testing if a point is inside this area. The implementation tests whether the point is inside the bounding box first. And if it is, then it proceeds to testing each point. This is cheaper and can eliminate useless tests.

Now let's talk partitioning. The implementation divides the world into a grid of 256x256 cells. Where the maximum supported coordinates are from -2048 to +2048 in the X axis and -2048 to +2048 in the Y axis. That means a 16x16 grid. Plus two extra cells that will be clamped back to a covered cell. Example:


The green cells are what the implementation can cover. It covers a total of 4096x4096 with each side from the center being 2048 in length. According to this, the world does go a little further to -2350 on one side. But the blue cells (keep reading) can be used to work around this by making your area touch a little of the green cells.

The blue cells are like rounding error an always fallback to the nearest green cell. So if you test for a point in any of those blue cells, it'll actually test for areas found in the nearest green cell. It's a 1024 extension of the 4096x4096 grid making the grid 5120x5120 with each side from the center being 2560 in length.

The red cells (which extend to infinity in each direction) are considered out of range and ignored completely. Basically, if you test for a point in those cells it won't actually be tested.

Locating in which cell is a point located should be fairly fast because it can be deduced from the point itself rather than testing against the bounding box of each cell.

Let's take as an example the area we used as an example previously. If the area bounding box is completely within a grid cell. Then any tested point must be in that that grid cell in order to even be tested against the bounding box of that area and then the area itself if both those tests pass. Example:


In the example above. The red point is found to be in the same cell as the area. So it first gets tested against the bounding box of the area. However since it's outside, it fails that test so it never even reaches the stage where it has to test against the area points.

The blue point is found in a different cell. A cell in which there are no areas. As such, no tests are performed against any areas and pretty much finishes as quickly as possible.

But what about areas that intersect with multiple cells in the grid? All the cells that intersect with a the bounding box of an area will manage that area and test against it. Example:


This time, both points are within a cell that intersects with the bounding box of the area. As such, both are tested against the bounding box of the area. But since neither of them are within the bounding box of the area, no tests are made against the area points (again).

That's as far as the implementation goes with optimizations. So let's talk examples.



Area testing is disabled by default for any created player or vehicle (only those are supported for now). They can be enabled individually after creating them with the `CollideAreas` property:

player.CollideAreas = true;
vehicle.CollideAreas = true;

To enable this feature by default for any newly created player or vehicle. That is, to enable the feature globally. You must specifically do it so with:

SqCore.SetAreasEnabled(true);
if (SqCore.AreasEnabled()) print("area collision enabled for all players and vehicles!!!");

False always disables things so I don't think I should've mention this. There is also a function available for players and vehicle that can be used to enable and disable area collisions:

player.AreasCollide(false);
vehicle.AreasCollide(false);

The only difference between the function and the property is that, when disabled, the function does one last check to notify the script if the player or vehicle exited any areas since the last check. Whereas the the property simply disables the option and clears everything without letting anyone know if the player or vehicle exited any areas that was previously in.

To create an area you can do it very simply by making an area instance:

local area = SqArea("some custom name"); // name is optional. used as an example
You can assign both names and IDs to areas to quickly identify them. Similar to how entities have. For example, they can help with databases if you save areas in a database to use the same ID as in the database.

area.Name = "some name";
print(area.Name);
area.ID = 54;
print(area.ID);

There are various other constructors that I won't go into detail here. For example, there are constructors that allows you to reserve space for points in advance if you know how many you have:

local area = SqArea(47, "some custom name");
In this case, I knew this area will have 47 points so I allocated the memory upfront to avoid doing repeatedly as I push things.

But just making an area is useless. So let's begin adding some points to the area to define it:

// If you have a Vector2 then use the regular Add
area.Add(Vector2(-661.522, 757.210));
area.Add(Vector2(-662.681, 743.307));
// If you have individual values. Use the expanded version
area.AddEx(-679.657, 748.757);
area.AddEx(-671.674, 763.510);

An area needs at least 3 points in order to be considered valid. I hope I don't have to explain why. So now that our area has some points. We must tell the area manager that our area can be added to the grid and tested against players and vehicles or whatever other points I chose to test globally. So we must tell the area that it can be managed:

if (area.Manage()) print("area cannot be modified now");
else print("area was not managed by any cell. probably out of range?");

Once the area is managed. It can no longer be modified. You must un-manage it first to be able to modify it by adding new points. The name and ID are not affected by this.

if (area.Unmanage()) print("area can now be modified");
else print("somehow the area is still managed");

To see if the area is managed by any cell in the grid you can see whether it is locked:

if (area.Locked) print("area managed by at least one cell");
Now that you added your areas. You can begin to connect yourself to the signals emitted by players and vehicles. Globally:

SqCore.On().PlayerEnterArea.Connect(this, function(player, area) {
    printf("%d entered area %s", player.ID, area.Name);
});

SqCore.On().PlayerLeaveArea.Connect(this, function(player, area) {
    printf("%d left area %s", player.ID, area.Name);
});

SqCore.On().VehicleEnterArea.Connect(this, function(vehicle, area) {
    printf("%d entered area %s", vehicle.ID, area.Name);
});

SqCore.On().VehicleLeaveArea.Connect(this, function(vehicle, area) {
    printf("%d left area %s", vehicle.ID, area.Name);
});

Or locally:
player.On.EnterArea.Connect(this, function(area) {
    printf("entered area %s", area.Name);
});

player.On.LeaveArea.Connect(this, function(area) {
    printf("left area %s", area.Name);
});

vehicle.On.EnterArea.Connect(this, function(area) {
    printf("entered area %s", area.Name);
});

vehicle.On.LeaveArea.Connect(this, function(area) {
    printf("left area %s", area.Name);
});

And you should start receiving calls from areas that collide with players and vehicles that are allowed to collide.

For remaining functions and properties that are available you probably must go through the source or ask about it. This was only an overview.



Please note that the accuracy of the implementation depends on how fast the server notifies the plugin of position changes. The plugin does not implement some continuous collision testing based on the elapsed time or segmenting the distance to have a fixed stepping rate. So please pay attention to that. Because fast moving players or vehicles might not trigger some areas, especially the small ones.

Here is a complete example with an area in the intersection of the default spawning point:

SqCore.SetAreasEnabled(true);

local area = SqArea("spawn intersection");

area.AddEx(-661.522, 757.210);
area.AddEx(-662.681, 743.307);
area.AddEx(-679.657, 748.757);
area.AddEx(-671.674, 763.510);

area.Manage();

SqCore.On().PlayerEnterArea.Connect(this, function(player, area) {
    printf("%d entered area %s", player.ID, area.Name);
});

SqCore.On().PlayerLeaveArea.Connect(this, function(player, area) {
    printf("%d left area %s", player.ID, area.Name);
});

SqCore.On().VehicleEnterArea.Connect(this, function(vehicle, area) {
    printf("%d entered area %s", vehicle.ID, area.Name);
});

SqCore.On().VehicleLeaveArea.Connect(this, function(vehicle, area) {
    printf("%d left area %s", vehicle.ID, area.Name);
});

SqCore.On().PlayerCommand.Connect(this, function(player, command) {
    SqVehicle.Create(132, player.World, player.Pos, 0, 0, 0);
});

Type any command to spawn a vehicle for testing.



Other than that there are a few other bug fixes and changes:

  • Signals could not be created without a name. Meaning you could not create free signals: local signal = SqCreateSignal();
  • MySQL connections now have the AutoCommit option set to true by default. To avoid confusion with new users.
  • A few other fixes that may or may not affect the behavior of your script. Stay on watch for that.

Binary Status: available
.

.

After constant reports that the SqPlayer.Spec property or SqPlayer.Spectate() function would crash the server when trying to set a null player and thus disable spectating. I've decided to revert back to the initial approach and instead implement a new property and a new function. The property and function in question are SqPlayer.SpecID and SqPlayer.SpectateNone().

The SqPlayer.SpecID property can retrieve or modify the raw spectator identifier. Meaning the raw player ID. Which in theory should allow you to do something like player.SpecID = -1; to disable spectating or the ID of other players to spectate them.

The SqPlayer.SpectateNone() does exactly what player.SpecID = -1; would achieve. In fact it's the same code. Except it makes more sense when you read it and in case the developers decide to change this behavior in the future, the plugin can easily be updated and you don't have to change a thing in your script.

That's why the latter option is the recommended approach.

WARNING: If you previously have used player.Spec = null; or player.Spectate(null); you should replace null with SqPlayer.NullInst() (actually you should use SqPlayer.SpectateNone()) because the property and function no longer accepts null but rather a player instance. This was the intended behavior from the start. I only allowed the use of null because I did not know what caused the crash and it was a temporary workaround until I had figured things out. Using null in this context has no benefits and in fact has more downsides. Which is why I've prohibited this kind of scripting practice.



Optionally, the SQLite library was updated the the latest version (3.20.0). To provide servers with the latest optimizations and bug fixes. So you may consider updating that as well.

Binary Status: available
.

.

Finally finished that spectate bug. It turns out the server uses the same callback to notify when a player is no longer spectated. By giving the target player ID as a negative integer (-1). The fun part was when the plugin treated that ID as a valid one and tried to retrieve the instance of the player at that given index. However, since any contiguous container in C++ is likely to use unsigned integers to represent indexes. When you try to interpret a negative signed integer as unsigned, you usually get a much bigger value than you'd expect. In this case, treating the negative 32 bit signed integer -1 as an unsigned integer, gives you the maximum unsigned integer you can get `4294967295`. Obviously that's not a valid player integer and out of range of any array/vector in the plugin. So that threw an exception. This behavior was made obvious when the exact error message was posted in the bug report.

Knowing that. I've separated the spectating callback into 2 events. The existing one `Spectate` and a new one `Unspectate`. So this should solve the spectating bug that kept causing issues. I was not aware of this behavior so I didn't know what could cause it.

While on this topic. The player method `SpectateNone()` was renamed to `Unspectate()` in order to keep it similar to the event that is likely to trigger.



The functions `SqPlayer.Find()` and `SqPlayer.Exists()`, as well as their global aliases `FindPlayer()` and `PlayerExists()`, have been improved. The `Find()` function returns the found player instance or `SqPlayer.NullInst()` if the player could not be found. While the `Exists()` function returns a boolean value with the appropriate response.

Several other properties and/or functions that used to return Player/Vehicle/Object instances where there was a possibility of that entity not existing. Those functions would've returned null. They have now been updated to return the appropriate SqWhateverEntity.NullInst() instance (<- that's a fake name to denote any type of entity). Off the top of my head, these should be:

  • player.Spec property
  • player.Spectating() function
  • player.Vehicle property
  • player.TouchedVehicle property
  • player.TouchedObject property
  • SqFind.Blip.WithSprID() functions
  • And probably others I forgot about.

So be sure to run a search through your scripts with something like NodePad++ and if you're comparing the results of those properties or functions with null be sure to update to check if it's a valid/active entity. For example:

if (player.Vehicle.Active) print("The player is in a car...");


A feature request from the repository issue tracker was implemented. Which was to make it possible to stop iteration of searched entities with `SqForeach.` and `SqForeachEx.` algorithms. Like:

SqForeach.Player.Active(this, function(player) {
    if (player.ID > 10) {
        return false; // stop looking for the others
    }
    // Returning nothing or something or null that evaluates to true
    // allows the remaining players to be iterated. All equivalent:
    // return;
    // return null;
    // return true;
    // return 33; evaluates to true because (33 > 0)
    // etc.
});

This feature was not tested at all. I simply didn't have the time to deal with it.  But in theory it should work. So if it does, let me know.

Binary Status: available
.