Namespaces

Variants

Share

Share
Views
Actions

Addon Tutorial

From Runes of Magic Wiki
Jump to: navigation, search

Well, you've decided to start an AddOn for Runes of Magic. Or at least you're contemplating it.

Contents

[edit] Making the folder

You start out by choosing a folder name for your AddOn. It should be something simple, and no funny characters (just letters, numbers, dashes, underscores, periods, basically).

For this exercise, we're going to make the cliché HelloWorld AddOn.

Assuming you installed RoM to C:\Program Files\Runes of Magic\, you'll want to verify that C:\Program Files\Runes of Magic\Interface\ and C:\Program Files\Runes of Magic\Interface\AddOns\ exist, otherwise you should create them, case-sensitive.

You'll then want to create the C:\Program Files\Runes of Magic\Interface\AddOns\HelloWorld\ folder (or whatever you want to call it), which will house all your AddOn's files.

Don't name your AddOn "test"... it won't work.

[edit] Laying the groundwork

There are three kinds of files used for AddOns (outside of image and sound files):

.toc file
Each AddOn should have one .toc file, it will match the name of your AddOn's folder. In our example, this will be HelloWorld.toc and will be placed as Interface\AddOns\HelloWorld\HelloWorld.toc. This will provide metadata about your AddOn as well as providing information on what files to load.
.lua files
This is where the bulk of your code lies, all the actual business logic should take place here.
.xml files
XML files can load Lua files and well as define frames. (At least until a way is provided to create frames in Lua.) Try not to put any actual logic into your XML files, but rather delegate it to a function or method inside your Lua.

We need to start with your TOC file. It should look something along the lines of this:

## Title: Hello World
## Version: 0.1
## Notes: Says "Hello, World!"
## Author: YourNameGoesHere

HelloWorld.lua

As you can see, you specify your AddOn's name (Title), what version number it is (Version), a one-line description (Notes), and your author handle (Author). You can specify other information, but if you do, prefix with X-, so that there are no conflicts in the future.

For right now, we're only loading one file, HelloWorld.lua, which will contain some simple code.

[edit] Let's get our code on

Here's what we'll stick in Interface\AddOns\HelloWorld\HelloWorld.lua

local HelloWorld = {} -- the AddOn namespace, all your functions should be placed inside here instead of as globals.
_G.HelloWorld = HelloWorld -- expose it to the global scope

--- Print out "Hello, World!" to the chat frame
-- @usage HelloWorld.Print()
function HelloWorld.Print()
    DEFAULT_CHAT_FRAME:AddMessage("Hello, World!")
end

HelloWorld.Print() -- call print

At this point, if you start up RoM, you'll get the message "Hello, World!" in your chat frame.

You can also run /script HelloWorld.Print() at any time from the in-game command line to print out the text again.

[edit] Addendum

For performance reasons, according the lua documentation and lua developer community it is a good practice to:

  • make use of code modules and package calls through namespaces;
  • use as many local function calls as possible;

How to implement this? Assume you created an addon called MyRomAddOn. It's possible to have more than one lua coded files within your addon.Each file then represents a separate module within your addon-package. So, you'd have your addon's folder %RoMInstallPath%Interface\AddOns\MyRomAddOn\, containing your lua table of contents (MyRomAddOn.toc) file which describes the internal file structure and any other lua files. Suppose your addon has 2 lua files, MyRomAddOn.lua and Tools.lua.

The file MyRomAddOn.lua could look like this:

--[[
    Interface section: Definition of your addon's public interface.
	This should match the name of your addon folder's foldername, e.g.
		%RoMInstallPath%Interface\AddOns\MyRomAddOn\
	and this lua file's filename, e.g.
		MyRomAddOn.lua.
--]]
local MyRomAddOn= {};
_G.MyRomAddOn = MyRomAddOn;

--[[
    Private section: Definition of your addon's private API.
	In object-oriënted programming terminology, here you should define
	all external APIs used within your addon,
	as well as any private functions, properties and methods of your addon
	by referencing them using the local operator.
--]]
local RoMAPI = _G;--external Runes of Magic game API.
local ParentFrame = MyRomAddOn_Frame;--external file MyRomAddOn.xml;same as _G.MyRomAddOn_Frame or RoMAPI.MyRomAddOn_Frame.

