S.L.C's Scripting Guides and Advices

Started by ., Mar 04, 2015, 06:36 PM

Previous topic - Next topic

.

I would like to reserve this topic for me to write small posts and ideas that I think would help new script-writers improve their scripts and code in general. Similar to the one I have for the Hybrid Squirrel plugin that I'm making. Except in this thread I'll try to prevent you from doing common mistakes and help you write better looking and more optimized code. But also to explain various key concepts that are missing from the official documentation when needed.

These posts will be very random with things I encounter when I review other people's code or simply when I remember them and have the time to write about them. So don't expect any consistency in these guides/tips. This are just like a collection of quick tips and FAQs for the long journey ahead of you when you begin scripting.


Please do not post anything in this thread. If you have any requests or questions about existing posts you can simply send me a PM. And I will either give you a better explanation in the PM or simply write a post here.



Some of these posts will get changed and updated as needed when a various expression has a better candidate or I forgot something in the first place. So be sure to check them from time to time.


PLEASE NOT THAT SOME OF THESE EXAMPLES LOOK AWFUL. THAT'S BECAUSE THE FORUM SCRIPT REMOVES INDENTATION FROM THE CODE.

THERE'S NOTHING I CAN DO ABOUT THAT. THE SCRIPT ONLY NEEDS TO BE UPDATED. I'VE TESTED THE LATEST GIT VERSION AND WORKED NORMALLY.

I FOUND THAT IF I USE 4 SPACE CHARACTERS INSTEAD OF A TAB CHARACTER THEN THE INDENTATION WORKS AS INTENDED.

If the staff doesn't allow such threads then please lock it.
.

.

#1
USING HARD CODED VALUES DOESN'T MEAN YOU ARE A HARD CORE PROGRAMMER!

Using hard-coded values for configurations has it's benefits in some languages. Mainly because the value is inlined and there's no performance cost. Whereas with variables, it has to first access the variable and get the value from the variable each time, which means extra instructions. You probably saw me using a global table sometimes where I simply keep values that can be changed/tweaked and which are used through out the code several times. Let me illustrate that:

// Global table used to scope base configurations
_CFG <- {
    // Cost of normal heal command
    Heal_Cost = 1000
    // The actual amount of health to set
    Heal_Amount = 100
}

// Number of milliseconds to delay the healing process
_CFG.Heal_Delay <- 2000;
// Number of milliseconds to wait before waking up the coroutines
_CFG.Heal_Speed <- 50;

Changing one of those values would have immediate effect through out the whole script. But there's a catch. If you end up using several of these values in functions called very frequently it would add more stress to the script. Mainly because, every time you want a value from one of those tables a few more instructions are needed unlike the usual hard-coded values. There's a few instructions for getting the table on the stack and a few more to get the requested value from the table. So, it's best that you use them wisely.

Note that you can declare table elements when you declare the table it self or later in the code when you need them as illustrated by the example above.



A way around the performance issue above would be the use of constants or enumerations. Constants are similar to global values and they are evaluated at compile time. Which means that  they can be used anywhere in the code. But the problem with constants and config values is that they end up cluttering the global table/namespace. Therefore constants aren't the solution for config values (at least when there's too many of them).

const integer_value = 59;
const float_value = 1.5;
const string_value = "I'm a constant string";

Similar to the constants and tables combined (actually, very similar) are the enumerations. Unlike the constants these can be scoped. So the global table/namespace is no longer cluttered and you can keep a clean syntax like you would with tables. They work pretty much the same way constants do. Except their values/elements can be scoped like we do in the tables.

enum Stuff {
    integer_value = 117
    float_value = 1.8
    string_value = "I'm another constant string"
}



Let's see how the example with tables can be translated to enumerations:
// Global enumeration used to scope base configurations
enum _CFG {
    // Cost of normal heal command
    Heal_Cost = 1000
    // The actual amount of health to set
    Heal_Amount = 100
    // Number of milliseconds to delay the healing process
    Heal_Delay = 2000
    // Number of milliseconds to wait before waking up the coroutines
    Heal_Speed = 50
}

Enumeration values will be evaluated compile-time. And using them for config values can result in a better performance than tables. But there are some disadvantages which will be discussed bellow.



