Windower: Lua Guide for programmers - Windower

Jump to content

Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

Lua Guide for programmers

#1 User is offline   Arcon 

  • Member
  • PipPipPip
  • Group: Playtesters
  • Posts: 75
  • Joined: 07-July 06
  • Gender:Male
  • Location:Munich, Germany
  • Name: Arcon
  • Server: Leviathan
  • Jobs: WAR/PLD/THF
  • Race: Hume Male
  • Linkshell: ShadowRavenloft

Posted 11 March 2013 - 05:29 PM

Since Lua will be of interest to anyone with moderate programming skills, but at the same time has some oddities that programmers from other languages or environments will find... well, odd, I thought I'd write up my experiences learning Lua to help others avoid some Lua beginner's mistakes. Fortunately, it's not that much of an issue, now that an innocent mistake doesn't crash POL anymore, but it would still help to know some tricks to getting around in this language.

What's familiar?

As many interpreted scripting languages, Lua has a focus on list management and string manipulation. That's why it provides a few syntactic shorthands for dealing with such constructs, as well as functions dealing with issues regarding those subjects. Lists can be explicitly declared using braces: list = {1, 2, 3}.

Here are a few keywords that will give people an idea of some of Lua's design aspects:
  • Case-sensitive
  • Dynamically and weakly typed
  • Line-based
  • Object-oriented (ish)


Objects

Lua is object-oriented (for all intents and purposes anyway), and class methods are accessed by the . (dot) operator. Accessing object methods is done by the : (colon) operator. Assuming a class cls, an object of that class obj and a method called method, these two are the same:
cls.method(obj, arg1, arg2, ...)

and
obj:method(arg1, arg2, ...)


Programming for Windower, you'll mostly need the string and table classes. For a complete reference of functions for those two (and others), check here (string) and here (table).

Lua also has a notion of a null value, named nil. It has a few special properties, that will be explained later.

Scoping and syntax

Lua does not use braces to denote blocks, but instead uses do, then and similar tokens to denote the beginning of a block, and end to denote the end.

Aside from that, Lua is reminiscent of Javascript, in that it's line-based and does not require semicolons or other tokens to denote the end of a statement, but requires them if multiple statements are to be placed in one line.

Types and coercion

As many other scripting languages, Lua uses dynamic weak typing. This implies that variables do not need to have a fixed type, and can be reassigned something else entirely where necessary. It also means that functions and operators do not work on fixed types, but can get varying types of arguments, which forces Lua to coerce types.

This does not happen often, however. Lua only converts numbers to strings and vice versa, where appropriate. The operator decides the outcome (.. is the concatenation operator):
> = "2"+5
7
> = 4/"2"
2
> = "a"..5
a5


Another kind of coercion happens for truth-value checking. Similar to a few other languages, Lua treats everything as true, except for false and nil.

This means that if varname can be used to check if varname has been defined, but it also has some pitfalls; if varname has been declared but set to 0 or the empty string, it will still evaluate to false, so the check for nil should be explicit.

Specifically for Windower, people will want to output strings to either the Windower console or to the FFXI chatlog. Lua provides a tostring function, which can take any argument at all and output a string representation of it. This is not needed for numbers, but for other values such as objects, nil or boolean values.

Variadic functions

Also like some other languages, it provides an interface for functions that take optional arguments, keyword arguments, or argument lists of arbitrary length.

If a function is called less arguments than specified, the remaining arguments will be nil. If a function is called with more arguments than specified, the superfluous arguments will be lost, unless the function is specifically declared to accept a variable number of arguments. How this works will be explained later.

It's important to note that neither of these will give an error, meaning error-checking has to be implemented manually, where required.

What's different?

This section will largely mirror the previous one, only highlight the sometimes subtle, sometimes glaring and sometimes entirely incomprehensible differences Lua presents when compared to other languages.

Operators

Lua is very limited to assignment operators. It has no in-place assignment operators whatsoever. ++ and -- do not exist, neither do += and any similar forms.

For comparison operators, the usual is available, only inequality is checked with ~=.

Arithmetic operators are largely the same, only exponentiation has its own operator and is denoted by ^. Bitwise operators are not present in the native library.

Logical operators are written out: and and or. Notable here is that or returns the first non-falsey value (as mentioned in the type coercion above). This is often use to define default operators:
function foo(bar)
	print(bar or 10)
end

> = foo(5)
5
> = foo('baz')
baz
> = foo()
10


Additionally, string concatenation is done by .. (double dot). This can be confusing, as Lua also uses . (dot) for accessing class methods, and ... (triple dot) for variadic arguments.

# returns the length of both tables and strings.

