Writing Addons - Windower/Lua GitHub Wiki
Writing your own Lua addon comes with a few pitfalls. While anyone is free to try it, if you really are not at all familiar with programming, you may wanna put in a suggestion for it to someone else. First of all, you can always find help with that particular topic on the Windower Discord or FFXIAH forums.
The first thing you need to do is learn to use GitHub. GitHub Desktop is the official client for Windows and MacOS, and will make life easier if you want to develop an addon. (If, for some reason, you want to develop addons on Linux, the git command line tool is quite helpful and easy to use, but this guide will not go into depth on how to use it).
If you haven't used Git before, here is the rough idea of how it works:
- The main Windower repository contains every addon that has been published officially. It's the repository the Windower launcher reads from and downloads missing and updated files from.
- You cannot push changes to this repository yourself. Instead you have to fork the repository, i.e. make a copy of it and change/add/remove whatever you think needs to be done in there. This includes making new addons.
- Once you've made your changes (like a new addon), you need to issue a pull request on GitHub.
- A Windower developer will merge your changes, assuming they check out and meet all the requirements (see below).
This is the development process for any GitHub project to which you don't have direct access. For further information, check out the respective help entries for the fork and pull request processes for a more in-depth guide on how each of them works.
Addons also have a certain infrastructure they need to adhere to. Assuming an addon named SomeName, the file structure should look like this:
Windower/addons/somename/data/settings.xml
Windower/addons/somename/somename.lua
Windower/addons/somename/readme.md
In this case, Windower/addons/somename/
is the root directory of the addon. The data directory is optional and should not be directly committed. It should only be created and populated by the addon at runtime, or by the user directly. Everything in the data directory will be downloaded once, but not overwritten by future updates. As such, it's ideal for any kind of user data, such as settings, although a default settings file should not be committed either. Instead, the config
library's default mechanism should be used. More on settings and how to use them later.
Any scripts or static data files for your addon that may require updates over time should go in the addon's root directory.
Whenever possible, you will want to use other libraries to make your coding life a bit easier. There are two ways to handle those. If you think they are specific to your own problem/addon, they should be placed in the addon's root directory. If you think that more people and other addons might benefit from them, place them in the Windower/addon/libs/
directory. LuaCore looks first in the addon's root directory, and then in the general libs directory.
If you are unsure about licensing issues with external libraries, you might wanna contact someone at Windower headquarters during their business hours.
At the beginning of every source file (every file containing Lua code, not XML, JSON or any other data files), you need to include the following disclaimer in a comment at the top:
Copyright © <year>, <your name>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of <addon name> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <your name> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Replace <year>
, <your name>
; and <addon name>
with the respective values. Addons that do not contain the BSD license will not be accepted into the Lua repository.
This guide will not go into depth about how to actually write an addon. For that, there's this guide. This section will merely detail how addons should be set up, to use some of the common features users expect from addons, and that should produce clean code that others can look at and maintain.
Note that for this guide, we will use a sample plugin with the name SomeName.
Some libraries may use certain information about an addon to make them more detailed or efficient. All information about the addon that a library could care about goes in the _addon
global variable.
_addon.name = 'SomeName'
_addon.author = 'YourName'
_addon.version = '3.1.4.1592'
_addon.command = 'sn'
_addon.commands = {'some', 'extra', 'commands',}
To communicate with an addon, you need to enter commands in the command line somehow. This can be done with the following command:
//lua command somename command arg1 arg2 arg3
This, however, is not very convenient to write. For that, you should define either _addon.command
for a single command or _addon.commands
for multiple commands. This registers the commands and automatically unregister them when the addon is unloaded.
Then players can use the following, which will have the same effect as the line above (where cmd
is the registered command):
//cmd arg1 arg2 arg3
Lua itself is very minimalistic in design. It offers very little functionality outside of the most basic things. For many things that are missing, there are libraries available that anyone can use when writing addons.
Since tables are Lua's only (proper) data structure, handling them becomes essential. Sadly, this is also very complex. Many times we need to iterate through a loop to do something to it: find elements, transform elements, count the number of elements, slice a certain range of elements, find the right key that points to an element, etc. To use tables more efficiently to that end, there's the tables
library, that introduces many of those features. To load it, simply type require('tables')
at the top of your file.
It will not only introduce many functions, but also a new table constructor. Here is an extended example in pure Lua:
t = {6, 2, 5} -- Defines a table with three elements
table.sort(t) -- Sorts them in place: t == {2, 5, 6}
print(t[#t]) -- Prints the last element: 6
t[#t+1] = 7 -- Appends the element 7 to the end of the list: t == {2, 5, 6, 7}
t2 = {3, 2, 1, 0, -1} -- Creates a new table
for _, val in ipairs(t2) do -- Iterates through the new table
t[#t+1] = val -- Appends the next element to t
end -- Result: t == {2, 5, 6, 7, 3, 2, 1, 0, -1}
start = 3 -- Start slicing index
finish = 6 -- End slicing index
t3 = {} -- Creates a new table
for key, val in ipairs(t) do -- Iterates through t
if key >= start and -- If they key is in slicing range...
key <= finish then
t3[#t3+1] = val -- ... appends value to t3
end
end -- Result: t3 == {6, 7, 3, 2}
-- Prints the result:
table.sort(t3)
print(table.concat(t3, ', ')) -- Result: "2, 3, 6, 7"
The tables
library introduces T-tables, which aren't any different from regular tables, but allow direct indexing of table methods. This, along with the numerous helper functions, turns the above code into this:
t = T{6, 2, 5} -- Defines a T-table with three elements
t:sort() -- Sorts them in place: t == T{2, 5, 6}
print(t:last()) -- Prints the last element: 6
t:append(7) -- Appends the element 7 to the end of the list: t == T{2, 5, 6, 7}
t2 = T{3, 2, 1, 0, -1} -- Creates a new T-table
t:extend(t2) -- Adds all elements of t2 to t: t == T{2, 5, 6, 7, 3, 2, 1, 0, -1}
start = 3 -- Start slicing index
finish = 6 -- End slicing index
t3 = t:slice(start, finish) -- Returns a table slice of t: t3 == T{6, 7, 3, 2}
-- Prints the result:
print(t3:sort():concat(', ')) -- Result: "2, 3, 6, 7"
The full set of T-table functions, along with their documentation, can be found here.
Same as tables, strings were amended significantly. To use this library, type require('strings')
. Here are some examples:
str = ' This is a test string.'
str = str:trim() -- Removes leading and trailing whitespaces
for c in str:it() do -- Iterates over every character in the string
print(c) -- Prints every character on a single line
end
if str:startswith('Bla') then
-- This will not be executed
elseif str:endswith('ng.') then
print(str:at(3)) -- Will print the third character in the string: 'i'
end
print(str:slice(2, 6)) -- Returns characters 2 through 6: 'his i'
print(str:slice(10)) -- Returns every character starting from position 10: ' test string.'
print(str:slice(1, -4)) -- Returns everything until the fourth from last: 'This is a test stri'
print(str:slice(-10)) -- Returns everything from the tenth from last: 'st string.'
print(str:enclose('</', '>')) -- Encloses the string in the provided strings
str = '/a/b/c/'
print(str:split('/'):concat(', ')) -- Splits the tokens by '/', ignores empty strings.
-- Joins them again by ', ': 'a, b, c'
str = '8'
print(str:zfill(3)) -- Fills with leading zeroes, until a max length of 3: '008'
names = {'this', 'that', 'what'}
if str:isin(names) then
-- names does not contain str
elseif str:isin('2', '4', '8', '16')
-- str found in the provided list, will be executed
print(('success!'):upper())
end
File input/output was also facilitated with the files
library. Unlike the previous two, you can't just require this module, because it doesn't modify any global namespaces like tables and strings. Instead you need to load it by assigning it to a variable, like this:
local files = require('files')
local f = files.new('test.txt')
f:write('Something')
f:append('... or other')
f:exists()
lines = f:readlines()
lines2 = files.readlines('test.txt')
require('tables')
if lines:equals(lines2) then
lines2:append('last line')
f:writelines(lines2)
end
The logging module, called logger
, is one of the few libraries that can be modified with a settings file. The file called logger.xml
contains settings to provide a regular log color, an error color, a warning color and a notice color. Using those functions to print will usually output into the chatlog directly (except for flog
, which always prints to file), unless logtofile
in the settings is set to true
.
It introduces the following functions into the global namespace:
log(a, b, c, ...) -- Prints all provided variables
error(a, b, c, ...) -- Like log, prefixed with 'Error: ' in the specified color
warning(a, b, c, ...) -- Like log, prefixed with 'Warning: ' in the specified color
notice(a, b, c, ...) -- Like log, prefixed with 'Notice: ' in the specified color
flog(f, a, b, c, ...) -- Like log, but always to file
-- Will print to the specified defaultfile if f is set to nil
table.print(t) -- Prints a table into one line
table.vprint(t) -- Prints a table in a vertical format with proper indentation
If the global _addon.name
variable is set, it will prefix all log messages with that value, so it's obvious which addon the print came from.
The module to handle settings management is called config
. Like the file helper, it has to be loaded into a variable to be accessed. config
can be called with a file name, but if omitted it will search for settings.xml
in the data/
directory. This file will be loaded permanently (unless overwritten) during the addon's runtime and settings can be saved to file again with it.
Since the settings
file should not be committed, using a settings file should not be a replacement for default settings. These should always be hardcoded into the addon. Below is a complete example that will either load an existing settings.xml
(with settings for variables mode
, color
and size
) in the data/
directory, or create a new one with that content, if none exists yet:
require('tables')
config = require('config')
local defaults = T{}
defaults.mode = 'a'
defaults.color = 'red'
defaults.size = 2.25
settings = config.load(defaults)
To save settings to file, there are two modes, a global and a per-character local mode. To save settings for one character only, call settings:save()
, where settings
contains the current settings structure. To save it globally for all characters, use settings:save('all')
.
If the file exists, but a setting doesn't, the file will be amended accordingly. That way, new settings can be introduced to an addon, and upon load, the default value for that setting will always appear in the file.
Continuing from the above example:
settings.mode = 'x'
settings.color = 'blue'
settings.size = 3.0
settings:save('all') -- Saves these settings for all characters
settings.size = 2.5
settings.font = 'arial'
settings:save() -- Saves existing global settings for the current character only
This would create the following XML (assuming character name Xcloudseferotx):
<?xml version="1.1" ?>
<settings>
<global>
<mode>x</mode>
<color>blue</color>
<size>3.0</size>
<font>arial</font> <!-- setting saved globally in first instance -->
</global>
<Xcloudseferotx>
<size>2.5</size>
</Xcloudseferotx>
</settings>
Lua's namespaces are something that addon writers generally won't have to worry about, but something that's crucial to library module writers. It's generally desirable to isolate namespaces, so that variable names and symbols from different modules don't conflict.
Lua supports only one kind of scoping, and that's nesting tables. The following are identical:
t.a
t['a']
This holds true for functions as well, as they're nothing but callable variables, and thus also extends to the :
syntactic sugar notion, meaning the following are all equivalent:
t.a(x)
t['a'](x)
x:a()
This is especially annoying if you have a table which you want to define methods on. Since methods are nothing but keys, it means you can't have arbitrary nameable tables to emulate objects as known from other object-oriented languages.
This is already a problem in the aforementioned config
library. It uses settings:save()
to save the current settings. However, as we just saw, save
is nothing more than a key in the settings
table. So if someone defined a settings-variable named "save", settings:save()
wouldn't work anymore, and they'd have to call config.save(settings)
manually.
While some of these problems cannot be fixed, others can. The solution for most libraries is to not dump anything in the global namespace, unless it amends an existing namespace such as table
or string
. Instead, all variables and methods can be declared local, and the result returned, like so:
local foo = {}
function foo.m()
-- Something
end
return foo
Then the including file would not just require('foo')
, but instead assign it to a variable:
f = require('foo')
f.m()
Global namespaces are also the only way to resolve some circular dependencies within Lua code. For example, if two modules want to require each other, the interpreter enters an infinite loop. It can normally resolve multiple includes, because it saves previously loaded files in an internal table, where it can look it up. However, it only saves a file there once it's fully loaded, meaning it will still try to resolve the circular reference.
The only way to prevent that is to globally store a variable indicating a module being currently loaded. Necessarily, that declaration has to come before the circular inclusion.
File A:
_libs = _libs or {}
_libs.A = true
_libs.B = require 'B'
File B:
_libs = _libs or {}
_libs.B = true
_libs.A = require 'A'
This seems kinda roundabout, and, like many things in Lua, could have been handled better. But it works, and will prevent circular loading. It has to be adjusted slightly, if loaded libraries are supposed to be stored in variables. For reference on how to handle that, check some of the current files in the /libs
directory.
Another useful global namespace that was mentioned before is _addon
, where addon information can be stored. logger
, for example, uses it to automatically prefix output based on the addon it's from, to quickly identify what's printing to the chatlog.
Lua's metatables dictate table behavior, and as such the behavior of all objects except for primitives (boolean, numbers, strings). Metatables contain settings about the namespace, and how objects with that metatable should react to certain operators. As such, it's also used to overload operators. Metatables themselves are just regular tables that contain certain keys. That's why they can be stored in and passed as variables like any other data type.
As an example, in the tables
module, the addition operator was overloaded to allow combining tables:
function table.extend(t1, t2)
-- Code to merge tables
end
mt = {__add = table.extend} -- This is the definition of the metatable
t1 = setmetatable({1,2,3}, mt)
t2 = setmetatable({4,5,6}, mt)
t = t1 + t2 -- t now contains {1,2,3,4,5,6}
This can be used for a number of interesting hacks. For example, you can overload the __index
operator, which tells the interpreter in which namespace to look when indexing the variable. This is used to emulate the string behavior for tables in the tablesr
module, so table can be accessed like strings. Excerpt from the code:
-- Constructor for T-tables.
-- t = T{...} for explicit declaration.
-- t = T(regular_table) to cast to a T-table.
function T(t)
if t == nil then
return
end
-- Sets T's metatable's index to the table namespace, which will take effect for all T-tables.
-- This makes every function that tables have also available for T-tables.
return setmetatable(t, {__index = table, __add = table.extend})
end
t = T{'a','b','c'}
t:concat('/') -- Results in "a/b/c"
Due to its nature to modify behavior, this is what is needed to emulate object inheritance and is thus Lua's way to allow for an object-oriented paradigm.
People familiar with functional programming know how powerful it can be when used correctly. This section will not discuss (or even advocate) functional programming in itself, but some techniques that resulted from it and are commonly used in functional programming languages, and increasingly outside of such.
The functional library is called functions
and loads its functions into various namespaces. Among others, it modifies the string
and table
namespace, and adds a boolean
and functions
namespace.
Three of the most well-known functional techniques to operate on lists or list-like structures (any iterable) are the map, filter and reduce functions (excerpt from the functions
module):
-- Applies function fn to all elements of the table and returns the resulting table.
function table.map(t, fn)
-- Code
end
-- Returns a table with all elements from t that satisfy the condition fn, or don't
-- satisfy condition fn, if reverse is set to true. Defaults to false.
function table.filter(t, fn)
-- Code
end
-- Returns the result of applying the function fn to the first two elements of t, then
-- again on the result and the next element from t, until all elements are accumulated.
-- init is an optional initial value to be used. If provided, init and t[1] will be
-- compared first, otherwise t[1] and t[2].
function table.reduce(t, fn, init)
-- Code
end
Having defined those three as stated, here is what we can do with them:
str = 'your mog locker lease is valid until 2014/4/6 8:18:08, kupo.'
t = str:split(' ')
log(t:map(ucfirst):sconcat())
-- Outputs: 'Your Mog Locker Lease Is Valid Until 2014/4/6 8:18:08, Kupo.'
t = T{1,2,3,4,5,6,7,8}
even = t:filter(math.even) -- Where math.even(x) == return x%2 == 0
odd = t:filter(math.odd) -- Where math.odd(x) == return x%2 == 1
-- Outputs: even == {2,4,6,8}; odd == {1,3,5,7}
even_sum = even:reduce(math.sum) -- Where math.sum(x,y) == x+y
odd_mult = odd:reduce(math.mult) -- Where math.mult(x,y) == x*y
-- Outputs: even_sum == 20; odd_mult == 105
As with all languages that treat functions as first-class objects, it's possible to operate on functions themselves. This allows for some handy shortcuts. For example, we can partially apply a function to a number of arguments. For example, if we wanted to add 10 to every number in a table, and then append '!' to every word in a sentence, we could do this:
t = {1,2,3,4,5}
t:map(functions.apply(math.sum, 10)) -- Where math.sum adds two numbers
-- Outputs: {11, 12, 13, 14, 15}
t = ('this is sparta'):split(' ')
t:map(functions.endapply(concat, '!')):sconcat() -- Where concat concatenates two strings
-- Outputs: "this! is! sparta!"
Similarly, we can pipe function output from one function to another:
names = T{'AIKAR', 'AZARIL', 'CLIFF', 'STARHAWK', 'TAJ'}
names:map(functions.pipe(string.ucfirst, string.lower))
-- Outputs: {'Aikar', 'Azaril', 'Cliff', 'Starhawk', 'Taj'}
functions.pipe(fn1, fn2)(...)
is equivalent to fn1(fn2(...))
, meaning the functions are applied right-to-left. As such it corresponds to the mathematical definition of function composition.
Unfortunately, using these methods can get ugly when combined:
t = table.range(65, 65+26-1) -- Numbers 65 through 90 (A-Z)
functions.pipe(string.char, functions.pipe(table.unpack, functions.endapply(table.filter, math.even)))(t)
-- Outputs: BDFHJLNPRTVXZ
That's where the previously mentioned operator overloading comes in handy. Function piping uses the concatenation operator (..
). Partial application uses a +
and -
for applying from the front and the end respectively. This reduces our previous code significantly, and even allows multiple chaining:
t = table.range(65, 65+26-1) -- Numbers 65 through 90 (A-Z)
(string.char..table.unpack..table.filter-{math.even})(t)
-- Outputs: BDFHJLNPRTVXZ
str = 'This is a \\\\x1F\\\\x05string with some\\\\x1E\\\\x01 color \\\\x1E\\\\xA1information \\x1F\\x80attached.'
str:gsub('\\x([%w%d][%w%d])', string.char..tonumber-{16})
-- Outputs: (The above string, with the literal color codes translated to ingame colors)
Similarly, we can rewrite the previous string operation:
names = T{'AIKAR', 'AZARIL', 'CLIFF', 'STARHAWK', 'TAJ'}
names:map(string.ucfirst..string.lower)
-- Outputs: {'Aikar', 'Azaril', 'Cliff', 'Starhawk', 'Taj'}
This deserves special mention, because the function string.ucfirst..string.lower is perfectly suited to create character-names in the ingame format, which has all letters in lower case except the first, which is always in upper case. This is useful for name comparison, since that is case-sensitive.
And finally, there's function negation, used with functions.negate
, which can be used with the unary minus operator. This can be useful to reverse a filter, for example:
t = T{1,2,3,4,5,6,7,8}
evens = t:filter(math.even)
odds1 = t:filter(math.odd)
odds2 = t:filter(functools.negate(math.even))
odds3 = t:filter(-math.even)
odds1 == odds2
-- Outputs: true
odds2 == odds3
-- Outputs: true
Sometimes you have a custom function that you want to apply, but can use it inline and redefining it outside would be bothersome. Lua provides a way to do that with anonymous functions, often denoted by a lambda keyword in other languages.
Assume you want to filter all elements that start with an 'm' from a table:
t = T{'meh', 'foo', 'bar', 'muh', 'baz', 'meow'}
t:filter(function(x) return x:startswith('m') end)
-- Outputs: T{'meh', 'muh', 'meow'}
Note that in simple cases, like this, it could have been handled with a function operator, as mentioned before:
t = T{'meh', 'foo', 'bar', 'muh', 'baz', 'meow'}
t:filter(string.startswith-{'m'})
-- Outputs: T{'meh', 'muh', 'meow'}
However, there's more complex examples of such anonymous function usage that are harder to handle with function operators:
t = T{23, 65, 531, 123, 564, 231, 32, 75, 20, 12, 532}
t:filter(function(x) return x > 50 and x % 3 == 0 end)
-- Outputs: T{531, 123, 564, 231, 75}
t = T{23, 65, 531, 123, 564, 231, 32, 75, 20, 12, 532}
t:map(function(x) if x < 10 then return 100*x elseif x < 100 then return 10*x else return x end end)
-- Outputs: T{230, 650, 531, 123, 564, 231, 320, 750, 200, 120, 532}