Plugin Along Episode 2: Developing a Quest Tracker Opacity Plugin part 1 of 3

Welcome back to Plugin Along, where we talk about LOTRO Plugins! Last time we 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.

We’re going to take that knowledge and try to solve an accessibility problem that a friend of mine had.

See the accompanying video at https://youtu.be/4bov3e1-HoI

The Problem

Today we’re going to look at creating a basic plugin to address an accessibility problem from a friend:

I wonder if anyone has ideas to help me see my quest tracker better. I wish it had a background that I could darken like the chat box. In some settings, I can see it fine.

but in others, I can’t see it at all

I have terrible eyes and I make the font larger but the background really doesn’t help me

Sandy, a friend in need

Well, that’s not good! Lord of the Rings Online is a lot of fun, and if we could make a small accessibility plugin to help people read their quest tracker more easily, that sounds like a win to me.

The First Solution

The first thing I do when thinking about a new plugin is to see if others have also thought about this. I did a web search for “lotro quest tracker opacity“, and the first result was this thread on the LOTRO Forums. User Vargax had the same request:

I have searched through the UI offerings and cannot seem to find a single modification to the Quest Tracker Panel. It is extremely hard to read quests in nearly all zones due to the lack of color. Can we get a slider for opacity like the chat box has please? Thanks for your time!

Vargax, 2019-03-23

Plugin developer Thurallor replied:

This could be done with a small plugin. The plugin would just draw an opaque rectangle behind the quest tracker.

The only problem would be, the plugin can’t determine the location of the quest tracker on the screen, so you would have to position it manually.

Thurallor, 2019-03-24

He then followed up with a simple plugin that does this!

left = 0.5; top = 0.5; width = 0.25; height = 0.25; opacity = 0.5;

import "Turbine";
import "Turbine.UI";
screenWidth, screenHeight = Turbine.UI.Display:GetSize();
window = Turbine.UI.Window();
window:SetPosition(left * screenWidth, top * screenHeight);
window:SetSize(width * screenWidth, height * screenHeight);
window:SetBackColor(Turbine.UI.Color(opacity, 0, 0, 0));
window:SetVisible(true);
window:SetZOrder(-1);

How does this work? It creates a Turbine.UI.Window, makes sure it’s behind everything else, and moves it to a specific location on the screen.

Because this is a trivial example, there are some more advanced aspects that are missing from this simple implementation: Moving, resizing, and changing the opacity of the window has to be done manually in the plugin file. These values and cannot be customized per-character.

Improvements

First, when making a plugin, it is a good idea to track where you are going, and what you have done. I do this in two files:

readme.txt

A readme file is a great place to document the changes between one version and another. Not only does it help people understand the benefits of upgrading to the latest version, but it’s also a reference for you (and any other developers working on the plugin), so you can look back and easily know when a feature was introduced.

You can also use the readme to thank any other plugins and their developers for inspiration, testers who put in a lot of time and effort, translators, and so on.

This file should be included when you distribute the plugin files to others or upload them to lotrointerface.com.

Our readme will start out very simple:

Special thanks to:
  Thurallor for the initial implementation idea of using a partially opaque background window to make the quest tracker easier to read

v1.0.0 changes:
  <none so far>

_todo.txt

A todo file is a great place to document the changes you want to make in the future. For more complicated plugins you might replace this with a more complicated and capable system, but for now this is a great start. I like to have sections for known bugs and issues, features we know we want to do soon, and possible features for the future.

This file is not meant to be included when you distribute the plugin files. To help remind myself, I start the filename with an underscore (_), so it shows up as the first file in the list.

Our todo will also start out very simple:

Known bugs:
  Window absorbs mouse clicks, means you can't click things behind it.

v1.0.0 features:
  Move the window in-game
  Resize the window in-game
  Adjust the opacity of the window in-game
  Save the window size/position separately for each character
  Localize the window text

Potential features:
  Save the current window size/location/opacity as the default for other characters.

Window vs Lotro.Window

LOTRO offers two Window types to use in our plugins: Turbine.UI.Window, and Turbine.UI.Lotro.Window. What is the difference? Turbine.UI.Window is a basic top-level-control:

Turbine.UI.Lotro.Window takes that basic window and adds a border, a titlebar, a close button, a black background, and the ability to move and resize. You can see here how all of these new elements are drawn on top of the underlying opaque Turbine.UI.Window.