There is no way to create new operators, but some existing operators can be overloaded rather inconveniently using metatables. That's a construct that won't be explained further at this point, but may be included at a later date for advanced programmers.

Strings

Lua has some rather weird string notions. It provides the usual string representation with double quotes "this is a string", and like some languages also provides single-quoted strings 'this is another'. There is no difference between the two, but there is a catch: neither of these methods work over more than one line. To provide a multiline string interface, Lua uses double brackets:
[[this
is a
multiline string]]


String splitting deserves a special mention, as there isn't any in Lua. For that you need to make your own function or use the stringhelper.lua in the /libs.

Comments

Comments are prefixed with a -- (double hyphen). Everything from there until the end of the line is considered a comment and not parsed. However, this only works for one line. Similarly to strings, multiline comments are enclosed in double brackets, but preceded by --:
--[[this is a
multiline comment]]


Tables

Tables, as mentioned before, are Lua's analogon to arrays and lists. However, they are very different in certain regards. For starters, tables are implemented as hash maps, and always map a key to a value. That means there's no way to define pure lists, but instead {'a','b','c','d'} is identical to {1='a', 2='b', 3='c', 4='d'}. Note, though, that the latter is not valid Lua syntax, you cannot assign integer keys in the explicit constructor, only strings. You can, however, assign arbitrary integer keys afterwards, like so:
> t = {a=4, b='c'}
> t2 = {}
> t2[1] = t
> t2[2] = t['a']
> t2['bla'] = t[2]
> = t2[2]
4


Surprisingly (to some), this does not error. t[2] was never defined, but tables return nil for undefined keys. This is a problem when trying to iterate over integer keys, as the table assume it ended as soon as it encountered a nil value, even if higher keys still have other values.

