News A server update was released on July 20. Server patch notes and downloads are here. Additionally, a client update was released on the same day. Client patch notes are here.
[CHANGELOG] Noteworthy changes in the plugin.

.

  • Moderator
  • Posts: 1,696
[CHANGELOG] Noteworthy changes in the plugin.
«  »
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.
[CHANGELOG] Re: Noteworthy changes in the plugin.
« Reply #1,  »Last edited
Dynamic dispatching for meta-methods on base types:

Code: [Select]
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:
Code: [Select]
[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
[CHANGELOG] Re: Noteworthy changes in the plugin.
« Reply #2,  »Last edited
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:
Code: [Select]
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:
Code: [Select]
[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
[CHANGELOG] Re: Noteworthy changes in the plugin.
« Reply #3,  »Last edited
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.
Code: [Select]
// 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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
SqRemoveSignal("MySignal");

To remove an anonymous signal you simply release all references to it and cleans itself up:
Code: [Select]
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:
Code: [Select]
function A()
{
    print("A");
}

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

That anonymous signal we made:
Code: [Select]
function B()
{
    print("B");
}

MySignal.Connect(this, B);

And also that signal from the class we made, after we make some instances:
Code: [Select]
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?

Code: [Select]
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.

Code: [Select]
local signal = {};

signal[y+c] = 32;

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

Code: [Select]
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:
Code: [Select]
function A()
{
    print("A");
}

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

That anonymous signal we made:
Code: [Select]
function B()
{
    print("B");
}

MySignal.Disconnect(this, B);

And also that signal from the class we made, after we make some instances:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:

Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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
[CHANGELOG] Re: Noteworthy changes in the plugin.
« Reply #4,  »Last edited
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:
Code: [Select]
local mmdb = SqMMDB.Database("GeoLite2-City.mmdb");

Now that you have the database open, you can search for information about a certain IP address:
Code: [Select]
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.
Code: [Select]
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:
Code: [Select]
result.EntryDataList.DumpTo("lookup.txt");

In this particular example with the database that I'm using, the file contains:
Code: [Select]
{
  "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":
Code: [Select]
"hello":
{
}

These are some elements inside that "hello" table:
Code: [Select]
"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:
Code: [Select]
"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 [].
Code: [Select]
"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:
Code: [Select]
  "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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
try
{
    print("Continent code: " + result.GetValue("continent", "code").String);
}
catch (...)
{
    print("Continent code: unknown");
}

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

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

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

The location on the globe:
Code: [Select]
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:
Code: [Select]
try
{
    print("Location accuracy: " + result.GetValue("location", "accuracy_radius").Integer);
}
catch (...)
{
    print("Location accuracy: unknown");
}

The state code:
Code: [Select]
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:
Code: [Select]
try
{
    print("State name: " + result.GetValue("subdivisions", 0, "names", "en").String);
}
catch (...)
{
    print("State name: unknown");
}

The postal code:
Code: [Select]
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
[CHANGELOG] Re: Noteworthy changes in the plugin.
« Reply #5,  »Last edited
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?
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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
Code: [Select]
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:
Code: [Select]
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
[CHANGELOG] Re: Noteworthy changes in the plugin.
« Reply #6,  »Last edited
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:
Code: [Select]
player.Bind(SqEvent.PlayerMessage, this, function(mesage) {
    print(message);
});

Now you can do it like this:
Code: [Select]
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:
Code: [Select]
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
Code: [Select]
[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:
Code: [Select]
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.
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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):
Code: [Select]
player.On.MyEventName.Emit(parameters, go, here);

And the same works with global signals:
Code: [Select]
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:
Code: [Select]
SqRoutine.Create(...);

Now you can do it just with:
Code: [Select]
SqRoutine(...);

There are also some other changes and improvements but I won't get into them right now.



Binary Status: available
.