While either of these could work to provide a readable background behind the quest tracker text, my goal is to be as unobtrusive as possible, which means doing without the border, title, etc. However, Turbine.UI.Window does not have two things that we want: Moving and Resizing. Let’s add them back in!

Move the Window

We can add in moving by handling a few of the mouse events. First, we want to know where the mouse cursor and window are when the mouse button is pressed:

window.MouseDown = function(sender, args)
    window.mouseDownMousePosition = { Turbine.UI.Display.GetMousePosition() };
    window.mouseDownWindowLocation = { window:GetPosition() };
end

If the mouse button is released, then dragging must be done. We’ll use the presence (or absence) of window.mouseDownMousePosition as a way of knowing if the mouse button is still down:

window.MouseUp = function(sender, args)
    window.mouseDownMousePosition = nil;
end

Finally, if the mouse moves while the button is down, we want the window to move too:

window.MouseMove = function(sender, args)
    if (window.mouseDownMousePosition) then
        local mouseStartX, mouseStartY = unpack(window.mouseDownMousePosition);
        local mouseCurrentX, mouseCurrentY = Turbine.UI.Display.GetMousePosition();

        -- calculate how much the cursor has moved
        local deltaX = mouseCurrentX - mouseStartX;
        local deltaY = mouseCurrentY - mouseStartY;

        local windowLeft, windowTop = unpack(window.mouseDownWindowLocation);
        -- move the window the same distance that the mouse has moved:
        window:SetPosition(windowLeft + deltaX, windowTop + deltaY);
    end
end

Now we can click and drag the window to move it around. That creates a new problem, however. The user can accidentally move the window off of the screen, and then not be able to move it back on the screen. Let’s add a helpful function to make sure the window stays on the screen:

function Onscreen(control)
    -- How big is the screen?
    local displayWidth, displayHeight = Turbine.UI.Display.GetSize();

    -- How big is the control?
    local controlWidth, controlHeight = control:GetSize();

    -- Where is the control?
    local controlLeft, controlTop = control:GetPosition();

    -- bring it back if it is too far right
    if ((controlLeft + controlWidth) > displayWidth) then 
        controlLeft = displayWidth - controlWidth;
    end
    -- bring it back from too far left:
    if (controlLeft < 0) then
        controlLeft = 0;
    end
    -- bring it back from too far down:
    if ((controlTop + controlHeight) > displayHeight) then
        controlTop = displayHeight - controlHeight;
    end
    -- bring it back from too far up:
    if (controlTop < 0) then controlTop = 0; end

    control:SetPosition(controlLeft, controlTop);
end

And we can call it from the end of the code in window.MouseMove:

Onscreen(window);

Now that we’ve added a feature, we can update our readme:

v1.0.0 changes:
  Window can be dragged.

Our todo file can also be updated:

Known bugs:
  Window absorbs mouse clicks, means you can't click things behind it.

v1.0.0 features:
  Move the window in-game
  Resize the window in-game
  Adjust the opacity of the window in-game
  Save the window size/position separately for each character
  Localize the window text

Potential features:
  Save the current window size/location/opacity as the default for other characters.

Resize the Window

Now the user can move the window under the quest area! However, the size of the window is fixed, and might not match the user’s quest tracker. LOTRO does not give us a way to determine the size of the quest tracker area, but if we add the ability for them to resize the window, then the user can do it themselves. Let’s do that next!

In a resizable Turbine.UI.Lotro.Window, the user can drag the edge of the window to resize in that direction: the right edge to move the right border, the top edge to move the top border, the upper-right corner to move both the top and right edges, and so on. Testing in-game shows that this draggable border is around 10 pixels wide. To mimic this behavior, we first need to change the MouseDown event handler, to check if the cursor is over the border:

window.MouseDown = function(sender, args)
    window.mouseDownMousePosition = { Turbine.UI.Display.GetMousePosition() };
    window.mouseDownWindowLocation = { window:GetPosition() };
    window.mouseDownWindowSize = { window:GetSize() };

    local borderWidth = 10;
    local windowWidth, windowHeight = window:GetSize();
    local relativeMouseX, relativeMouseY = window:GetMousePosition();

    window.isOverLeftBorder = relativeMouseX < borderWidth;
    window.isOverTopBorder = relativeMouseY < borderWidth;
    window.isOverRightBorder = relativeMouseX > (windowWidth - borderWidth);
    window.isOverBottomBorder = relativeMouseY > (windowHeight - borderWidth);

    window.isOverBorder = 
        window.isOverLeftBorder or
        window.isOverTopBorder or
        window.isOverRightBorder or
        window.isOverBottomBorder;