Also, if no keys are provided at the definition of a table, a table will be 1-indexed, not 0-indexed, as people may know from other languages:
> t = {4, 5, 6}
> = t[1]
4
> = t[2]
5
> = t[#t]
6
> = t[0]
nil


The latter works, because t[0] is undefined, and undefined values all return nil.

Another interesting point is that string-keyed tables can be accessed both with bracket syntax (t['bla']) as well as dot syntax (t.bla).

Unlike in some languages, it's not possible to slice tables without writing a custom function for that. However, there will be functions provided in libraries that can be included in Windower addons.

Tutorial

So much for the similarities and differences, now for some specifics about how to actually program in Lua. This will mostly cover syntax and how to use certain features of the language.

Assignment

Lua has a global and a local scope. The global scope crosses between functions and variables are global by default. To restrict a variable to the local scope (within a function, loop or block), you need to explicitly declare them with the local keyword:
function foo()
	x = 5
	local y = 2
	print(x, y)
end

> foo()
5	2
> print(x, y)
5	nil


A useful assignment method to append an element to a table is as follows:
> t = {'a', 'b', 'c'}
> = t[3]
c
> = t[4]
nil
> t[#t+1] = 'd'
> = t[4]
d


This can be used in a loop, to populate a table by appending a value in each iteration.

Another useful feature is multiple assignment, especially when coupled with another Lua feature, multiple return. In some languages, returning multiple values requires putting them into a data structure, such as a list or array, and then extracting them again. In Lua, there's a syntactic mechanism for that:
function foo()
    return 13, 7
end

> a = foo()
> print(a)
13
> a, b = foo()
> print(a, B)
13	7
> a, b, c = foo()
> print(a, b, c)
13	7	nil


Loops

Lua provides a standard while loop (while cond do body end) and a self-explanatory repeat loop (repeat block until condition). It also provides a for loop, with different syntax:
> startnum = 3
> endnum = 13
> stepsize = 2
> for i = startnum, endnum, stepsize do print(i) end
3
5
7
9
11
13


stepsize is optional and defaults to 1.

Iterating over tables

The previous looping function can be used on some tables, namely tables with integer keys that don't skip any values:
> t = {'a', 'b', 'c'}
> for i = 1, #t do print(i) end
a
b
c


All other tables will have to be iterated over by making pairs of the key/value pairs. This is done with the appropriately named pairs function:
> t = {a='c', b='a', c='b'}
> for key, val in pairs(t) do print(key, val) end
a	c
c	b
b	a


As you can see, this does not maintain the original input order, and thus behaves like a regular hashmap. For array-tables, you can use ipairs to iterate over the values in an ascending order, but that will only work if the keys are sequential integers with no gaps. It will not work for non-numeric keys at all.

Functions and arguments

A few function definitions were already shown above, as well as some normal function behavior. Functions are defined with a simple function name(arg1, arg2, ...) body end block. The argument handling was also mentioned before, except for variadic arguments. Assume we want to write a max() function, which takes an arbitrary number of arguments, and returns the maximum of those. To do that, we use this:
function max(...)
	-- Code here
end


When this function is called, the ... will contain the arguments, but not in a table format, instead in a raw sequence format, such as returned by a function that returns multiple arguments:
function max(...)
	local a, b, c = ...
	print(a, b, c)
end

> max(1,2,3)
1	2	3
> max(1)
1	nil	nil


Most often, you'll probably want the values in a table:
function max(...)
	local args = {...}
end


From here we can iterate over it, as defined above, to find the highest value:
function max(...)
	local args = {...}
	local highest
	for i = 1, #args do
		if highest == nil or highest < args[i] then
			highest = args[i]
		end
	end
	return highest
end


This will return nil if no arguments were provided, because then highest will never be assigned to.

Handling Windower functions

Finally, a short explanation of how to handle the Windower-related functions and behavior.

Player data and mob array

Both player data and mob data is stored in Lua tables. The mob array holds all the information you have available on monsters, NPCs, PCs (including yourself) and objects. You can access it, by providing an index. The first confusing thing is that player characters have two IDs. One permanent ID which is your global character identifier. Additionally, every object in a zone has an associated ID within that zone, and as a player character, you also get one as soon as you zone in. For anything that deals with players inside a zone, you'll need to use the latter so-called mob index or just index.

In contrast, the player array contains information about your own character. This has information about your equipment, your inventory, job, subjob, etc, whereas the mob array only holds information about you that it also has for any other player character.

The player array can be accessed by the get_player() function, and the mob array with the get_mob_by_index() function. For example, to get the name of your current target, you would do this:
player = get_player()
index = player['targets_target_id']
target = get_mob_by_index(index)
targetname = target['name']


Events

Addons are event-driven, meaning they can't do things on their own, and instead react to events. This even includes sending commands, because they're caught with the appropriate event. A full list of events can be found here.

For example, if we wanted to get the name of a character who /waves at us, we would have to use the event_emote() event:
function event_emote(senderId, targetId, emoteId, motionOnly)
	if(emoteId == 8 and targetId == get_player()['target_id']) then -- /wave has ID 8
		local name = get_mob_by_index(senderId)['name']
		-- Do something with name here
	end
end


Interface functions

These functions are designed to communicate with external functions, such as FFXI related Windower functions. The full list of the functions available can be found here.

For example, let's amend the previous function to a /tell to the person who waved at us:
function event_emote(senderId, targetId, emoteId, motionOnly)
	if(emoteId == 8 and targetId == get_player()['target_id']) then
		local name = get_mob_by_index(senderId)['name']
		send_command('input /tell '..name..' What\'s up?') -- Escape the apostrophe
	end
end


Register Windower commands

One of the most important parts is how to control the addons you write. For that, you need to be able to send commands from the FFXI chatlog (or the Windower console) to Lua. This is done with the following syntax: //lua command <AddonName> [arg1[, arg2[, ...]]. (Note: You can abbreviate //lua command to //lua c.)

This is caught addon-side with an event, as mentioned above, specifically the event_addon_command(...) event. Here it's defined with variadic arguments, that is, it takes an arbitrary number of arguments, separated by whitespaces in the chatbox.

If you have a plugin called "Position" which outputs your target's x or y position depending on what you specify, you could write the following command handler:
function event_addon_command(xory)
	if(xory == 'x') then
		get_mob_by_index(get_player()['targets_target_id'])['x_pos']
	elseif(xory == 'y') then
		get_mob_by_index(get_player()['targets_target_id'])['y_pos']
	else
		add_to_chat(160, 'No axis or invalid axis specified.')
	end
end


Now if you type //lua c Position x, x gets passed as the first argument to event_command_addon(), which is called xory, and that argument is then further analyzed.

Let's assume we want to write an addon "MTell" that sends a /tell to multiple people for whatever reason. We want
//lua c mtell name1, name2, ... send <message>

to expand to
input /tell name1 message; wait 2; /tell name2 message; wait 2; ...

where the number of names should be variable. So send should be a token word, that separates the name list from the message.

To achieve that, we would have to define event_addon_command(...) variadic, because the number of arguments can be variable. A possible solution would be this:
function event_addon_command(...)
	local args = {...}
	local names = {}
	local index = 1
	while args[index] ~= 'send' do
		names[#names+1] = args[index]
		index = index + 1
	end
	index = index + 1
	local words = {}
	while args[index] ~= nil do
		words[#words+1] = args[index]
		index = index + 1
	end
	local message = table.concat(words, ' ')
	local commandmsg = ''
	for i = 1, #names do
		commandmsg = commandmsg..'input /tell '..names[i]..' '..message
		if(i < #names) then
			commandmsg = commandmsg..'; wait 2; '
		end
	end
	send_command(commandmsg)
end


Loading/Unloading

Loading and unloading can be used to setup certain variables that are necessary, but it can also be used to define aliases. In our previous, for instance, typing out //lua c mtell in front of the arguments is pretty annoying in itself. So what we can do, is put this in the event_load() event:
function event_load()
	send_command('alias mt lua c mtell')
end


And similarly, remove that alias again when unloading the addon:
function event_unload()
	send_command('unalias mt')
end


Now to send our multitell, all we have to do is //mt playerA playerB send sup guys and it would send sup guys to both of them.

Library functions

A few of the issues mentioned above can be remedied by helper functions, but some of them are kinda ugly to implement. Additionally, you would not want to implement them again in every addon you use. If you find something helpful that more than one addon can use, feel free to add to certain libraries.

Libraries are included by require 'library_name'. This will first search in ./ then in ../libs/ for a file titled library_name.lua. All the functions and variables defined there will be pushed in the global scope and can be used in your addon.

Logging library

Unfortunately, there's no actual debug mode for this, so to debug we need to log as much as possible. Also unfortunately, the add_to_chat(color, msg) function is not very well suited for quick debugging. In addition to being too verbose and requiring a color number, it cannot take multiple arguments, and trying to concatenate something that isn't a string to an output string will result in an error, if not explicitly casted. For that, this library provides a log function to remedy those issues. It automatically converts every argument to a string, concatenates them by a whitespace and outputs the result into the FFXI chatlog:
> require 'logger'
> 
> log('sample', 5, nil, false)
sample 5 nil false


Another problem is printing tables. Trying to convert them to a string will result in an object ID being printed, which is of extremely little use to anyone. For that, this library adds a print() and vprint() (vertical print) method to the table namespace:
> require 'debug'
> 
> t = {1,2,3}
> table.print(t)
{1, 2, 3}
> 
> t = {a=5, b='c', eff=false}
> table.vprint(t)
{
    a=5,
    b='c',
    eff=false
}


String helper

This library provides a few function defined in the string namespace. This means that when this library is loaded, all strings will provide these functions as object methods.

> require 'stringhelper'
> 
> str = 'Random string'
> = str:at(1)
R
> = str:at(2)
a
> = str:at(3)
n
> = str:at(-4)
r
> 
> str = '/a/b/c/d/'
> t = str:split('/')
> = table.concat(t, ', ')
a, b, c, d
> 
> = str:slice(2, 5)
a/b/
> = str:slice(4)
b/c/d/
> = str:slice(-3)
/d/


Table helper

Tables are tricky, because unlike strings, they don't default to the table namespace when trying to access instance methods. Meaning that the table namespace does not automatically link to tables. However, the table helper library introduces a new table notation (called T-tables), denoted by T:
> require 'tablehelper'
> 
> t_old = {1,2,3,4,5}
> = table.concat(t_old, '/')
1/2/3/4/5
> = t_old:concat('/')
Error: attempt to call method 'concat' (a nil value)
> t_new = T{1,2,3,4,5}
> = table.concat(t_new, '/')
1/2/3/4/5
> = t_new:concat('/')
1/2/3/4/5
> 
> t_trans = T(t_old)
> = t_trans:concat('/')
1/2/3/4/5


All you have to do is define your tables with T{} instead of the regular {}. Existing tables (like the mob array or player array) can be converted to T-tables by using T() on them.

In addition to that, this library also provides some useful functions that extend the table namespace:
> require 'tablehelper'
> require 'debug'
> 
> t = T{1,2,3,4,5,6,7,8,9,10}
> ts = t:slice(3, 7)
> ts:print()
{3, 4, 5, 6, 7}
> 
> function double(num)
> 	return 2*num
> end
> tm = t:map(double)
> tm:vprint()
{
    2,
    4,
    6,
    8,
    10,
    12,
    14,
    16,
    18,
    20
}
> 
> function iseven(num)
> 	return num%2 == 0
> end
> tf = t:filter(iseven)
> tf:print()
{2, 4, 6, 8, 10}


Math helper

This is nothing special, just a select few functions added to the math namespace:
> require 'mathhelper'
> 
> = math.round(5.7)
6
> = math.round(math.pi, 5)
3.14159
> 
> require 'tablehelper'
> 
> t = {12.5, -3.4, -5.004, -1.5, 12.5, 98.5, -12.22}
> tsgn = t:map(sgn)
> t:print()
{1, -1, -1, -1, 1, 1, -1}

This post has been edited by Arcon: 16 April 2013 - 05:03 PM

Recycle, stay in school and fight the power..
0

Share this topic:


Page 1 of 1
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users