A Slightly Complicated "Hello World" - meldavy/ipf-documentation GitHub Wiki

I personally like my Hello Worlds to have a larger scope than what is expected of it. I like to touch on all parts of development that most developers will have to eventually go through. This page will act as a tutorial to get started with writing an addon, and I'll expand this page as I learn new things.

Boilerplate

Let's first start with the boilerplate. We first create an XML File, named helloworld.xml:

<?xml version="1.0" encoding="UTF-8"?>
<uiframe name="helloworld" x="0" y="0" width="0" height="0">
</uiframe>

and a lua file named helloworld.lua:

local author = 'yourname'
local addonName = 'helloworld'
_G['ADDONS'] = _G['ADDONS'] or {}
_G['ADDONS'][author] = _G['ADDONS'][author] or {}
_G['ADDONS'][author][addonName] = _G['ADDONS'][author][addonName] or {}
local helloworld = _G['ADDONS'][author][addonName]

function HELLOWORLD_ON_INIT(addon, frame)
    helloworld.addon = addon;
    helloworld.frame = frame;
end

You can see that we used a naming convention of all lowercase. I highly doubt this matters at all but it seems to be the convention most people are using. The above is the default template of getting started, and most of it is purely for conventional reasons.

As explained in the intro, ON_INIT is a special function that acts as an addon constructor. Repeating myself, I actually do not know how TOS's addon loader finds, detects and invokes ON_INIT methods, nor do I know how much naming conventions have an impact on how it works. But given that we all have a template that works, we start from here.

In TOS, LUA files are only loaded once, and ON_INIT is also only every called once until the user closes the Tos Client process. This behavior can be tested by adding some print() statements around the code and using the developer console.

Thus, there is no garbage collection or automatic variable reinitializations that happen - if we create large variables scoped to our addon rather than to individual functions, we need to make sure that they are cleaned up properly if we do need them to be clean.

System.out.println

function HELLOWORLD_ON_INIT(addon, frame)
    helloworld.addon = addon;
    helloworld.frame = frame;
    print("Hello World");
end

In lua, we print to the console through the print() function. With my time with Lua and how many times I had issues with it, I really recommend you call tostring on everything you want to print that you know is not a string. For instance, printing a number, you want to do print(tostring(1)) just so that you can avoid all kinds of headaches.

Now you may ask, where is the "console" that the stdout is going to? https://github.com/martinwind/Tree-of-Savior-Addons/tree/master/addons/developerconsole/addon_d.ipf/developerconsole That's where the developerconsole addon comes into play. By typing in /dev in the ingame chat, a console opens where you can see stdout.

There are various other ways to log. CHAT_SYSTEM(string) is a global function you can call to output a message to the System chat tab in-game. Note that it has a limited throughput, thus you can't logs many lines to the chat system consecutively in a short amount of time. But it's a good debugging method.

You can also log to the center of the game screen as an alert message, using ui.SysMsg(string). But since CHAT_SYSTEM() and ui.SysMsg() both connect to in-game UI and elements, it's not the safest form of debug logging when you are packaging your addon for production. Thus, feel free to use these alternative alert methods for local testing, but avoid using them in production code.

Variations

function HELLOWORLD_ON_INIT(addon, frame)
    helloworld.addon = addon;
    helloworld.frame = frame;
    print("Hello World");
    ui.SysMsg("Hello World");
    CHAT_SYSTEM("Hello World");
end

Hello File

ToS's Lua is packaged with the IO library, as can be seen in first party code. We can use the same library, which is standard Lua file IO.

function HELLOWORLD_ON_INIT(addon, frame)
    helloworld.addon = addon;
    helloworld.frame = frame;
    print("Hello World");
    local file = io.open('HelloFile.txt','a');
    file:write("Hello World!");
    file:close();
end

Running this addon will create or open a file called HelloFile.txt in the /data directory, append Hello World! and close the file.

Hello Message!

The reason why I hate traditional Hello Worlds is because it teaches you so little. We can write as much variations of the code as we want, but we need to get outside of the ON_INIT method to really learn addon development.

In this section, we'll subscribe to a simple message, and get a better understanding of how the game works. Let's go through the available (known) messages from the message dump and find one that seems to make sense in plain english.

For instance, ANCIENT_MON_REGISTER is a message where "AncientMon" means assisters, and "Register" probably means when we register an assister to our deck. Or let's try a different one. How about BUFF_ADD? Sounds like its a message that gets fired when we gain a buff, right? Don't worry, I'm also just like you while writing this - I actually don't know if that's how those messages work, I'm just making assumptions based on the name. But that's the whole point of addon development. Resource and documentation is so low that it's often times just trial and error. At least BUFF_ADD sounds like a simple message we can test, so let's do that.

function HELLOWORLD_ON_INIT(addon, frame)
    helloworld.addon = addon;
    helloworld.frame = frame;
    addon:RegisterMsg('BUFF_ADD', 'HELLOWORLD_ON_BUFF_ADD');
end

function HELLOWORLD_ON_BUFF_ADD(frame, msg, argStr, argNum)
    ui.SysMsg("Hello World");
end

And let's try casting any skill that adds a new buff into our new buff bar in game... and it should work!

Note that we called our function HELLOWORLD_ON_BUFF_ADD rather than something generic like ON_BUFF_ADD. This is just simple convention of adding addon name as a prefix. The reason behind that is because all of our lua functions go into the _G global table. If there are two functions with the same name, a conflict happens and whichever the loader picked up last will be the one that gets invoked. Thus for us to at least have a good chance that we keep our function naming unique, adding the addon name as a prefix is a good convention to practice.

Wrapping up

This quick tutorial showed how you can create an addon and listen to a system message. Did you not that argNum in the BUFF_ADD event is the buff class ID?

https://handtos.mochisuke.jp/ is a good database that contains metadata including classIDs of various entities, including buffs.

ID 2222 and 2106 are buff IDs to Dragoon Helmet. With this information, what if we instead listend to BUFF_REMOVE(frame, msg, argStr, argNum)? We would be able to develop an addon that fires a system message when our dragoon helmet gets removed in PVP, or when its buff duration runs out, telling us to re-cast the skill! Thus, even with very minimal knowledge on how various TOS data tables and userdata structs work, we can easily start making useful addons!

⚠️ **GitHub.com Fallback** ⚠️