end

This modified code stores how big the window currently is, and checks if the mouse cursor is near the border. The window:GetMousePosition() function returns coordinates relative to the window, so if the cursor is in the upper-left corner of the window, its position is 0,0.

We now need to change the MouseMove event handler to either move or resize based on if the cursor is over the border:

window.MouseMove = function(sender, args)
    if (window.mouseDownMousePosition) then
        -- Resize
        if (window.isMouseOverBorder) then

            -- where did the cursor start?
            local mouseStartX, mouseStartY = unpack(window.mouseDownMousePosition);
            -- where is the cursor now?
            local mouseCurrentX, mouseCurrentY = Turbine.UI.Display.GetMousePosition();
            -- where did the window start?
            local mouseDownWindowLeft, mouseDownWindowTop = unpack(window.mouseDownWindowLocation);
            -- how big did the window start?
            local mouseDownWindowWidth, mouseDownWindowHeight = unpack(window.mouseDownWindowSize);

            -- how much will the width and height change?
            local widthDelta = 0;
            local heightDelta = 0;
            -- where will the window be?
            local left = mouseDownWindowLeft;
            local top = mouseDownWindowTop;

            -- check for each of the borders:

            if (window.isMouseOverLeftBorder) then
                -- make wider / narrower, adjust width and left to match.
                -- left is bigger, right is smaller:
                widthDelta = mouseStartX - mouseCurrentX; 
                left = mouseDownWindowLeft - widthDelta;
            end

            if (window.isMouseOverRightBorder) then
                -- make wider / narrower, adjust width to match.
                -- left is smaller, right is bigger:
                widthDelta = mouseCurrentX - mouseStartX; 
            end

            if (window.isMouseOverTopBorder) then
                -- make taller / shorter, adjust height and top to match.
                -- up is bigger, down is smaller:
                heightDelta = mouseStartY - mouseCurrentY; 
                top = mouseDownWindowTop - heightDelta;
            end

            if (window.isMouseOverBottomBorder) then
                -- make taller / shorter, adjust height to match.
                -- down is bigger, up is smaller:
                heightDelta = mouseCurrentY - mouseStartY; 
            end

            -- Adjust the width and height based on which border was moved:
            local width = mouseDownWindowWidth + widthDelta;
            local height = mouseDownWindowHeight + heightDelta;

            -- Don't let the window get too small:
            if (width < 200) then width = 200; end
            if (height < 200) then height = 200; end

            -- Update the window size and (if necessary) position:
            window:SetSize(width, height);
            window:SetPosition(left, top);
        -- Move
        else
            local mouseStartX, mouseStartY = unpack(window.mouseDownMousePosition);
            local mouseCurrentX, mouseCurrentY = Turbine.UI.Display.GetMousePosition();

            local deltaX = mouseCurrentX - mouseStartX;
            local deltaY = mouseCurrentY - mouseStartY;

            local windowLeft, windowTop = unpack(window.mouseDownWindowLocation);
            window:SetPosition(windowLeft + deltaX, windowTop + deltaY);
        end
        -- Make sure the window is still fully onscreen:
        Onscreen(window);
    end
end

Now that the window is resizable by the user, we can update our readme:

v1.0.0 changes:
  Window can be dragged.
  Window can be resized.

Our todo file can also be updated:

Known bugs:
  Window absorbs mouse clicks, means you can't click things behind it.

v1.0.0 features:
  Resize the window in-game
  Adjust the opacity of the window in-game
  Save the window size/position separately for each character
  Localize the window text

Potential features:
  Save the current window size/location/opacity as the default for other characters.

Summary

That’s it for today’s topics. We’ve covered the problem we’re trying to solve (lack of opacity behind quest tracker text), Thurallor’s simple solution and some possible improvements we could make, some basic project management with readme.txt and todo.txt files, using source control to take snapshots of our work, and how to make a window movable and resizable.

Next time we’ll add options so the user can change the opacity of the window and lock the window, localize the plugin, and support saving and loading user data.

2 comments

Leave a Reply

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