local function SysMsg(msg)
	RoMAPI.DEFAULT_CHAT_FRAME:AddMessage(tostring(msg), RoMAPI.ChatTypeInfo["SYSTEM"].r,  RoMAPI.ChatTypeInfo["SYSTEM"].g,  RoMAPI.ChatTypeInfo["SYSTEM"].b );
end

--[[
    Public section: Definition of your addon's public API.
	In object-oriënted programming terminology, here you should define
	all public functions, properties and methods of your addon
	by referencing them using the dot operator.
--]]
function MyRomAddOn.Load()
        SysMsg("MyRomAddOn loaded.");
end

--[[
    Remark the final return statement in your MyRomAddOn.lua!
    This exposes the public API of your addon.
--]]
return MyRomAddOn;

To test, type /script MyRomAddOn.Load(); in the game's chatwindow.

[edit] Event fun

Code which can't react to the game isn't very useful, which is why we need to start messing with some events.

To do this, we have to unintuitively make a frame, which is how we can listen in on events.

So, let's add HelloWorld.xml to our .toc file (before the HelloWorld.lua file). It would look like this:

## Title: Hello World
## Version: 0.1
## Notes: Says "Hello, World!"
## Author: YourNameGoesHere

HelloWorld.xml
HelloWorld.lua

And create the Interface\AddOns\HelloWorld\HelloWorld.xml file:

<Ui xmlns="http://www.runewaker.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.runewaker.com/UI.xsd">
    <Frame name="HelloWorld_Frame">
        <Scripts>
            <OnEvent>
                -- call the OnEvent method on our AddOn, passing in any relevant args.
                HelloWorld:OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
            </OnEvent>
        </Scripts>
    </Frame>
</Ui>

Now we need to define an OnEvent method as well as register any events, so let's return to our lua.

local HelloWorld = {} -- the AddOn namespace, all your functions should be placed inside here instead of as globals.
_G.HelloWorld = HelloWorld -- expose it to the global scope
local frame = _G.HelloWorld_Frame -- made in the XML

--- Print out "Hello, World!" to the chat frame
-- @usage HelloWorld.Print()
function HelloWorld.Print()
    DEFAULT_CHAT_FRAME:AddMessage("Hello, World!")
end

frame:RegisterEvent("VARIABLES_LOADED")