The problem with with constants and enumerations is that they're... constant. As the name suggest, they are evaluated at compile-time and never changed. Being evaluated at compile time also brings a few other limitations. They cannot be loaded from an external file and cannot contain any expressions like (1 + 3 - 2) where you would get the value of 2 in the configured element.

And I'm pretty sure that you noticed how constants and enumerations can only store integers, floats or string literals. So you should keep that in mind when using them.

Like many other things out there, constants and enumerations have their own downsides that (sometimes) match even the advantages. But being constants can also be of great use when you need to be sure that a value cannot be changed when your code doesn't expect that. So that's something to also keep in mind.

Tables on the other hand, don't suffer from those limitations. Which means that you can store any value you'd like and even load them at run-time from external sources (like a database or ini file). You can store other tables and/or arrays,  functions, class instances etc. you name it. So you see that both methods have their advantages over the other.

So why not combine them? Why not use two global variables for both dynamic and static configuration values like:
// Global table used to scope base configurations
_DCFG <- {
    // Cost of normal heal command
    Heal_Cost = 1000
    // The actual amount of health to set
    Heal_Amount = 100
}

// Global enumeration used to scope base configurations
enum _SCFG {
    // Number of milliseconds to delay the healing process
    Heal_Delay = 2000
    // Number of milliseconds to wait before waking up the coroutines
    Heal_Speed = 50
}

// Using them it's the same for both
print(_DCFG.Heal_Cost);
print(_SCFG.Heal_Delay);

In this case we have two global configurations. The first is `_DCFG`, short for Dynamic Configuration and is composed of a table which can have it's values changed at run-time. And the second is `_SCFG`, short for Static Configurations and it's composed of an enumeration which cannot be changed at run-time. You can also simply name the table `_CFG` and only prefix only the enumeration to reduce the amount of characters you have to type each time when accessing an element.

So, please, stop using hard-coded values. Even if it's just for something as simple as the time-span of a Timer instance. Because hard-coded values are hard to maintain with time when your code grows or when you'd like to share some code with others. Anything that can be tweaked should reside in the configurations. But not everything. Use these wisely or you'll end up adding more stress to the code.
.

.

#2
WHY DOES THE SWITCH CASE LOOK SO SEXY WITH REPEATED CONDITIONS?

There are times when you have a sh!t load of values to test against. Let's say that you have the variable 'my_id' and you want to test that against several integers to decide the type of that variable. Let's see how that looks done using a if statements:

function GetAnimalName(my_id)
{
    if (my_id = 1) return "Alpaca";
    else if (my_id = 2) return "Buffalo";
    else if (my_id = 3) return "Banteng";
    else if (my_id = 4) return "Cow";
    else if (my_id = 5) return "Cat";
    else if (my_id = 6) return "Chicken";
    else if (my_id = 7) return "Common Carp";
    else if (my_id = 8) return "Camel";
    else if (my_id = 9) return "Donkey";
    else if (my_id = 10) return "Dog";
    else if (my_id = 11) return "Duck";
    else if (my_id = 12) return "Emu";
    else if (my_id = 13) return "Goat";
    else if (my_id = 14) return "Gayal";
    else if (my_id = 15) return "Guinea pig";
    else if (my_id = 16) return "Goose";
    else if (my_id = 17) return "Horse";
    else if (my_id = 18) return "Honey Bee";
    else if (my_id = 19) return "Llama";
    else if (my_id = 20) return "Pig";
    else if (my_id = 21) return "Pigeon";
    else if (my_id = 22) return "Rhea";
    else if (my_id = 23) return "Rabbit";
    else if (my_id = 24) return "Sheep";
    else if (my_id = 25) return "Silkworm";
    else if (my_id = 26) return "Turkey";
    else if (my_id = 27) return "Yak";
    else return "Unknown";
}

Not only that it's not pretty at all. And it's also a bad decision performance-wise. How can we identify when if statements aren't the solution:
  • Is your if statement testing a single value against 5 or more other values?
  • Is your if statement testing only the equality of two values?
  • Are run-time evaluated expressions not needed in your if statement so far?
  • ...

