Jump to content

Scripting (lua)


Codex

Recommended Posts

A useful foundation. I learned some things today. Thanks!

 

I added this to the Modding Basics section of the List of Mods and Modding Guides on the SacredWiki. Looking forward to seeing if you expand this.

 

No need to put down previous threads, whatever their usefulness. Your contributions stand strong enough on their own without that. :)

  • Like! 1
Link to comment

Operators

Its important that we cover the different type operators which are available to us in lua (and many other languages)

 

​Arithmetic Operators

+ The plus sign is the addition operator it is used to add 2 operands together.

 

Example: (2 + 2) = 4
 

 

- The hyphen is the minus operator it has 2 purposes 1 of which can negate a value (-2) (aka the urinary operator) and the other subtracts 2 operands.

 

Example: (2 - 2) = 0
 

 

/ The forward slash is the division operator, it is used to divide 2 operands.

 

Example: (2 / 2) = 1
 

 

* The asterisk is known as the multiplication operator it is used to multiply 2 operands.

 

Example: (2 * 2) = 4
 

 

% The percentage sign is known as the modulo operator, its purpose is a 2 step process, 1st it divides 2 operands and then returns the remainder of what is left over.

 

Example: (3 % 2) = 1
 

 

^ The caret is called the exponent operator, it takes 2 operands and returns the exponent of

 

Example: (2 ^ 3) = 8
 

 

 

Relational Operators

The relational operators return true or false based on the outcome of their comparison.

== Two equal signs is known as the equivalence operator, it is used to check whether 2 operands are equal or not

~= The tilde and the equal sign is known as the not equal to operator, it essentially checks whether the 2 operands are not equal to one another.

> The greater than checks to see if the value on the left is greater than the value on the right.

< The less than checks to see if the value on the left is less then the value on the right.

>= The greater than or equal to is used to check if the value on the left is greater than or equal to the value on the right.

<= The less than or equal to checks to see if the value on the left is less than or equal to the value on the right.

 

Logical Operators

The logical operators also return true or false based on the outcome of their comparison.

and The and logical operator checks to see if both operands are true, if they both are true, then a value of true is returned.

 

Example: (1 == 1 and 2 == 1) -- returns false 
 

 

or The or logical operator checks to see if either the left or the right operand is true, if either is true then it returns a value of true.

 

Example: (1 == 1 or 2 == 1) -- returns true
 

 

not The not logical operator returns the opposite of the condition, if the condition is true not makes it false and vice versa.

 

Example: ( not true) -- returns false 
 

 

 

Miscellaneous

.. The 2 dots are used in concatenation.

 

Example: print("hello" .. " world!") -- prints hello world!
 

 

# The hash tag is known as the unary which returns the length of a string or table.

 

