-
Notifications
You must be signed in to change notification settings - Fork 20
Addon Development
This page assumes you have read and understood the previous pages:
Here we will create a sample addon with commonly used features to guide you through the process of creating an addon. It will feature a simple display of some character stats, controllable via commands.
Make sure you enable the debug console in the Windower > Advanced menu when editing a profile. This helps significantly with debugging errors.
The debug console functions as both input and print output. As for input, it can handle everything the FFXI chat log does, so it handles both FFXI and Windower commands. You cannot, however, just type an invalid command and have it go to the default chatmode channel. It only accepts commands, either Windower or FFXI commands. Unlike v4's console it requires a single slash, same as commands in the FFXI chat log.
As for output, it prints a lot of error information any time an error happens, which is crucial for debugging.
It is also the default output channel for Lua's print
command.
Go to the packages folder and create a new folder with the name of the addon (=test
). The folder needs at least two files:
test/
|- test.lua
|- manifest.xml
The manifest needs to look like this:
<package>
<name>test</name>
<version>1.0.0.0</version>
<type>addon</type>
<dependencies/>
</package>
The name
element must match the addon name. The version
needs at least the major and minor version, optionally also the revision and build numbers. The type
needs to be addon
. Other known values for packages are library
and service
, but they deserve their own development guides as they follow some different rules. This page will focus only on addon development.
The dependencies
element is used to define other libraries and services this addon relies on. Ideally services should not be accessed by addons at all, only by libraries written specifically for them, so this should really only list libraries that the addon depends on.
Only the libraries defined as other packages need to be listed as dependencies. Internal libraries like command
and ui
do not need to be listed as dependencies and are always available.
Note that the manifest is loaded only once. If, during development, you add a new dependency you will need to reset the packages internally using the /pkg reload
command. After that you can reload. This goes for creating new addons as well, so if you only now created it and are already in-game, type /pkg reload
and then you can load your addon by typing /load test
. It should display a message that it loaded the addon.
> /pkg reload
> /load test
loaded test
To make sure it's really loaded let's give it something really simple to do:
print('testing...')
Then /reload test
to get the desired output:
> /reload test
unloading test
testing...
loaded test
Windower 5 has an immediate GUI framework, unlike Windower 4 and many other GUI frameworks out there. For a developer that means there is a displaying function which is called on each frame which dictates which objects live where and how they appear.
local ui = require('core.ui')
ui.display(function()
ui.text('Testing!')
end)
Reloading the addon now will simply display Testing!
in the top-left of the screen. To adjust its position we just use the ui.location
function:
local ui = require('core.ui')
ui.display(function()
ui.location(300, 30)
ui.text('Testing!')
end)
This will print the same as before, at the position (300, 30).
But now we want to print something useful, say the current character's name, HP, MP and TP. That info is available in various places, for example the player
library. To be able to use it we first need to add it as a dependency to our manifest:
<package>
<name>test</name>
<version>1.0.0.0</version>
<type>addon</type>
<dependencies>
<dependency>player</dependency>
</dependencies>
</package>
Note: If you do not have a dependency already installed you will need to install the dependency manually. This is done by typing:
/install <dependency name>
i.e./install player
Now we need to tell the package manager that the manifest has changes - /pkg reload
. Now we can go back to editing the addon:
local ui = require('core.ui')
local player = require('player')
ui.display(function()
ui.location(300, 30)
ui.text(player.name)
end)
Now if we're logged in, this will display the player name. Once we log out, it will error. That's because player.name
becomes nil
when logging out and the function expects a string. Notice that the value of player.name
was automatically updated on logout, unlike in Windower 4. We can easily work around that though by using player.name or '-'
. That way the player name will be displayed if available, and a simple dash if not.
We can now expand it to display more information:
local ui = require('core.ui')
local player = require('player')
ui.display(function()
ui.location(300, 30)
ui.text(player.name or '-')
ui.location(300, 60)
ui.text(tostring(player.hp) or '-')
ui.location(300, 90)
ui.text(tostring(player.mp) or '-')
ui.location(300, 120)
ui.text(tostring(player.tp) or '-')
end)
Not pretty, but that's not what we're here for.
Addons will often need to be controlled by user commands. This can be achieved with the command library. To register a command load the module and write:
local command = require('command')
Now let's assume we want to handle visibility and position of each piece of information via commands. Some sample commands that people might want to use are:
/test hp hide
/test mp show 500 40
/test name show
/test hp_percent show 500 60
First we need to adjust our display function to check the visibility and position before displaying an item:
local ui = require('core.ui')
local player = require('player')
local options = {
name = { x = 300, y = 30, show = true },
hp = { x = 300, y = 60, show = true },
mp = { x = 300, y = 90, show = true },
tp = { x = 300, y = 120, show = true },
}
ui.display(function()
for key, info in pairs(options) do
if info.show then
ui.location(info.x, info.y)
ui.text(tostring(player[key] or '-'))
end
end
end)
Now the display function looks up all the values and their positions dynamically, so to adjust it with the command handler, we just need to adjust the values in that table.
To be able to catch /test
commands Windower 4 used __addon.command = 'test'
at the beginning of the file. In Windower 5 we can achieve that with the following line:
local test_command = command.new('test')
Now we have configured the /test
command, but we don't have any handlers registered yet. Windower 5 supports a number of convenience functions for argument checking, so we can save a lot of boilerplate compared to Windower 4:
test_command:register(handle_field, '<field:string>', '<visibility:one_of(show,hide)>', '[x:number]', '[y:number]')
This line defines the function to call (handle_field
, which we haven't implemented yet) as well as the number and format of the arguments. The first argument is required (indicated by the <>
) and of string
type. The second argument is also required and one of show
or hide
. The third and fourth arguments are both optional (denoted by []
) and of numeric type.
So that helps us define the handler:
local handle_field = function(field, visibility, x, y)
local info = options[field]
info.show = visibility == 'show'
if x and y then
info.x = x
info.y = y
end
end
And that's it! Well, almost... if we enter the last command of the ones we mentioned above it would error, because hp_percent
is not a known value. To fix that we can simply create it when it's accessed:
local handle_field = function(field, visibility, x, y)
local info = options[field]
if not info then
info = { x = 0, y = 0} -- set default positions because these are optional and may be nil
options[field] = info
end
info.show = visibility == 'show'
if x and y then
info.x = x
info.y = y
end
end
And with that, everything works as expected. Now it supports adding any value that is available in the player table dynamically via commands.
We would not want to enter these settings every time we log in. And adding those commands to an initialization script is also annoying. So instead we'll try to make it remember our settings. That's where the settings
library comes in. At first we need to adjust the manifest for the new dependency:
<package>
<name>test</name>
<version>1.0.0.0</version>
<type>addon</type>
<dependencies>
<dependency>player</dependency>
<dependency>settings</dependency>
</dependencies>
</package>
Now we can require('settings')
to get access to the settings library. The change from our current code to the one with saved settings is rather trivial:
local settings = require('settings')
local defaults = { -- Change this from `options` to `defaults`
name = { x = 300, y = 30, show = true },
hp = { x = 300, y = 60, show = true },
mp = { x = 300, y = 90, show = true },
tp = { x = 300, y = 120, show = true },
}
local options = settings.load(defaults) -- This loads character-specific settings based on the provided defaults
And with this the settings are persisted to file. But we need to manually save after each write:
local handle_field = function(field, visibility, x, y)
local info = options[field]
if not info then
info = { x = 0, y = 0} -- set default positions because these are optional and may be nil
options[field] = info
end
info.show = visibility == 'show'
if x and y then
info.x = x
info.y = y
end
settings.save()
end
And with this we have character-specific settings that persist across logins.
There is more to be done, but this is as far as the example needs to go. It should give people an idea of how to code for the Windower 5 API. It didn't cover all possibilities even within the subset of features we used, but for more information check the docs of the individual libraries. You should also examine existing addons to check how they work, that is always a great resource for learning.
Here a final recap of our entire addon that we wrote during this guide:
local settings = require('settings')
local command = require('command')
local player = require('player')
local ui = require('core.ui')
local defaults = {
name = { x = 300, y = 30, show = true },
hp = { x = 300, y = 60, show = true },
mp = { x = 300, y = 90, show = true },
tp = { x = 300, y = 120, show = true },
}
local options = settings.load(defaults) -- This loads character-specific settings based on the provided defaults
local handle_field = function(field, visibility, x, y)
local info = options[field]
if not info then
info = { x = 0, y = 0} -- Set default positions because these are optional and may be nil
options[field] = info
end
info.show = visibility == 'show'
if x and y then
info.x = x
info.y = y
end
settings.save()
end
local test_command = command.new('test')
test_command:register(handle_field, '<field:string>', '<visibility:one_of(show,hide)>', '[x:number]', '[y:number]')
ui.display(function()
for key, info in pairs(options) do
if info.show then
ui.location(info.x, info.y)
ui.text(tostring(player[key] or '-'))
end
end
end)
- Background and Architecture
- Windower Data Locations
- Code Standards and Guidelines
- Addon Development
- Windower Commands
- Packet Tutorial
- burdometer
- config
- delay_me_not
- distance
- dress_up
- enternity
- fps
- ime
- logger
- party_time
- paste
- pouches
- send
- shortcuts
- speedometer
- target_info
- terminate
- timestamp
- window_title
- Game
- Windower
- General