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

Welcome back to Plugin Along, where we talk about LOTRO Plugins! Last time we continued making improvements to the Opaque Quest Tracker plugin by adding the ability to change the window opacity and lock state through the options panel, and the ability to save and load options.

This time we’re going to cover basic localization and how to save/load decimal numbers correctly across all languages and client versions (32-bit & 64-bit). We’ll also talk about how to publish your plugin once it’s finished! done enough for people to use!

See the accompanying video at https://youtu.be/3xa1Hefoqbo

Localization

LOTRO currently supports English, French, and German clients, and there is an unofficial Russian client as well. As plugin developers, we can help make a more streamlined player experience by respecting their client’s language. The first thing we need to do is separate the text we want to display from the code that displays it. A well-designed localization system does not need all of the text to be translated at once, and that is reflected here:

EN = Turbine.Language.English;
FR = Turbine.Language.French;
DE = Turbine.Language.German;
RU = Turbine.Language.Russian;

local pluginDescription = "'" .. plugin:GetName() .. "' v" .. plugin:GetVersion() .. ", by Cube";

_LANG = {
    ["STATUS"] = {
        ["LOADED"] = {
            [EN] = "Loaded " .. pluginDescription;
            [DE] = "Geladen " .. pluginDescription;
        };
        ["UNLOADED"] = {
            [EN] = "'Opaque Quest Tracker' unloaded";
        }
    };
    ["OPTIONS"] = {
        ["LOCKED"] = {
            [EN] = "Lock the window in place.";
        };
        ["OPACITY"] = {
            [EN] = "Opacity: %d%%";
            [FR] = "Opacité: %d%%";
            [DE] = "Opazität: %d%%";
        };
    };
};

Now we need a helper function to get a specific string for the current language. Since I develop in English, I fall back to the English text if the current language is not present, since I know the English one will be there.

function GetString(array, language)
    -- Allow the caller to override which language is used,
    -- but default to the client.
    if (language == nil) then language = Turbine.Engine:GetLanguage(); end

    -- If they passed in a non-existant thing, return an empty string
    if (array == nil) then return ""; end

    -- If the specified language is present, return it.
    if (array[language] ~= nil) then return array[language]; end

    -- Otherwise, return the English version
    return array[EN];
end

Now we call GetString() wherever we had hard-coded text:

-- in Turbine.Plugin.Unload:
    Turbine.Shell.WriteLine(GetString(_LANG.STATUS.UNLOADED));

-- When making the options:
lockedCheckbox:SetText(GetString(_LANG.OPTIONS.LOCKED));
opacityLabel:SetText(string.format(GetString(_LANG.OPTIONS.OPACITY), SETTINGS.OPAQUE_WIN.OPACITY * 100));
...
    opacityLabel:SetText(string.format(GetString(_LANG.OPTIONS.OPACITY), sender:GetValue()));

-- When finishing loading:
Turbine.Shell.WriteLine(GetString(_LANG.STATUS.LOADED));

Success! Now, when someone is playing with the German or French client, they’ll see the translated versions of the text. (For the text that has been translated.) Now the localization is completed, we can update our readme:

v1.0.0 changes:
  Window can be dragged.
  Window can be resized.
  Opacity can be changed.
  Bug fix: window can be locked, so it does not respond to the mouse.
  Window location, size, opacity, and lock status are saved for each character.
  Options screen, and load/unload messages localized.

Our todo file can also be updated:

v1.0.0 features:
  Localize the window text
  Make sure decimal numbers are saved and loaded correctly regardless of client version or language.

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

Localizing Save Files with Vindar’s Patch

Now that we support localizing the window text, we should go back and look at how the Turbine.PluginData.Save and Turbine.PluginData.Load functions work when running the client under French or German.

Here’s what our save file looks like when saved from an English client. (Note that there is no defined order for Lua array elements, so you may see these in a different order, and this is expected behavior.)

return 
{
	["OPAQUE_WIN"] = 
	{
		["LEFT"] = 0.747899,
		["TOP"] = 0.317682,
		["WIDTH"] = 0.250000,
		["HEIGHT"] = 0.492507,
		["LOCKED"] = true,
		["OPACITY"] = 0.750000
	}
}

And here is what the save file looks like when saved from a 64-bit LOTRO client in German, with no code changes:

return 
{
	["OPAQUE_WIN"] = 
	{
		["LEFT"] = 0,747899,
		["TOP"] = 0,317682,
		["WIDTH"] = 0,250000,
		["HEIGHT"] = 0,492507,
		["LOCKED"] = true,
		["OPACITY"] = 0,750000
	}
}

As of 2021-07-13, the 64-bit client saves out floating-point numbers in a localized manner. This means that the English client uses a period and the French and German clients use a comma as the radix point (separator) between the integer part and the fractional part. However, when the LOTRO client tries to load in the French and German values it sees them as a pair of integers in the same way that local borderWidth, borderHeight = 10, 10; uses a pair of numbers to initialize two variables.

The standard LOTRO Plugin solution is to use a few helper functions created by Vindar. The original version can be found at lotrointerface.com. Fortunately, we don’t need to understand exactly how it works for now. Download and unzip the zip file. The code we need is in the Patch.lua file. You will often see this renamed as VindarPatch.lua, and we’ll do that here too. Then, we can instruct our code how to find it:

import "CubePlugins.OpaqueQuestTracker.VindarPatch";

This import statement is a period-delimited path, so this tells Lua to look in the folder CubePlugins/OpaqueQuestTracker for a file called VindarPatch.lua.