Well, perhaps it's time that you'd consider using a switch statement instead. There's a reason someone would bother implementing a switch statement in their script if not for optimizing such situations. So let's see how the above example would look in a switch case:
function GetAnimalName(my_id)
{
    switch (my_id)
    {
        case 1: return "Alpaca";
        case 2: return "Buffalo";
        case 3: return "Banteng";
        case 4: return "Cow";
        case 5: return "Cat";
        case 6: return "Chicken";
        case 7: return "Common Carp";
        case 8: return "Camel";
        case 9: return "Donkey";
        case 10: return "Dog";
        case 11: return "Duck";
        case 12: return "Emu";
        case 13: return "Goat";
        case 14: return "Gayal";
        case 15: return "Guinea pig";
        case 16: return "Goose";
        case 17: return "Horse";
        case 18: return "Honey Bee";
        case 19: return "Llama";
        case 20: return "Pig";
        case 21: return "Pigeon";
        case 22: return "Rhea";
        case 23: return "Rabbit";
        case 24: return "Sheep";
        case 25: return "Silkworm";
        case 26: return "Turkey";
        case 27: return "Yak";
        default: return "Unknown";
    }
}

Eh? What did I tell you. Not only that the code looks much more clean but it's also much more optimized now. Notice that this time we're returning immediately after  the condition is met. Which means that a `break;` isn't needed here. But let's look at a case where the break is needed:

function GetAnimalName(my_id)
{
    local name;

    switch (my_id)
    {
        case 1:
            name = "Alpaca";
        break;
        case 2:
            name = "Buffalo";
        break;
        case 3:
            name = "Banteng";
        break;
        case 4:
            name = "Cow";
        break;
        case 5:
            name = "Cat";
        break;
        case 6:
            name = "Chicken";
        break;
        case 7:
            name = "Common Carp";
        break;
        case 8:
            name = "Camel";
        break;
        case 9:
            name = "Donkey";
        break;
        case 10:
            name = "Dog";
        break;
        case 11:
            name = "Duck";
        break;
        case 12:
            name = "Emu";
        break;
        case 13:
            name = "Goat";
        break;
        case 14:
            name = "Gayal";
        break;
        case 15:
            name = "Guinea pig";
        break;
        case 16:
            name = "Goose";
        break;
        case 17:
            name = "Horse";
        break;
        case 18:
            name = "Honey Bee";
        break;
        case 19:
            name = "Llama";
        break;
        case 20:
            name = "Pig";
        break;
        case 21:
            name = "Pigeon";
        break;
        case 22:
            name = "Rhea";
        break;
        case 23:
            name = "Rabbit";
        break;
        case 24:
            name = "Sheep";
        break;
        case 25:
            name = "Silkworm";
        break;
        case 26:
            name = "Turkey";
        break;
        case 27:
            name = "Yak";
        break;
        default:
            name = "Unknown";
        break;
    }

    return name;
}

After the condition is met we simply add our code after the `:`symbol, like we would in the blocks `{ ... }` of an if statement. But unlike the the if statements where the block of code is started by left curly brace `{` and closed by the right curly brace `}` symbols. The switch case has it's block of code started by the `:`symbol and it's closed by the `break;` keyword. And the `break;` keyword can be placed wherever you want in that block of code and can even be specified multiple times as if you would have multiple return points in a function. Let me illustrate that bellow:
function SomeTest()
{
    local data;

    switch (2)
    {
        case 1:
            data = "blah";
        break;
        case 2:
            if (2 > 3) break;
            else {
                data = "meh";
               
                if (2 < 3) break;
            }
            data = "overwritten";
        break;
        case 3:
            data = "wush";
        break;
    }

    return data;
}

As you can see we have 3 `break;` points in one block of code. Please node that a `break;` point isn't the same as a return point. A `break;` point simply breaks from the current context. In this case that switch statement. But it can also break from loops. So here's something you can't do with if statements.

But you probably noticed that I've drifted away from the main point of that second example. Which is: What would happen if I remove all the `break;` points? What do you think would happen if there nothing to say "Hey, Stop! I found my match. No need to visit the rest.". Well, I'll tell you what would happen if there's nothing to say stop. The switch statement would continue the tests even after the condition was matched until it would reach the default condition. Which means that no matter the number/id you pass to that function you would always get "Unknown".

