Plugin Along Episode 1: Basic User Interfaces

Welcome back to Plugin Along, where we talk about LOTRO Plugins! Last time we covered a lot of the basics of LOTRO Plugins. Things like what are plugins, where can we find plugins, how do we install plugins, and how do we load and unload them. We also looked at the bare minimum for making a plugin – the .plugin file, and a .lua file.

Today we’re going to look at the basics of how to read and write Lua code, and the building blocks of graphical user interfaces, or GUIs – things like windows, buttons, and labels.

See the accompanying video at https://youtu.be/yTWdf1UObqU

Documentation

To start, here are some links to help with developing your LOTRO plugins:

Lua Reference Manual

Lua is a programming language that exists outside of games like LOTRO. You can find the reference for the version that LOTRO uses at: Lua 5.1 Reference Manual

At the time of writing, Lua is on version 5.4. How do we know which version LOTRO uses? The easiest way is to ask it! If we execute this code in a LOTRO plugin:

Turbine.Shell.WriteLine("Lua Version: " .. _VERSION);

We get the following output to our standard channel:

Lua Version: Lua 5.1

This means that we should not expect any Lua language features that were introduced after version 5.1 to be available for our plugins. However, LOTRO further limits what language features are available for security reasons.

Lua lets you add comments, bits of non-code text that let you document what you’re doing. Any text on a line that follows two hyphens (–) is ignored by Lua:

local a = 5; -- This sets variable a to the value 5

Lua is flexible in how you write things. If you are coming to Lua from more structured languages, like C#, then this could be unexpected. For example, in a more structured environment you would expect to see parentheses in expressions and function calls, and statements ending in a semi-colon:

local a = 5;
local b = 10;
if (a == b) then
    Turbine.Shell.WriteLine("a == b");
else
    Turbine.Shell.WriteLine("a ~= b");
end

You can also write it like this, however:

local a = 5
local b = 10
if a == b then
    Turbine.Shell.WriteLine "a == b"
else
    Turbine.Shell.WriteLine "a ~= b"
end

Either way is fine by Lua. I just recommend you choose a style and stick to it consistently, as it will make it easier to come back later and read what you wrote.

If you are new to programing, the above code blocks (which both do the same thing) can be read like this:

tell Lua we have a thing called 'a', and it holds the number 5
tell Lua we have a thing called 'b', and it holds the number 10
if a and b are equal
    write "a == b" to the standard chat channel
otherwise
    write "a ~= b" to the standard chat channel
this if block is over

LOTRO Lua Documentation

LOTRO defines an interface that Lua scripts can use to do things. You can find the Official Update 25 Lua Documentation at LoTROInterface.com. Download it, unzip the zip file, and open index.html to start:

The Index page of the LOTRO Lua Documentation.
The Index page of the LOTRO Lua Documentation.

LOTRO Forums

There is a great series of posts on the LOTRO forums titled Writing LoTRO Lua Plugins for Noobs. If you want to get a head start on many of the topics we’ll cover, jump in and read up!


Errors

Errors

Syntax

There are three types of errors to watch out for. The first is syntactical: writing something that isn’t valid Lua. For example, a variable initialization statement normally looks like this:

local a = 5;

If you accidentally write it like this, however, it is not a valid Lua statement:

local a == 5;

LOTRO will not load your plugin if there are any syntax errors. It will instead send a message to Error channel highlighting the file and line for the first syntax error it found:

 ...nts\The Lord of the Rings Online\Plugins\PluginAlong\HelloWorld\Main.lua:1: unexpected symbol near '=='

Runtime

When LOTRO loads your plugin, it only checks that the syntax is correct. Once your plugin is loaded, however, you might try to use a variable before you declare it, call a function that doesn’t exist, or other errors while the plugin is running. For example, this is how you create a window and change where on the screen it will be drawn:

window = Turbine.UI.Lotro.Window();
window:SetPosition(500, 300);
window:SetVisible(true);

By default, however, there is no function called SetGandalf, so this will cause a runtime error:

window:SetGandalf(true);
...Rings Online\Plugins\PluginAlong\HelloWorld\Main.lua:13: attempt to call method 'SetGandalf' (a nil value)

Calling a window function on a variable that isn’t a window will also result in a runtime error:

otherWindow = nil;
otherWindow:SetPosition(100, 100);
…Rings Online\Plugins\PluginAlong\HelloWorld\Main.lua:15: attempt to index global 'otherWindow' (a nil value)

If LOTRO encounters a runtime error, it stops whatever your plugin is doing right now, but it does not turn off your plugin.

Logic

Logic errors are when your code does something different than what you intended. For instance, if I want to make a window 500 pixels wide and 200 pixels tall, this code has a logic error:

window = Turbine.UI.Lotro.Window();
window:SetSize(200, 500);
window:SetVisible(true);

To see what’s wrong, let’s look at the documentation for Turbine.UI.Lotro.Window:

Description of SetSize() showing the first parameter is width and the second parameter is height.
Description of SetSize() showing the first parameter is width and the second parameter is height.