Finally, we can change out our calls to Turbine.PluginData.Load and Turbine.PluginData.Save, changing them to PatchDataLoad and PatchDataSave.

What changes? The main difference we’ll see here is that the numerical values are written to file as a string, appearing as “0.747899” instead of just 0.747899 or 0,747899. This means that when Lua parses the file it sees a string instead of a pair of integers, if a comma is used. The English version now appears as:

return 
{
	["OPAQUE_WIN"] = 
	{
		["LEFT"] = "0.747899",
		["TOP"] = "0.317682",
		["WIDTH"] = "0.250000",
		["HEIGHT"] = "0.492507",
		["LOCKED"] = false,
		["OPACITY"] = "0.750000
"
	}
}

And the French or German save data now appears as:

return 
{
	["OPAQUE_WIN"] = 
	{
		["LEFT"] = "0.747899",
		["TOP"] = "0.317682",
		["WIDTH"] = "0.250000",
		["HEIGHT"] = "0.492507",
		["LOCKED"] = false,
		["OPACITY"] = "0.750000
"
	}
}

If LOTRO only came with a 64-bit client, or if no one used the 32-bit client, then our work here would be done. Since LOTRO does have a 32-bit client that people use, though, we should check in on how it handles things. Unlike in 64-bit LOTRO, which (since around March, 2021) loads numbers in English format regardless of which client is running, in 32-bit LOTRO numbers are still parsed based on the locale. We can address this with Garan’s euroNormalize function:

-- will be true if the number is formatted with a comma for decimal place, false otherwise:
euroFormat=(tonumber("1,000")==1); 

-- now create a function for automatically converting a number in string format to its correct numeric value
if euroFormat then
    function euroNormalize(value)
        return tonumber((string.gsub(value,"%.",",")));
    end
else
    function euroNormalize(value)
        return tonumber((string.gsub(value,",",".")));
    end
end

Finally, we can process each loaded string from the PluginData file:

    SETTINGS = savedSettings;

    SETTINGS.OPAQUE_WIN.LEFT = euroNormalize(SETTINGS.OPAQUE_WIN.LEFT);
    SETTINGS.OPAQUE_WIN.TOP = euroNormalize(SETTINGS.OPAQUE_WIN.TOP);
    SETTINGS.OPAQUE_WIN.WIDTH = euroNormalize(SETTINGS.OPAQUE_WIN.WIDTH);
    SETTINGS.OPAQUE_WIN.HEIGHT = euroNormalize(SETTINGS.OPAQUE_WIN.HEIGHT);
    SETTINGS.OPAQUE_WIN.OPACITY = euroNormalize(SETTINGS.OPAQUE_WIN.OPACITY);

These changes mean the plugin can successfully load decimal numbers regardless of whether they were saved with a comma or a period as the radix point, and regardless of whether a 32-bit or 64-bit client was used to save and load the file. We can now update the todo file:

v1.0.0 features:

  Make sure decimal numbers are saved and loaded correctly regardless of client version or language.

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

Ship It!

LoTROInterface

Now that all of the v1.0.0 features we wanted are done, it’s time to ship it! By that I mean package it up for lotrointerface.com, and announce it on the LOTRO forums.

The first thing LoTROInterface will ask is which category your plugin goes in. The current list has many LOTRO entries:

  • LotRO Complete Sets
  • LotRO Compilations
  • LotRO Beta Interfaces
  • LotRO Stand-Alone Plugins
    • Action Bars & Main Bar
    • Bags, Bank & Inventory
    • Class Specific (sub-categories)
      • Burglar
      • Captain
      • Champion
      • Guardian
      • Hunter
      • Lore-Master
      • Minstrel
      • Rune Keeper
      • Warden
    • Crafting
    • Creep Play
    • Graphical Modifications (Skins)
    • Maps, coordinates & compasses
    • RolePlay & Music
    • Raiding & Instances
    • Unit Frames (Character, Fellowship, Target)
    • Other
  • LotRO Tools & Utilities for Users
  • LotRO Tools & Utilities for Developers
  • LotRO Libraries
  • LotRO Patches
  • Outdated LotRO Interfaces

If you’re not sure which category your plugin goes in, you can get a better sense of the categories by looking at other plugins already there. For the Opaque Quest Tracker, after checking and ruling out the Unit Frames category, I think Other is the closest match.

The next page asks you to select a zip file to upload, choose images to use, and fill out the plugin title, version, and description. After agreeing to the Rules & Guidelines you’re ready to go!

LOTRO Forums

It may take some time for lotrointerface.com to review your submission. Once it’s finished, we can make the corresponding LOTRO Forums post to let people know. At lotro.com/forums, there is a section for plugin announcements at Forum / Community & Social / User Interface & Lua Scripting / Lua Scripting / Plugins & Requests. Since this is a Plugin announcement, we’ll title the post [Plugin] Opaque Quest Tracker and put in a brief description, with a link back to the lotrointerface.com page.

LOTRO Plugin Compendium

Finally, you can post a comment on the LOTRO Plugin Compendium entry at lotrointerface.com asking to be included in the Plugin Compendium. This makes it very easy for anyone to use your plugin, even if they are not comfortable manually installing it. This is a manual process, so be patient.

Summary

That’s it for today’s topics. We’ve continued making improvements to the Opaque Quest Tracker plugin by adding load and unload messages, localizing the plugin, and fixing our saving/loading to handle decimal numbers correctly. We then looked at how to publish the plugin and tell people about it!

2 comments

Leave a Reply

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