I recently had the impulse to learn lua and addon programming for ESO. Part because I missed functionalities, part because I do enjoy a programming challenge/new language and part because I thought I could make some stuff better then the orignal programmer (if possible within the constraints of the API).
I always learn quite a bit in a short time and thought I write down what I learned thus far.
This is not supposed to be a full Toutorial or Syntax reference (for that there is the rest of the internet). It's aimed towards fellow programmers for whom only Lua and/or "Lua in ESO" are new things. I cover some stuff I felt incompletely explained in the Toutorials on ESOUI.
Variables and Scope:
Variables in Lua are truly Global. They are shared not only between all files in you addon, but all addons loaded into the client.
It does not even stop at addons, every function provided by the API is also just a Global variable, shared between the API and all Addons. So only expose or overwrite something global if you really know you want it exposed/what you are doing.
Global Variables are never declared. They are assigned. If you try to access a variable that has not gotten a value, the value is "nil" (like null or nothing in other languages). Hence to "delete" a global variable, assign it nil. If you mispell a variable or function name, the results will be a nil (or the value of a global variable that has been written this way). So mind your spelling (and avoid globals where you can).
Think of global variables as a giant, string indexed table (see table below) to wich both addons and the API have full read/write access and nobody expects "his" values to have been overwritten by somebody else (or at least only in a meaningfull way so it still does the normal work).
To cite Linux after elevating to root: "With great power comes great responsibility."
Local Variables have a scope limited to whatever block they were declared in. Local Varriables are the only ones that have to been "declared" in the classical sense.
At biggest scope will still be in the entire File they are declared in (if declared outside of any function it's effective scope is the .lua file). At same name local variables always take precedence before global ones. And more local ones from those of "higher" blocks.
Make it a habit to write local in front of every variable. Don't even think about it. If you do need it global still mark it local now and explicitly expose it via the Namespace construct at the end of the file (see table).
Function Parameters and running numbers declared in a for are automatically local to the block they belong to, but if you need a temp variable or even a seperate counter for a more complex loop forgetting local can cause untold havoc (because more then one piece of code might try to access the same global variable). It certainly did for me.
http://lua-users.org/wiki/ScopeTutorialTypes:
Variables have no fixed type. At any given time any Variable could be a int, a float, a string, a Function Reference, a Reference to a complex type (like a Saved Variables Access instance) or even a table - wich is just a collection of normal variables with indexers (each of wich can be any of the above, including another table).
People who programmed in PHP and other languages without type decalrations will find this aspect familiar.
That also means you need a good tool for printing variables that can iterate over all the elements as deep as needed. A way to dump the actuall values instead just checking the asumed values. Personally I like kikito's inspect for displaying the content of a variable for debugging:
https://github.com/kikito/inspect.lua
Note that d() (see debugging and libDebug) seems to have similar abilities, but they seem slower/might lag behind.
Functions:
You are not declaring a function named "X". You declare a variable named "X" and assign it an Anonymous function.
Functions do know via wich variable name they have been first assigned for debugging purposes so it may seem as if they have a name. But in realiy they do not.
This also means that handing a function as parameter is easy - they are after all just another variable type.
As functions are handeled in variables a function must have been declared and assigned before it is used (otherwise the value of the variable is still nil). Keep that in mind when ordering .lua files in your Textfile and functions in your lua file: If it needs to be assigned before your Code file accesses it, it has to come before it in the list (but keep the difference between .lua file and the OnLoad/Initialsiation events in mind)
Functions can take as many parameter as you want.
They can be called with anything from the full list of arguments to no argument (non-assigned arguments are supposed to be nil, extra arguments are ignored)
They can also return as many parameters as you want. They do not return a table, but instead just spew out the entire list. Those you do not assign/use directly are ignored. In some cases only the first return value is used by default, but when using one function as argument for another the return values are matched to the parameters (String.Format is one function that returns more then one value).
Table:
This is the only collection type and is also often used to simulate structures and classes. And namespaces during global exposure.
Tables are the only type that has to be initialized before it can be used, but you can easily initiate a emtpy one and assign it as many values as you like in a loop. You can also use array intialisers to set core values here and now.
There is no fixed type for indexers and no fixed size.
You can have int indexers or string indexers and mix both indexers on the same table. If you want to remove something from the table, assign nil to that index (as you would with a global/local variable). The number 1 and the string "1" are distinct for indexing purposes.
Int Indexer:
Also called the list or array style (with list being the more precise term)
Int indexes in Lua always start with 1. 0 can still be assigned and read as any other index, but as a rule of thumb you should avoid starting anywhere else but 1. You only end up having to differentiate between "array I declared manually" and "arrays the system or array initialisers created", wich does not aid your productivity.
You can get the highest int indexer for a table by putting a # before the table name. (#myTable) wich is helpfull for for-looping over the table. Keep in mind there might be nil values between 1 and #myTable and string indexes values cannot be iterated over this way (for those see the internet)
Note: No mater how you layed them out, # will not work on the top level of settings objects (See persisting data below)
String indexer:
Also called the "Dictionary Style". As said earlier enumerating over them is harder the itterating over the int indexed part of a table
string indexer Syntax Sugar:
Writing myTable.SomeStringIndex is equal too myTable["SomeStringInex"]. This syntax sugar is core of how you simulate structures or namespaces with string indexes tables. But keep in mind that no mater how it looks, it is still a string index for a table. So expect index related exceptions if you write something wrong.
Hybrid table:
As said before, you can mix string and int indexers. Some call the Hybrid tables, but otherwise there is no difference between a pure int or pure string indexed table.
namespaces via tables:
As all Addons and even the API use the same global values and there is no special namespacing system, having unqiue names for your exposed functions can be tricky. But using tables it is quite possible to have a globally unique name (that you can also change as nessesary without affecting he internals of your code)
http://www.lua.org/pil/15.2.html
Metatable:
There are no classes in Lua. While the XML UI has a concept of Inheritance (at least it seems so), there is nothing like this in Lua.
However you can simulate classes via Metatables.
Metatables can be assigned to table variables as well as primitive types.
What is the place of a lua file?
For all intents, .lua files are really just very large functions whose local variables are accessible by local functions even after the code has finished. Global variables stay around anyway (till they are overwritten by somebody else). Think of them as the Constructors of your addon. Every event or callback you define inside it will be able to access anything else inside the file (that is why local is a good idea).
This includes that you can just cancel thier execution with a return, like any other function (libStub makes use of that)
As opposed to that, the OnAddonLoad and OnInitilaised events are explicitly raised after all constructors/addon.lua files have already run. As such your will always register the on-load events off all addons, wheter they have been loaded before or after yours (because loading has long passed at this time). So make sure to filter for your addons event. Using the
if addonname ~= "<Your Addons Name>" then return end
cosntruct is the fastest way.
When is the file compiled?
Every time the UI is loaded, all addons are read in and executed (unless they have been disabeled in the menu)
The UI is loaded every time you log in or give the /reloadui command, via chat, Addon menu or a Addon (yes, there is a Addon for that)
So all you need for "recompilation" of a changed file is saving everything and hitting /reloadui
As long as you do not reload the last loaded version will stay in memory.
The UI XML:
People who worked with XAML/WPF before will instanly find familiar concepts here. It's a XML based markup language for UI Elements. Unfortunately it shares two other things with XAML (early versions): The most precise debug message you will get is "Something went wrong while parsing the XML" (without even telling you the line) and mispelled Markup is silently ignored, wich can result in all kinds of "X does not work" cases. Plus sometimes some values (like moveable) depend on other values (like mousenabeled) to be set, but nothing in the documentation says so.
95% of all "nil where value expected" here mean: you tried to assign an event in XML, but forgot to name wich event code should handle it. Or the events name in XML and Lua do not match. Or the event is not properly exposed globally under that name.
Debugging:
The fastest way to give something out into the chat is using the function d() (propably for Debug or Dump, but some call it the "loot message" too).
This will put the data as yellow text into your chat windows current tab. The input will not be shared between tabs - wichever is active will take it, all others will ignore it. It's as close as any addon can get to "sending a message to chat" (as addons cannot send chat messages).
Unfortunately by default it does not seem to work during the Initialistion event as the chat is not yet loaded (at least as far as displaying d() messages goes) and there is no buffering in the build in version of d(). As part of it's functionality, libDebug overwrites d() to implement buffering, as long as "Show Pre-Init Messages" is set to true.
Stucture of the Addon-Manifest:
This explains it all
http://wiki.esoui.com/Addon_manifest_(.txt)_formatPersisting data between Character sessions and sharing data between Characters (account wide data):
The game provides a SavedVarriables system:
http://wiki.esoui.com/AddOn_Quick_Questions#How_do_I_save_settings_on_the_local_machine.3F
They are stored in a folder at the same level as the addon folder, in a .lua file named after the addon
Using them requires the following steps:
1. you add the name of the settings to your Manifest file. Remember this name for later
2. you declare a local variable at the top of your .lua file. This is where the settings instances will be stored and where they will be accessed by the functions of your addon. Note that you cannot yet assign the value. You have to wait till the Loaded Event of your addon.
During said loading event you do:
3. You declare the default value to be used. While this might seem a useless overhead for most runs, this is how this part works. The default values come before you even try to read the settings from the disk!
4. You try to access the Variables using ZO_SavedVars:New (this constructs the Settings Object), giving the name you specified in the manifest (Step 1), a version number (start with 1) and the default value you created at step 3
If there are values and their version matches up, they are loaded from the disk.
If there are no values or there are values but with wrong version, the default is used. From now on it behaves as if there always had been the values on the disk
Note: When inspecting the value of the object it seems that the default values are added under the metatable.
If you want Character specific saved variables, use ZO_SavedVars:New(). You can share the same Step 1 name, as the file will automagically stuctured based on gameaccount-name and Character Name.
If you want account wide variables (including sharing information between characters when you cannot access other characters data at runtime), use ZO_SavedVars:NewAccountWide() instead.
Profile and Namespace are optional and can be nil, but I prefer to use different namespaces for global and character variables (unless I just make my own profile management in the global saved variables). All this does is add another "layer" of indexes to the saved var table.
Deleting/new default for settings files:
If something in the structure or default values of your SavedVariable changed, there are only two ways to refresh it:
1) Log out or disable the addon and /reloadui. Wait some time till the values have been written to disk (there is some caching in place^so it might take a few seconds). Then delete file. Relog/reactivate addons afterwards.
2) Change the version, save, /reloadui, so the current values are ignored and default is used
For obvious reasons I prefer 2 during development. But deployed versions should "stick" to thier format unless something major changed.
Structure of a .lua file (while nothing is nesseary to be in this order, it seems a very simply and understandable order and helps read others files):
--libStub block, if you use it
--A bunch of (local) variables that has to be shared across functions
--Needs to include variables used for saved variable objects
--Function1, cannot use any function declared after it but any variable above it
--Function2, wich can use Function1
--Function3, wich can use Function1 and Function2
--[...] (A bunch of functions omitted)
--Function to handle the EVENT_ADD_ON_LOADED Handler
--Here you regsiter all Event handlers
--Especially the function of the EVENT_ADD_ON_LOADED
--Exposure of functions in global variable via namespace pattern
--the optional return from the Namespace pattern
Libraries:
There is the old saying to "never reinvent the wheel".
And nothing can be as productive as "using the code of other peoples" to solve a problem.
Libraries is .lua code specifically designed to be used by other addons. While they might have some standalone features, their main purpose is to be used in other addons.
I mostly used the ones for esoui.com, wich can be found here:
http://www.esoui.com/downloads/cat53.html
Here is list of libraries you might find helpfull:
libStub:
http://www.esoui.com/downloads/info44-LibStub.html
You can easily have multiple instances and even version of the same library in your addon folder. Not only bring several addons them along, you might have a standalone copy. In this case wichever version is last executed would be the one used (as all addons access the same global variables for exposure, so it's "last come last served"). Unless the Library uses libStub. It's a library with the sole purpose of being a library version tool.
Using the libStub block other libraries .lua file will first check if a newer or the same version has already been loaded/executed. If that is the case, the .lua file is just skipped (via an early return).
If not (no or older version) the .lua file is executed. This makes certain that only the most recent version is in memory and that if a dozen addons bring the same version along only one of them is executed.
For optimal result also add the libraries to your "OptionalDependsOn". If the user has a standalone version installed that one is much more likely to be up to date. While it would still end up "on top" eventually, the lesser versions might still be executed costing unessesary time.
libStub uses two values to identify a library: The major and minor. It they have different major values, they are different libraries. If they have different minors, they are just different versions of the same library. You only change the major if something changed with the exposed functions (like names or wich functions are provided) or if otherwise backwards compatibility is not certain.
libAddonMenu:
http://www.esoui.com/downloads/info7-LibAddonMenu.html
Do you like to add a addon settings page to the game menu? While you could write the code yourself, there is a library for that wich takes care of the plumbing.
I think the Documentation is good enough that it does not need explanations.
libDebug:
http://www.esoui.com/downloads/info348-LibDebug.html
The first rule of all debugging is to figure out what the real value of a variable is (because when you have bug it is always not the value you asumed).
The second rule is to have error messages in a copy/pasteable format (this can be especially true for normal users)
Aside from changing d() so output can be buffered (and includes d() messages that come in before the chat is ready) it also provides a way to replace the error dialog with a version that allows copy to clipboard for exceptions. (Note that I had some issues with it swallowed messages, so I only use it if I really need this part).
Final words:
Keep in mind that addons can only do what the API allows them to do. As with all programming (short of drivers or OS) your code is only guest on the computer it is run on. If your addon does not play well with the other guests, takes up too much time/resources or tries to do stuff a guest should not do, it will get kicked out.
If you see something that is obviously ineffective/overly complicated built, chances are high that this is the only way this thing can be build at all. At least till the API is revised.