Also, I've written that code in the second example quite relaxed. What that means is that I've added a few lines where I could just merge them all with the `break;` points in a single line. Let  me illustrate what I mean:
function GetAnimalName(my_id)
{
    local name;

    switch (my_id)
    {
        case 1: name = "Alpaca"; break;
        case 2: name = "Buffalo"; break;
        case 3: name = "Banteng"; break;
        case 4: name = "Cow"; break;
        case 5: name = "Cat"; break;
        case 6: name = "Chicken"; break;
        case 7: name = "Common Carp"; break;
        case 8: name = "Camel"; break;
        case 9: name = "Donkey"; break;
        case 10: name = "Dog"; break;
        case 11: name = "Duck"; break;
        case 12: name = "Emu"; break;
        case 13: name = "Goat"; break;
        case 14: name = "Gayal"; break;
        case 15: name = "Guinea pig"; break;
        case 16: name = "Goose"; break;
        case 17: name = "Horse"; break;
        case 18: name = "Honey Bee"; break;
        case 19: name = "Llama"; break;
        case 20: name = "Pig"; break;
        case 21: name = "Pigeon"; break;
        case 22: name = "Rhea"; break;
        case 23: name = "Rabbit"; break;
        case 24: name = "Sheep"; break;
        case 25: name = "Silkworm"; break;
        case 26: name = "Turkey"; break;
        case 27: name = "Yak"; break;
        default: name = "Unknown"; break;
    }

    return name;
}

Doesn't that look cleaner? Also note that the `break;` point in the default case it's pointless since there's no more condition after that. But you get the point.



But what about testing for multiple conditions for one value? Is that possible with switch statements? Well, of course it is. Let's have an example first:
function GetAnimalName(my_id)
{
    switch (my_id)
    {
        case 1:
        case 2:
        case 3:
        case 4: return "It's either 1,2,3 or 4";
        case 5:
        case 6:
        case 7:
        case 8: return "It's either 5,6,7 or 8";
        case 9:
        case 10:
        case 11:
        case 12: return "It's either 9,10,11 or 12";
    }
}

It's like testing for 1,2,3,4 in a single expression with if statements (equivalent of: my_id == 1 || my_id == 2 || my_id == 3 || my_id == 4). Therefore passing either 1,2,3 or 4 would return the "It's either 1,2,3 or 4" string. And the same applies for the rest of the groups. See? There's plenty of things you can do with a switch case.

Also note that switch cases can also have expressions like if statements do. Except smaller and must be evaluated at compile-time. You can't do this in many languages but Squirrel allows it. Let me illustrate that:
function TestExpr(my_id)
{
    switch (my_id)
    {
        case 1 < 10: return "blah";
        case 2 + 4: return "baz";
        case 3 - 1: return "foo";
    }
}

Expected results:
print(TestExpr(true)); // Outputs "blah"
print(TestExpr(2)); // Outputs "foo"
print(TestExpr(6)); // Outputs "baz"

Even I wasn't expecting that. But it seems that in as long as the expression can be evaluated at compile time you can use it in switch cases as well.

Anyway, I'm not saying that you should replace if statements with switch cases. All I'm saying is that there are some situations where a switch statement would be more appropriate than an if statement. So you should get the hang of them because they're quite useful.
.

.

#3
(DUUM DU DU DUM DUM) CAN'T TOUCH THIS!

With the appearance of so called 'free hosts'. I would like to raise some awareness on script leaking and what can you do to protect yourself against such cowardly actions. It's not a total protection but you can be sure that anyone having your scripts can't extract any vital information or pieces of code (snippets/ideas/implementations) from them.



But first let me explain what byte-code serialization is and how can you use it. Normally, every time you wish load your game mode, you specify the script that you wish to load. Then the Squirrel compiler loads that text file and compiles it every time. Let's say we have this dummy code for example:
local test = 32;
if (test = 24)
{
print("test == 24");
}
else
{
print("test != 24");
}

Obviously I can come in and copy whatever I need from this code. And that's not what we want to have on an insecure server.

Have you ever heard of assembly? That weird language that 'nobody uses' and 'only computers can understand it'. Well, Squirrel has it's own kind of assembly. It's called byte-code. After your readable code is read from the text file. It's parsed and transformed into byte-code. Byte-codes are a series of instructions that tell the computer what to do based on what you code wrote.

Because assembly is such an old and wide-spread language. People have created software to read assembly and convert it back to readable code. When you attempt to convert assembly back to human readable code, the process is called decompiling (which is the opposite of compiling the code). When you simply want to look at the actual generated assembly, the process is called disassembling (which is the opposite of assembling the code). So, pay close attention to those therms to not get confused.