Example: print(#"hello world!") -- prints 12
Example: print(#{1, 2, 3}) -- prints 3
 

 

 

The Order of Precedence

Although in programming you can use parentheses to explicitly direct the order execution of an expression its still important to learn the order of precedence.

Unary 			not # -  	  (right to left)
Concatenation 	        ..		  (right to left)
Multiplicative 	        * / % 	          (left to right)
Additive		+ - 	 	  (left to right)
Relational 		< > <= >= == ~=   (left to right)
Equality 		== ~= 		  (left to right)
Logical AND 	        and 		  (left to right)
Logical OR 		or 		  (left to right)

 

 

 

Control Structures

A control structure is simply a logical operation which is used to control the flow of execution.

Here is an example of a simple control structure.

if a > b then
    print("a is greater than b")
elseif a < b then
    print("b is greater than a")
else 
    print("both a and b are equal in value.")
end

 

The above code states, if a is greater than b then print a is greater than b. Else if b is greater than a print b is greater then a, else print both a and b are equal in value.

 

Another way we can use a control structure is through short hand, using the logical operators to assign values to a table or variable.

Example:

-- if the player level is greater than 100 (and) assign 50 to experience
-- else (or) assign 200 to experience
local experience = (playerLevel > 100) and 50 or 200

The short hand assignment can be nested, I'll expand more on this at a later time. If you are familiar with c++ or even php you've already used shorthand

notation using ? and :

 

In php or c/c++ the and/or short hand is very much similar to

// php
$experience = ($playerLevel > 100) ? 50 : 200;

// c/c++
int experience = (playerLevel > 100) ? 50 : 200;
Edited by Codex
Link to comment

Iterators

 

There are 4 types of iterators aka loops in lua, the numerical for, the generic for, the while and the repeat. Lets take a close look at each one.

 

The Numerical For

	for variable = start, stop, step do
		-- code goes here
	end
The numerical for loop consists of the keyword for, variable is simply the local variable associated with the for loop and can be named almost anything, I know there is that word local again :P. Then we have the assignment operator which takes the value stored in start and stores it in variable.
Start is merely a description of the execution of the for loop, start is a value of where we would like our for loop to start counting, e.g. for I = 1. Stop is a value which would like our for loop to stop at (e.g. for I = 1, 10) and finally Step is the incremental value for our local variable, if omitted the the for loop uses step's default value of 1.
Lets take a look at an example of the numerical for loop so maybe it will make more sense.
   -- 1st example, with the step value omitted
     for I = 1, 5 do
       print(I)
     end
     -- prints
     1
     2
     3
     4
     5

   -- 2nd example, with a step value of 2
     for I = 1, 10, 2 do
       print(I)
     end
     -- prints
     1
     3
     5
     7
     9
    
In the 1st example the numeric for loop prints the value of I 5 times, by omitting the step value the for loop used the default step value of 1 and printed every iteration of I to screen.
In the 2nd example the for loop does pretty much everything as the 1st example did except that we manipulated the I's value based on the step value.
Using 2 as our step value we tricked the for loop into increasing the value of I by 2 rather than 1 per iteration this is why the results of I are 1,3,5,7,9 rather than 1,2,3,4,5,6,7,8,9,10, because we are adding the step value to I per iteration.
Using this formula per iteration for step being 2.
     I = 1
     step = 2
     max = 10
  -- compare I to max
     -- iteration 1
     print(I) -- 1
     I = (I + step)
  -- compare I to max
     -- iteration 2
     print(I) -- 3
     I = (I + step)
  -- compare I to max
     -- iteration 3
     print(I) -- 5
     I = (I + step)
  -- compare I to max
     -- iteration 4
     -- etc...

So how does the numeric for loop know when to stop?
Simple it initially evaluates all 3 expressions, the value of I, the max value to meet and the step value if any
And then compares the value of I to the max value to meet based on the step value.. this sounds confusing I bet
We might need another example to show what I am referring to when I say based on the step value.
   -- 1st example
     for I = 5, 1, -1 do
       print(I)
     end
     -- prints
     5
     4
     3
     2
     1

   -- 2nd example
     for I = 10, 1, -2 do
       print(I)
     end
     -- prints
     10
     8
     6
     4
     2
   
In the 1st example we are using a negative number (-1) as our step value and assigning I a value of 10 while making the max value to meet 1
Since the step value + I controls the value to increment per iteration we can still use this same formula as we did for a positive step value

 

Using this formula per iteration for step being -1.
     I = 10
     step = -1
     max = 1
  -- compare I to max
    -- iteration 1
     print(I) -- 10
     I = (I + step)
  -- compare I to max
     -- iteration 2
     print(I) -- 9
     I = (I + step)
  -- compare I to max
     -- iteration 3
     print(I) -- 8
     I = (I + step)
  -- compare I to max
     -- iteration 4
     -- etc...

Using this formula per iteration for step being -2

     I = 10
     step = -2
     max = 1
  -- compare I to max
    -- iteration 1
     print(I) -- 10
     I = (I + step)
  -- compare I to max
     -- iteration 2
     print(I) -- 8
     I = (I + step)
  -- compare I to max
     -- iteration 3
     print(I) -- 6
     I = (I + step)
  -- compare I to max
     -- iteration 4
     -- etc...

If we add a negative number to a positive number we are essentially subtracting e.g. 10 + (-2) = 8

 

 

The Generic For

The generic for loop is always used on a table, it is define as

     for key, value in pairs(table_name) do
        -- code goes here
     end
     -- or
     for key, value in ipairs(table_name) do
        -- code goes here
     end
   
What a generic for loop does it allows us to easily traverse through a table.
Early on we referred to tables having properties and indexes well the index of a table can be referred to as its key and a property as its value.
Lets break apart the generic for loop and explain how it works.
The generic for loop is initialized using the for keyword (just as you did with the numeric for), it is then assigned a variable to be used to hold the table's key value this variable can be named anything even an underscore ( _ ) as long as it follows lua's naming convention.
We then place a comma after the key's variable name and create another variable this variable will hold the value of the table's key.
We then place a space and type the in keyword and then another space and use either pairs or ipairs followed by an open parentheses followed by the table name followed by a closing parentheses and then another space followed by the do keyword and the end keyword to close the generic for loop.

 

The difference between pairs and ipairs is pairs will iterate over everything in the table regardless if it has a numerical key or not in no particular order. Where as ipairs will only iterate through a table if the table has keys that are numerical.

 

Lets take a look at both pairs & ipairs to see how they are different.

 

Using pairs

	local t = {
		["one"] = 1,
		[2] = 23,
		codex = "my name",
		50
	}

	for k, v in pairs(t) do
		print(k, v)
	end
	-- output
	1   50
	one   1
	2   23
	codex   my name

 

 

Using ipairs

	local t = {
		["one"] = 1,
		[2] = 23,
		codex = "my name",
		50
	}

	for k, v in ipairs(t) do
		print(k, v)
	end
	-- output
	1   50
	2   23

As you can see from the 2 examples pairs printed everything to the screen where as ipairs only printed non-text based keys.

 

While loop

 

The while loop although hardly ever used in most scripts (at least in my experience), has a simple structure.

while(condition) do
    -- code to execute here
end

As long as the condition of the while loop returns true the while loop will execute. Here is an example of a while loop:

local I = 1
while(I < 10) do
   I = I + 1
   print(I)
end
-- prints
2
3
4
5
6
7
8
9
10

Repeat Until loop

The repeat until loop has a similar structure to the while loop.

repeat
    -- code to execute goes here
until (condition)

Here is an example of the repeat until loop:

local I = 1
repeat
    I = I + 1
    print(I)
until I > 10

-- prints
2
3
4
5
6
7
8
9
10
11

 

I rarely if ever use the while/repeat loops for anything in lua due to the fact that they can cause an infinite loop and crash a program.

 

 

Edited by Codex
Link to comment
  • 2 weeks later...

Functions and Scope

 

A function in any programming language is created and used to simplify redundant coding, the basic form of a function body is constructed as:

function myFunction()
end

To construct or define a function we start with the keyword function followed by the name of the function, a both open and closed parentheses and the end keyword to close the function.

 

Functions can also be declared/defined like this.

myFunction = function()
end

You can also rename a function by passing the function to another identifier.

function myFunction(value)
    print(value)
end

myNewFunction = myFunction

myNewFunction(10)
-- prints
10

The value identifier in the parentheses of myFunction is arbitrary and can be named anything, this identifier is known as a parameter, it is used to pass information to the function so that the function can do what it needs to do with that data.

 

Lets say we wanted to calculate the conversion of fahrenheit to celsius and vice versa we could create a function to calculate those values for us.

function convertTemperature(temperature, toCelsius)
    local value = "The temperature ".. temperature .. " in "
    if toCelsius then
        value = value .. "fahrenheit is "..(((temperature - 32) * 5) / 9) .. " in celsius."
    else
        value = value .. "celsius is "..(((temperature * 9) / 5) + 32) .. " in fahrenheit."
    end
    print(value)
end

Now we can call on the function to determine what the temperature of say 23 fahrenheit is in celsius.

convertTemperature(23, true)
-- prints
The temperature 23 in fahrenheit is -5 in celsius.

Now if we wanted to determine what the value of say 0 celsius is in fahrenheit we could simply set the 2nd parameter to false or just omit it entirely.

convertTemperature(0)
-- prints
The temperature 0 in celsius is 32 in fahrenheit.

 

Scope

 

Scope refers to the visibility of variables, functions or tables. In other words, which parts of your program can see or use them. There are two forms of scope Global & Local.

 

Local variables are created with the local keyword, their scope or visibility only exist within the body it was declared in where as global is simply defined without using the local keyword and its visibility is seen outside the body it was declared in but only when that body is brought into view.

 

Lets look at an example of both local and global scope.

 

function A()
    -- x is global, but only global within the scope of function A
    x = 1
end

function B()
    -- since we never called on the function A, x returns nil, because it doesn't exist outside of A
    print(x)
end

B()
-- prints
nil

As you can see from the comments above, the scope of x defined in A is global to A, but when called by B its looses its scope because we never brought A inside of the scope of B.

 

Lets try this again but this time calling A inside of B.

function A()
    -- x is global, but only to the scope of function A
    x = 1
end

function B()
    A() -- call function A and bring its values which are global into function B's scope
    print(x)
end

B()

-- prints
1

Now that we have called function A within function B, B can now clearly see the value of x.

 

So what if we changed x's scope to local, would B still see the x when we called A from B? Well lets try it.

function A()
    local x = 1
end

function B()
    A()
    print(x)
end

B()
-- prints
nil

The answer is no, because the scope of x was declared local to the function of A, so even tho function B called on A, any values which were not declared as global B won't be able to see.

 

Here is where it might get a little confusing (If you aren't confused already :P ). When a value is declared as local outside of a function then its scope although declared local is actually global. You are probably asking yourself, what do you mean "When a value is declared as local outside of a function then its scope although declared local is actually global?"

 

Well lets take a look at an example:

local text = "I am text, although I am declared local, I am actually global to most things"

function test()
    print(text)
end

test()
-- prints
I am text, although I am declared local, I am actually global to most things

 

The reason the test function can see text is because text was declared before test, allowing test to access text's value. If we were to declare text's value after test, it would print nil when we called it.

 

function test()
    print(text)
end

local text = "I am text, although I am declared local, I am actually global to most things"

test()
-- prints
nil

So now you might be asking yourself, "What if I omit the local keyword from text, will I be able to access text's value?". Lets have a look.

function test()
   print(text)
end

text = "I am text"

test()
-- prints
I am text

Sure enough you can access text's value from test, even tho test was defined before text, this is because the lua interpreter isn't actually calling the value of text when the function test is defined, it only looks for the value of text when the function test is called.

 

 

Returning Values

 

We have covered quite a bit in relation to functions, we've learned how to construct and define a function, pass information to a function, we learned about scope and even learned how to print values to the console. But there is more to functions than just printing values, such as returning them to whatever called the function.

 

To return a value from a function is pretty straight forward, in order to this we use the return keyword.

 

function add(a, b)
    return a + b
end

The add function could be called like this.

function add(a, b)
    return a + b
end

local sum = add(6, 7)

print(sum)
-- prints
13

Functions can also return not just single values but they can also return true/false, tables, multiple values or even other functions.

 

Here is an example (although it could be written a lot better :P ) of a function calling other functions but returning either a string or a table of values.

--[[ 
    if the strings length is greater 2 this will treat a string as an array 
    and pop off the 1st 2 characters of the string returning the remaining string, 
    if not it returns nil ( nil essentially means false )
]]
function pop(v)
    return #v > 2 and v:sub(3, #v) or nil
end

-- this function will clip the string for the 1st 2 characters and return them as a table
function clip(j)
    local y = {}
    for I = 1, #j do
        table.insert(y, j:sub(I, I))
    end
    return y
end

--[[ 
    this function will convert hex color codes to its respective rgb values
    optionally returning a string or table of values
]]
function hexToRgb(hexColor, asString)
    -- all possible hexidecimal values
    local hex = {
        ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5, 
        ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
        ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15
    }
    -- table to hold the rgb color codes
    local rgb = {}
    -- loop while hexColor does not equal nil
    while(hexColor) do
        local value = clip(hexColor:sub(1, 2):lower())
        table.insert(rgb, ((hex[value[1]] * 16) + hex[value[2]]))
        hexColor = pop(hexColor)
    end
    return asString and table.concat(rgb, ", ") or rgb
end

print(hexToRgb('FF2344', true))
-- prints
255, 35, 68

 

Admitingly the above code could be written a lot better, with type checking and such... so lets do that now, lets re-write the above code to handle different types of values with the appropriate lengths.

 

-- all possible hexidecimal values
local hex = {
    ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5,
    ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
    ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15
}

--[[
    if the strings length is greater 2 this will treat a string as an array
    and pop off the 1st 2 characters of the string returning the remaining string,
    if not it returns nil ( nil essentially means false )
]]
function pop(v)
    return #v > 2 and v:sub(3, #v) or nil
end

-- this function will clip the string for the 1st 2 characters and return them as a table
function clip(j)
    local y = {}
    for I = 1, #j do
        table.insert(y, j:sub(I, I))
    end
    return y
end

--[[
    this function will convert hex color codes to its respective rgb values
    optionally returning a string or table of values
]]
function hexToRgb(hexColor, asString)
    -- table to hold the rgb color codes
    local rgb = {}
    -- determine what type of data hexColor is
    if type(hexColor) == "string" then
	while(hexColor and #hexColor > 1) do
	    local value = clip(hexColor:sub(1, 2):lower())
	    table.insert(rgb, ((hex[value[1]] * 16) + hex[value[2]]))
	    hexColor = pop(hexColor)
	end
	return (#rgb > 0) and (asString and table.concat(rgb, ", ") or rgb) or "You must use at least 2 values"
    else
	-- this will handle all other types of data which we try to pass to this function 
	return "This function will not process " .. type(hexColor) .. "s, please place your values in single(\') or double quotes(\")"
    end
end

 

So now lets test that theory. Can this semi-rewritten function handle different values types and proper lengths?

 

Lets try it passing limited values

print(hexToRgb('B', true))
-- prints
You must use at least 2 values

Now lets try passing it just numbers

print(hexToRgb(00, true))
-- prints
This function will not process numbers, please place your values in single(') or double quotes(")

Now lets try with a table, empty or otherwise.

print(hexToRgb({}, true))
-- prints
This function will not process tables, please place your values in single(') or double quotes(")

Next lets try with a function, in this case an anonymos one.

print(hexToRgb(function() end, true))
-- prints
This function will not process functions, please place your values in single(') or double quotes(")

Lets pass it a nil value (literally)

print(hexToRgb(nil, true))
-- prints
This function will not process nils, please place your values in single(') or double quotes(")

 

So... As you can see the function can now handle different values thrown at it ( still not perfect but who/what is :P ).

Edited by Codex
  • Like! 1
Link to comment

When you write a program/script you are testing the possible outcomes whether they are for the benefit of your idea or the exclusion of things you don't want. So you should always have the thought in the back of your mind (especially if the code is meant for production) of "What if?".

 

This what if? mindset is what seperates good code from really bad code, all code is written in steps, and 99% of it never works the 1st time around, plus your code is always evolving so its ok to write something out in a complicated manner and then go back later on and shorten it up or add to it.

 

On a final note, if you are satisfied with the layout and execution of your code... then you are missing or overlooked something.. Good programmers are never satisfied with their code... hell we will spend days working on a small fragment of code til we figure out why the hell it isn't working.. But that is how you become a good coder is learning from your mistakes.. and you will make a lot of them :)

 

I am sure this tutorial could of been better, but I don't really have a lot of time to devote to expanding it.

 

For further reading please check out.

https://www.lua.org/manual/5.1/
or
https://www.tutorialspoint.com/lua/index.htm
Edited by Codex
  • Like! 1
Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...
Please Sign In or Sign Up