function HelloWorld:OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
    -- this is a fun trick that will call a method named the event, passing in all the relevant args.
    self[event](self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end

function HelloWorld:VARIABLES_LOADED()
    self.Print()
end

Now let's explain what self[event](self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) does. It allocates the events to each function corresponding to the event variable, without having to use structures like

if event == "VARIABLES_LOADED" then 
    self:VARIABLES_LOADED()
elseif 
    ...
end

Alright, now instead of printing Hello, World! when the Lua loads, it will do it when VARIABLES_LOADED is fired.

[edit] Timers

Unlike in a single-threaded environment, you can't just run a sleep command to wait to execute something, you need to use timers.

If you were to manually sleep, then you'd freeze up the entire UI, which nobody wants.

OnUpdate is a script that runs every frame tick, similar to an event firing every tick.

In the following, we'll run a command every 5 seconds:

For your XML:

<Ui xmlns="http://www.runewaker.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.runewaker.com/UI.xsd">
    <Frame name="HelloWorld_Frame">
        <Scripts>
            <OnEvent>
                -- call the OnEvent method on our AddOn, passing in any relevant args.
                HelloWorld:OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
            </OnEvent>
            
            <OnUpdate>
                -- call the OnUpdate method on our AddOn
                HelloWorld:OnUpdate(elapsedTime) -- elapsedTime fix provided by Alleris.
            </OnUpdate>
        </Scripts>
    </Frame>
</Ui>

And in the Lua:

local HelloWorld = {} -- the AddOn namespace, all your functions should be placed inside here instead of as globals.
_G.HelloWorld = HelloWorld -- expose it to the global scope
local frame = _G.HelloWorld_Frame -- made in the XML

--- Print out "Hello, World!" to the chat frame
-- @usage HelloWorld.Print()
function HelloWorld.Print()
    DEFAULT_CHAT_FRAME:AddMessage("Hello, World!")
end

frame:RegisterEvent("VARIABLES_LOADED")

function HelloWorld:OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
    -- this is a fun trick that will call a method named the event, passing in all the relevant args.
    self[event](self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end

function HelloWorld:VARIABLES_LOADED()
    self.Print()
end

local time_remaining = 5 -- in seconds
function HelloWorld:OnUpdate(elapsed)
    -- elapsed is the amount of time in seconds since the last frame tick
    
    time_remaining = time_remaining - elapsed
    if time_remaining > 0 then
        -- cut out early, we're not ready yet
        return
    end
    time_remaining = 5 -- reset to 5 seconds
    self.Print()
end

You can stop OnUpdate from firing by calling frame:Hide() and starting it again with frame:Show().


This code will generate an error in the current release. The error may come from the XML OnUpdate(elapsedTime) or from the function HelloWorld:OnUpdate(elapsed) needs investigation

[edit] Slash Commands

We will now learn how to register slash (/) commands. Lets start out by doing a simple slash command, with no other parameters. The only changes will be in the .Lua file.

local HelloWorld = {} -- the AddOn namespace, all your functions should be placed inside here instead of as globals.
_G.HelloWorld = HelloWorld -- expose it to the global scope
local frame = _G.HelloWorld_Frame -- made in the XML

SLASH_HelloWorld1 = "/HelloWorld" -- First slash command
SLASH_HelloWorld2 = "/hw" -- Second slash command
SlashCmdList["HelloWorld"] = function(editBox, msg) -- Definition of a slash command, having to use editBox and a message
	HelloWorld.Print()
end

--- Print out "Hello, World!" to the chat frame
-- @usage HelloWorld.Print()
function HelloWorld.Print()
	DEFAULT_CHAT_FRAME:AddMessage("Hello, World!")
end

frame:RegisterEvent("VARIABLES_LOADED")

function HelloWorld:OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
	-- this is a fun trick that will call a method named the event, passing in all the relevant args.
	self[event](self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end

function HelloWorld:VARIABLES_LOADED()
	self.Print()
end

local time_remaining = 5 -- in seconds
function HelloWorld:OnUpdate(elapsed)
	-- elapsed is the amount of time in seconds since the last frame tick

	time_remaining = time_remaining - elapsed
	if time_remaining > 0 then
		-- cut out early, we're not ready yet
		return
	end
	time_remaining = 5 -- reset to 5 seconds
	self.Print()
end

Now, to declare a slash command name, we need to use the SLASH prefix to a variable, then the name of the slash variable, and a number after it. Note that the number has to be an integer. Examples of slash names:

SLASH_thisIsASlash1

SLASH_lulzSlash1

A more complex slash command, in which you have extra parameters, but still keeping it simple.

local HelloWorld = {} -- the AddOn namespace, all your functions should be placed inside here instead of as globals.
_G.HelloWorld = HelloWorld -- expose it to the global scope
local frame = _G.HelloWorld_Frame -- made in the XML

SLASH_HelloWorld1 = "/HelloWorld" -- First slash command
SLASH_HelloWorld2 = "/hw" -- Second slash command
SlashCmdList["HelloWorld"] = function(editBox, msg) -- Definition of a slash command, having to use editBox and a message
	if string.lower(msg) == "hide" then -- If the 2nd string is <tt>hide</tt>
		frame:Hide()
		DEFAULT_CHAT_FRAME:AddMessage("OnUpdate now stopped.")
	elseif string.lower(msg) == "show" then
		frame:Show()
		DEFAULT_CHAT_FRAME:AddMessage("OnUpdate now started.")
	elseif string.lower(msg) == "print" then
		HelloWorld.Print()
	else
		DEFAULT_CHAT_FRAME:AddMessage("Not a viable command")
	end
end

--- Print out "Hello, World!" to the chat frame
-- @usage HelloWorld.Print()
function HelloWorld.Print()
	DEFAULT_CHAT_FRAME:AddMessage("Hello, World!")
end

frame:RegisterEvent("VARIABLES_LOADED")

function HelloWorld:OnEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
	-- this is a fun trick that will call a method named the event, passing in all the relevant args.
	self[event](self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end

function HelloWorld:VARIABLES_LOADED()
	self.Print()
end

local time_remaining = 5 -- in seconds
function HelloWorld:OnUpdate(elapsed)
	-- elapsed is the amount of time in seconds since the last frame tick

	time_remaining = time_remaining - elapsed
	if time_remaining > 0 then
		-- cut out early, we're not ready yet
		return
	end
	time_remaining = 5 -- reset to 5 seconds
	self.Print()
end

[edit] Frame Tutorial

Needs additional information on how to call a frame to show it for the user including a working example


Now lets take a look at frames and the xml.

Before we look at frames and all this stuff lets take a look at xml.

An easy XML example would be:

<MyRoom>
    <MyBookshelf height="1,5m" width="1m">
        <Book name="A book" pages="666"/>
        <Book name="Another book" pages="123"/>
    </MyBookshelf>
</MyRoom>

We see that XML has something like tags in HTML. (called elements in XML) An element can be opened and closed again (<element>; </element>) or be empty, which is shown through the "/" at the end of the element.

Elements can have attributes (height) with a value (1,5m), but they aren't necessary.

Elements have parents (the outer element; parent of MyBookshelf would be MyRoom) and children (a child has only one parent but a parent can have multiple children).

To make a frame for our AddOn we'll do the following in our .xml:

<Ui xmlns="http://www.runewaker.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.runewaker.com/..\..\WorldXML\UI.xsd">
    <Frame name="MyAddon_MainFrame" parent="UIParent" inherits="UICommonFrameTemplate" hidden="false" enableMouse="true">
	<Size>
		<AbsDimension x="320" y="300"/>
	</Size>
	<Anchors>
		<Anchor point="CENTER">
			<Offset>
				<AbsDimension x="0" y="0"/>
			</Offset>
		</Anchor>
	</Anchors>
	<Layers>
		<Layer>
			<FontString name="MyAddon_MainFrame_Title" inherits="GameTitleFont" text="Title">
				<Anchors>
					<Anchor point="TOP" relativePoint="TOP">
						<Offset>
							<AbsDimension x="-10" y="8"/>
						</Offset>
					</Anchor>
				</Anchors>
			</FontString>
		</Layer>
	</Layers>
    </Frame>
</Ui>

What does everything do?

<Frame name="MyAddon_MainFrame" parent="UIParent" inherits="UICommonFrameTemplate" hidden="true" enableMouse="true">

Starts a Frame with the apperance of a default-frame. (like the bag- or guild-frame) name = Name of the frame. Must be unique and is used to identify the element in the .lua. parent = The parent of the frame (I don't know if you can leave this out). inherits = Some default-values for the element rather then setting everithing by hand (UICommonFrameTemplate can be found in "ui.xls" in "interface.fdb/interface/worldxml/". hidden = Defines if the frame is hidden or not. enablemouse = Defines if the frame can get mouse input.

<Size>

Defines the size of the element

<AbsDimension x="320" y="300"/>

An element to define sizes. x = Width. y = Height.

<Anchors>

Contains the location of the element via anchors.

<Anchor point="CENTER">

An anchor for the element. point = A relative point. If no element to align relative to is given then the parent will be used. CENTER aligns the frame in the center of the parent element.

<Offset>

An Offset of the element. Can be used to align it e.g. 200 px right from the center.

<AbsDimension x="0" y="0"/>

Again AbsDimension. Here the Offset is set.

<Layers>

Layers are texts on the frame. <Layers> contains all of them.

<Layer>

One layer.

<FontString name="MyAddon_MainFrame_Title" inherits="GameTitleFont" text="Title">

FontString is a layer with Text in it. text = The text that is displayed on the FontString.

This displays a Frame with the title set in the FontString.

But just a frame with a title isn't enough for a real addon. Where are the buttons, textfields and that stuff? To Use all that Stuff we first need to set a Frame where all this is going. (I don't know why it cannot be put into MyAddon_MainFrame, but it can't.) So add the following code:

<Ui xmlns="http://www.runewaker.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.runewaker.com/..\..\WorldXML\UI.xsd">
...
...
        </Layers>
        <Frames>
            <Frame name="MyAddon_Content">
	        <Anchors>
		    <Anchor point="TOPLEFT"/>				
	        </Anchors>
    	        <Frames>
	    	    <!-- Here our button will go -->
	        </Frames>
            </Frame>
        </Frames>
    </Frame>
</Ui>

What does <Frames> do? It gives us a space inside an element, where we can add frames inside of it. In our example we add a frame called "MyAddon_Content" into "MyAddon_MainFrame" to put the button in. In the MyAddon_Content frame we open <Frames> again to add the button. (because the button is threaded as a frame itself)

The Text between the is a commentary.

Now lets replace this commentary with our button:

<Button name="MyAddon_TestButton" inherits="UIPanelButtonTemplate" text="Test">
    <Size>
        <AbsDimension x="50" y="25"/>
    </Size>
    <Anchors>
	<Anchor point="TOPLEFT" relativeTo="MyAddon_Content" relativePoint="TOPLEFT">
	    <Offset>
		<AbsDimension x="0" y="0"/>
	    </Offset>
	</Anchor>
    </Anchors>
    <Scripts>
	<OnClick>
            <!-- Add vour script to call when the button is clicked here -->
	</OnClick>
    </Scripts>
</Button>

We see that we use an element called Button. With inherits="UIPanelButtonTemplate" we will use the standard-button used in RoM, which prevents us from the need to define a texture and stuff like that. You can unpack "interface.fdb" and look into the files in "/interface/worldxml/" to see other templates.

There are still some things to lern about placement of elements in frames. as seen in <Anchor point="TOPLEFT" relativeTo="MyAddon_Content" relativePoint="TOPLEFT"> and in the frame ingame the button is places in the top left corner of the frame. This is done defining a relative anchor.

point = The point where the element should be "grabbed" at to place it relative. possible are: TOP TOPRIGHT RIGHT BOTTOMRIGHT BOTTOM BOTTOMLEFT LEFT TOPLEFT

relativeTo = An element to place relative to. If not set then outward element will be used.

relativePoint = Where at the relativeTo-element the at "point" defined point should be attached to. Cas have the same values as point.

There are more elements like CheckButtons, EditBoxes and stuff, which are used pretty similar in the code like the button. And maybe some examples out of WoW can be used to.

The complete code would look like

<Ui xmlns="http://www.runewaker.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.runewaker.com/..\..\WorldXML\UI.xsd">
<frame name="MyAddon_MainFrame" parent="UIParent" inherits="UICommonFrameTemplate" hidden="false" enableMouse="true">
	<Size>
		<AbsDimension x="320" y="300"/>
	</Size>
	<Anchors>
		<Anchor point="CENTER">
			<Offset>
				<AbsDimension x="0" y="0"/>
			</Offset>
		</Anchor>
	</Anchors>
	<Layers>
		<Layer>
			<FontString name="MyAddon_MainFrame_Title" inherits="GameTitleFont" text="Title">
				<Anchors>
					<Anchor point="TOP" relativePoint="TOP">
						<Offset>
							<AbsDimension x="-10" y="8"/>
						</Offset>
					</Anchor>
				</Anchors>
			</FontString>
		</Layer>
	</Layers>
	<Frames>
		<Frame name="MyAddon_Content">
			<Anchors>
				<Anchor point="TOPLEFT"/>				
			</Anchors>
			<Frames>
				<Button name="MyAddon_TestButton" inherits="UIPanelButtonTemplate" text="Test">
					<Size>
						<AbsDimension x="50" y="25"/>
					</Size>
					<Anchors>
						<Anchor point="TOPLEFT" relativeTo="MyAddon_Content" relativePoint="TOPLEFT">
							<Offset>
								<AbsDimension x="0" y="0"/>
							</Offset>
						</Anchor>
					</Anchors>
					<Scripts>
						<OnClick>
							<!-- Add vour script to call when the button is clicked here -->
						</OnClick>
					</Scripts>
				</Button>
			</Frames>
		</Frame>
	</Frames>
</Frame>
</Ui>