But even after decompiling the assembly (kinda confusing at first, right?). The resulted code is not even close to the original one. So you can safely assume that once you've compiled your code into assembly/byte-code you can never fully reassemble it back as it was.

Squirrel on the other hand is not that popular and no one bothered to create a decompiler for it. Of course, someone can do that any time now. But it won't be that easy. Which means that after you've compiled your code, people cant turn it back into readable code and make any changes.

Table of references:
  • Compiling : Taking human readable code and turning it into machine-code/byte-code.
  • Decompiling : Taking machine-code/byte-code. and turning it back into human readable code. (actually, trying to)
  • Byte-code : Instructions resulted from compiling human readable code (byte-code is not machine code (assembly))
  • Assembly : Machine code resulted from compiling human readable code.

Compiled Squirrel scripts, usually have the extension '.cnut' (compiled .nut). Let's have a look at how the previous file looks after compiled:


Let's start with Notepad++. The right half, displays the compiled script in hex and the left half displays it loaded as a simple text file. While the small Notepad on the lower left corner displays the original code before compiling.

You can clearly see that there's nothing readable from our script after it was compiled. And anyone trying to use your script is forced to us it as is without being able to make any changes such as fix any bugs or adding new features. Which means that you can even leave small exploitable bugs in your script that are only known to you and if anyone tries to use your leaked script you can crash their server easily.

I hope you understood a bit about how scripts work and how you can protect your self. Now to the good stuff. Compiling our scripts.



Assuming that you're on your PC and you need to compile your code. Create a dummy script like 'compile.nut' and load that as your game mode inside 'server.cfg'. Now create another file, as an example 'my_game.nut' and for the sake of this example add the code we listed above.

Add the following code to 'compile.nut' and run your server:
// Compile the script we want and save it's bytecode into a variable
local bytecode = loadfile("my_game.nut", true);

// Save the bytecode from the variable above to a file
writeclosuretofile("my_game.cnut", bytecode);

When the server starts you should see a new file named 'my_game.cnut' containing your compiled version of the file 'my_game.nut'. You can do this for as many files as you wish. Now let's explain what the above code did.

There's to functions that you can use to execute other scripts from another script. They are dofile([string] filepath, [boolean] show_errors) and loadfile([string] filepath, [boolean] show_errors).

The first function 'dofile(...)' takes a file compiles it and then executes it.The second parameter is a Boolean which tells whether you want to show errors when compiling your script (true) or hide them and fail silently on errors (false). I strongly recommend that you enabled errors. This is the file we need to load compiled files.

The second function 'loadfile(...)' works exactly as the first one. Except it only compiles the script and returns the byte-code as a closure (aka. function). This is the one we use to compile files.

Which means that the line of code above compiles the script we want and saves the resulted byte-code in the variable that we specify.

The second line of code uses another function writeclosuretofile([string] filepath, [closure] closure)
writeclosuretofile("my_game.cnut", bytecode);
This function takes a string as the first argument that will represent the file where we wish to save our stuff. And a closure (aka. function) that we wish to save into that file.

And since we have the byte-code from the previously compiled file saved in a variable. We simply give it that variable to save. Now that we have our code compiled and serialized it's time to load it back.



Assuming that you're on the machine where your server is running. Create a dummy script like 'loader.nut' and load that as your game mode inside 'server.cfg'. Also add your previously compiled file (usually with the extension .cnut) as you would simply add a normal script. Remember to only add your compiled script file and NOT your actual text script.

Loading byte-code from compiled scripts is the same as loading normal scripts with the 'dofile(...)' function. If we follow the example then I would simply add the following in my 'loader.nut' to load my code :
// Loads a text or compiled script end executes it
dofile("my_game.cnut", true);

And voila, your script is now just a huge chunk of alien code that only a really advanced programmer could understand it but never bring it back to it's original form.

Your compiled version stays with the server while the text version stays safely in your computer.



I haven't tested this on a complex gamemode and there might be some issues with the serialization of some registered types. I'm not saying that there is some issues just that there could be. Until we encounter them I can't tell for sure.

I hope you understood the idea and even though it's a pain to update your scripts with all that compiling it's worth to know that your script is safe.

DO NOT POST HERE FOR QUESTIONS. EITHER PM ME OR OPEN A TOPIC.
.