The SetSize function always takes width as the first parameter and height as the second parameter, so I accidentally made a window 200 pixels wide and 500 pixels tall. The code did exactly what I told it to do, but I told it to do the wrong thing!

User Interface Elements

Now that we have references in case we get stuck, and know what it will look like if we make mistakes, let’s get started!

LOTRO Window

All parts of a plugin’s user interface are contained in a Window. LOTRO provides two different windows. Today we’ll look at the one in Turbine.UI.LOTRO.Window.

import "Turbine.UI.Lotro";           -- Tells Lua how to make a window
window = Turbine.UI.Lotro.Window();  -- Make a new window
window:SetSize(400, 200);            -- Width = 400, Height = 200
window:SetPosition(100, 150);        -- 100 pixels from the left, 150 from the top
window:SetText("Hello World!");      -- The title of the window
window:SetVisible(true);             -- Can the player see it?
A window titled Hello World.
The Hello World window!

LOTRO Button

A window can hold other controls, like buttons and labels. Let’s make a button!

button = Turbine.UI.Lotro.Button();  -- Make a new button
button:SetSize(100, 25);             -- 100 wide, 25 tall
button:SetPosition(100, 50);         -- 100 from left of parent, 50 from top of parent
button:SetText("Push Me");           -- What's on the button?
button:SetParent(window);            -- Which control contains this button?
The Hello World window has a button labeled Push Me.
The Hello World window, now with button!

The button comes with some built in functionality. When the mouse cursor is on top of the button, the colour changes to the “rollover” color. When the mouse clicks the button, the color changes to the “clicked” color. By default, the rollover color is lighter than the default color, and the click color is darker than the default color. The button doesn’t do anything else when we click it though. Let’s add some functionality!

-- Define what this button does when clicked:
button.MouseClick = function(sender, args)
    local x, y = button:GetPosition(); -- Where is the button?
    local newX = x + 5;                -- It will go right
    local newY = y + 5;                -- It will go down
    
    -- Testing Output:
    Turbine.Shell.WriteLine("Moving button to " .. newX .. ", " .. newY);

    button:SetPosition(newX, newY);  -- Move the button a bit
end

Each time you click the button, it moves a bit right (+5) and a bit down (+5). Keep clicking the button, and it keeps moving. You can even move it outside the visible window!

Label

Labels are the basic way to display some text to the user. Let’s add one now:

import "Turbine.UI";            -- Tells Lua how to make a label
label = Turbine.UI.Label();     -- Make a new label
label:SetSize(200, 50);         -- 200 wide, 50 tall
label:SetPosition(25, 100);     -- 25 from left, 100 from top
label:SetText("A basic label"); -- what text will the user see?
label:SetParent(window);        -- Which control contains this label?
The Hello World window has a button labeled "Push Me" and a label that says "A basic label".
The Hello World window, now with button and label!

Events

As we saw with the button, the UI controls don’t do much by themselves, but we can react to events. This lets us respond to the user clicking, dragging, typing, and more! Let’s react to the user moving the window by changing what the label says.

window.PositionChanged = function(sender, args)
    local left,top = window:GetPosition();
    local text = "Left: " .. left .. ", Top: " .. top;
    label:SetText(text);
end
The label says "Left: 178, Top: 181".
The label updated when the window was moved.

This shows the position of the window. Let’s make the window resizable, and add another label that can show the size of the window:

sizeLabel = Turbine.UI.Label();     -- Make a new label
sizeLabel:SetSize(200, 50);         -- 200 wide, 50 tall
sizeLabel:SetPosition(25, 150);     -- 25 from left, 100 from top
sizeLabel:SetParent(window);        -- Which control contains this label?

window:SetResizable(true);
window.SizeChanged = function(sender, args)
    local width,height = window:GetSize();
    local text = "Width: " .. width .. ", Height: " .. height;
    sizeLabel:SetText(text);
end
The position label says "Left: 299, Top: 240". The size label says "Width: 314, height: 218".
The labels update with the position and size.

Now that we can resize the window, we can talk about controls covering other controls. If you make the window small enough that the size-label covers the bottom-right corner, you can no longer click on the bottom-right corner to resize the window. That’s because the label is on top of it. This is easier to see if we change the background color of the label:

sizeLabel:SetBackColor(Turbine.UI.Color.DarkRed);
The size label is on top of the bottom-right corner, preventing us from resizing the window from there.
The label is on top of the corner.

In this case, we have a simple solution – Tell the label to ignore the mouse!

sizeLabel:SetMouseVisible(false);

Now we can still click the corner to resize the window even when the label is on top of it, because the the cursor is acting like the label is not there.

Summary

That’s it for today’s topics. We’ve covered where to find documentation; what Lua errors look like; the basics about the user interface controls LOTRO Window, LOTRO Button, and Label; and how to handle a few events.

Next time we’ll take a look at some other UI controls, and put them together into a more functional plugin.

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *