How to write LotRo-PlugIns: Unterschied zwischen den Versionen

Aus HdRo-Wiki
Wechseln zu:Navigation, Suche
(Die Seite wurde neu angelegt: „Anbei ein englischsprachiger Guide zum Skripten von HdRo-PlugIns aus dem offiziellen Forum (Original von Garan "https://www.lotro.com/forums/showthread.php?428…“)
(kein Unterschied)

Version vom 9. Juni 2019, 13:44 Uhr

Anbei ein englischsprachiger Guide zum Skripten von HdRo-PlugIns aus dem offiziellen Forum (Original von Garan "https://www.lotro.com/forums/showthread.php?428196-Writing-LoTRO-Lua-Plugins-for-Noobs").
Wird ggf. noch übersetzt bzw. aktualisiert.

Start

The tools you will need are fairly simple. First, you will need a language reference (e.g. http://www.lua.org/manual/5.1/ which is fairly easy to understand and navigate).

Second, you will want the Turbine API documentation. As of the time this guide was written, the lastest API docs were published on the LoTROInterface.com website at https://www.lotrointerface.com/downloads/info621-IsengardLuaAPIDocumentation.html

You will need an editor, a simple text editor like Notepad will suffice but some users prefer syntax highlighting editors or project managers to organize their files like Notepad++.

If you plan on using any custom graphics, you will want an image editor that can generate .jpg and/or .tga files as these are the only file formats that LoTRO Lua will display.

The last thing you might want are some sample plugins to dissect and play around with. Turbine published a package of sample files which can be downloaded in a 7zip archive from http://content.turbine.com/sites/lotro/lua/Beta_LuaPlugins.7z You may also want to check out LoTROInterface.com or other plugin sources. One of the best ways to learn is to dig in, twist, pull, yank and turn and see what happens

One rule to bear in mind, most things dealing with Lua are case sensitive so if you keep getting a nil value or an error that a function doesn't exist or any other mysterious error, always double check that you have the correct case.


Getting Started



__init__.lua FILES



Loading a plugin



Hello World



Programming with Class



Function Calling or Method to the Madness



Plugin State and Saved Data



Internationalization or "How Vindar Saved the World"


Many developers tend to forget that LoTRO is an international application supporting three client languages, English, German and French (and to a limited extent, a fourth language, Russian). When developing a plugin for the general public, it is a good idea to separate out all of the text strings. There are two basic approaches to this, one is to create one table and load it with only the currently selected language strings. The other is to use a table that has a separate index for the language. Either way, all references to static text should provide some means of translating them to each of the three client languages. If you aren't comfortable translating your interface, ask someone on the forums to assist you, there are many friendly multi-lingual people that will be glad to help you.

A more troublesome problem arises when saving and reloading data. Since the user has the option to change which language his client is supporting and not only the character sets but the numeric formatting is different between English and German/French, this can cause some serious problems. The first problem of supporting the UTF-8 characters for non-English clients has several solutions, I have chosen to implement a variant of the patch originally published by Vindar. This patch creates a wrapper for the standard Turbine data save and load methods which encodes the data before saving it and decodes it when it is reloaded so that UTF-8 characters are properly saved and loaded. The Vindar Patch can be found on LoTROInterface.com at http://www.lotrointerface.com/downloads/info456-afixfortheparseerrorforeuropeanclients.html

The Vindar Patch does not by itself solve the numeric formatting problem. Fortunately, there is a fairly simple solution to this. Since numeric data is saved as strings via the Vindar Patch, you can coerce the string into the correct format when reloading. Create a global variable to track whether european formatting or english formatting is currently in use:

euroFormat=(tonumber("1,000")==1); -- will be true if the number is formatted with a comma for decimal place, false otherwise
-- 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

then whenever you load a saved numeric value, force it to the current number format. The following example assumes data was stored for the current character

Opacity=1; -- default
local settings=PatchDataLoad( Turbine.DataScope.Character, "Settings");
if settings~=nil then
  Opacity=euroNormalize(settings.Opacity);
end

Note that PatchDataLoad is the wrapper for the PluginData.Load method from the Vindar Patch. Not only will this allow saving and loading data in the DE/FR clients, this has the added benefit of automatically adjusting the numeric format if a client changes from DE/FR to EN or EN to DE/FR between saving and loading the data.

Another internationalization issue that can arise is automatically detecting the current client locale setting. The built in function GetLocale() returns the operating system locale, NOT the current client application locale. Lua authors developed a workaround but Turbine eventually responded by implementing the Turbine.Engine:GetLanguage() method which returns one of the Turbine.Language values: Turbine.Language.Invalid=0 Turbine.Language.English=2 Turbine.Language.EnglishGB=268 435457 (0x10000001) Turbine.Language.French=268435 459 (0x10000003) Turbine.Language.German=268435 460 (0x10000004) Turbine.Language.Russian=26843 5463 (0x10000007)

The different clients have different chat commands and chat messages - for instance in the french client, you don't use "/plugins load HelloWorld", you would instead use "/plugins charger HelloWorld". This can become a significant issue when creating quickslot alias commands or when using the Chat object to trap incomming messages. You can use the locale test above to determine the running client and then create the appropriate command.

Another issue has to do with creating resource strings. Some people have created their .lua files with UTF8 encoding with success, but I prefer a slightly more "brute force" approach. Anywhere that I need a special character, I simply concatenate the appropriate character codes to generate the desired character. For instance to generate a cedilla (the french "c" with a squiggle under it which indicates a soft c) I use "\195\167" - see the example below. The "\" character escapes the character code and the cedilla is character code 195 + character code 167. This is essentially the same as string.char(195)..string.char( 167).

The last issue has to do with the lack of text metrics in LoTRO Lua. Different languages will need different amounts of space for their translation of a string. In most programming languages, you would simply use a function to determine how much space a string requires. Unfortunately, Turbine did not provide us with such a luxury. However, there is a workaround using the Visibility attribute of a bound scroll bar and a non-multiline label control. Set the label control's width to a small number (a good estimate for the 20 point fonts would be 8 pixels per character in the string) and then slowly increase the width of the label (I usually use increments of 8 to save time) until the scrollbar:IsVisible() returns false. Once the scrollbar detects that it no longer needs to be rendered, you will know that you have enough room for the text. Ideally, you create one such label with a bound scrollbar and re-use it as needed to test all strings. You should probably hide the label and the scrollbar off canvass by setting their top properties to a negative value. By obtaining the metrics, you will know if you need to increase the size of a control or possibly indicate that text has been cropped.

This leads me to the last of the "Hello World" samples. This one will remember where it was loaded and will display the message in the correct client language (of course, you'll have to close and restart the client, selecting a different language to test it )

import "Turbine.UI"; -- this will expose the label control that we will implement
import "Turbine.UI.Lotro"; -- this will expose the standard window that we will implement
locale = "en";
if Turbine.Shell.IsCommand("hilfe") then
 locale = "de";
elseif Turbine.Shell.IsCommand("aide") then
 locale = "fr";
end
strings={}; -- create a table for the string resources - note, this would usually be generated in a separate .lua file
strings["en"]={}; -- create the English resource string table
strings["en"][1]="Hello World Window"
strings["en"][2]="Hello World"
strings["en"][3]="English"
strings["de"]={}; -- create the German resource string table
strings["de"][1]="Hallo Welt Fenster"
strings["de"][2]="Hallo Welt"
strings["de"][3]="Deutsch"
strings["fr"]={}; -- create the French resource string table
strings["fr"][1]="Fen\195\170tre Bonjour tout le monde"
strings["fr"][2]="Bonjour tout le monde"
strings["fr"][3]="Fran\195\167aise";
function UnloadPlugin()
 if HelloWindow~=nil then
  local settings={}
  settings["top"]=HelloWindow:GetTop();
  settings["left"]=HelloWindow:GetLeft();
  Turbine.PluginData.Save(Turbine.DataScope.Account, "HelloWorld", settings);
 end
end
HelloWindow=Turbine.UI.Lotro.Window(); -- we call the constructor of the standard window object to create an instance
local x,y=Turbine.UI.Display:GetWidth()/2-100,Turbine.UI.Display:GetHeight()/2-100;
local settings=Turbine.PluginData.Load(Turbine.DataScope.Account, "HelloWorld");
if settings~=nil then
 if settings["top"]~=nil then y=settings["top"] end
 if settings["left"]~=nil then x=settings["left"] end
end
HelloWindow.loaded=false;
HelloWindow:SetWantsUpdates(false);
HelloWindow.Update=function()
 if not HelloWindow.loaded then
  HelloWindow.loaded=true;
  Plugins["HelloWorld"].Unload = function(self,sender,args)
   UnloadPlugin();
  end
  HelloWindow:SetWantsUpdates(false);
 end
end
if locale=="fr" then
 HelloWindow:SetSize(350,200);
elseif locale=="de" then
 HelloWindow:SetSize(260,200);
else
 HelloWindow:SetSize(280,200);
end
HelloWindow:SetPosition(x,y);
HelloWindow:SetText(strings[locale][1]); -- assigns the title bar text
HelloWindow.Message=Turbine.UI.Label(); -- create a label control to display our message
HelloWindow.Message:SetParent(HelloWindow); -- sets the label as a child of the main window
HelloWindow.Message:SetSize(HelloWindow:GetWidth()-20,20); -- sets the message size
HelloWindow.Message:SetPosition(10,90); -- places the message control in the vertical middle of the window with a 10 pixel left and right border
HelloWindow.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- centers the text in the message control both horizontally and vertically
HelloWindow.Message:SetText(strings[locale][2]); -- sets the actual message text
HelloWindow:SetWantsUpdates(true);
HelloWindow:SetVisible(true); -- display the window (windows are not visible by default)


Turbine API


This is as good a spot as any to delve a bit into the basic elements of the Turbine API. There are currently four Turbine object libraries that form the API: Turbine, Turbine.Gameplay, Turbine.UI, and Turbine.UI.Lotro. These libraries must be "imported" before their objects can be accessed so any plugin that uses any of these libraries will import them as the first lines of their main file. I won't go into great detail on all of the API classes, there is a set of API docs available from LoTROInterface.com for that. However, each library contains some elements that deserve exta attention above and beyond what the API docs provide.

The "Turbine" library exposes the Chat, Engine, base Object, Plugin, PluginData, PluginManager, Shell and ShellCommand objects - basically all the top level, generic stuff. The Chat object is one of the newest and most powerful elements for creating plugins that do more than just repaint the same old UI. By filtering and capturing the chat messages, Plugins can now interact between players. This is a HUGE step forward for Plugin usefulness. Unfortunately, the interaction isn't programatically bi-directional. That is, it requires user interaction in at least one of the directions to make anything happen. However, in many cases this is more than enough to make it useful. The Chat object has only one event, Recieved which is an event handler for all incomming chat messages. The Engine object is handy for debugging and timestamping. The GetCallStack method can be useful for debugging as can the ScriptLog method. The GetGameTime method is great for setting timers that can be checked in an object's Update event handler and GetLocalTime is great for timestamping things such as incomming chat messages. The Plugin object is great for accessing the current plugins settings from the .plugin definition file. Of particular interest is the GetVersion as this will allow authors to version stamp their saved data and automatically detect when a plugin has been updated. The PluginData object is how data is saved and loaded to and from .plugindata files The PluginManager is an incredibly useful object as it can control loading and unload plugins as well as listing the available plugins and loaded plugins. One point that deserves mentioning, a plugin can not commit suicide by unloading its own apartment - plugins can only unload another plugin's apartment. The Shell object is where plugins register the chat commands that they should respond to. This object also has a WriteLine method which can output messages to the Standard channel of the chat window. This is a GREAT debugging tool. The ShellCommand object is an instance of a shell command that was registered with the Shell object.

The "Turbine.Gameplay" library exposes the Player and Party stuff such as LocalPlayer, Party, Actor, Backpack, Attributes, etc. Before accessing any of the characer attributes, you must first obtain a handle to an instance of the LocalPlayer using Turbine.Gameplay.LocalPlayer:G etInstance(). The other methods are called on that instance. The Actor class exposes many of the attributes of a character instance including morale, power, effects, level and name. This is the base class from which other classes like LocalPlayer inherit many of their properties. The effects are held in an EffectsList object. The Attributes class exposes information about entities. The Backpack object provide access to the items in the character's backpack. To access the backpack, you must first create an instance of the LocalPlayer object and then access that instance's backpack: player=Turbine.Gameplay.LocalP layer:GetInstance(); backpack=player:GetBackpack(); The ClassAttributes is a base class for character class attributes. The Effect object represents an effect on an entity and is a member of an EffectsList. You can use this object to retrieve information about a specific effect such as the Category, Description, Duration, Icon, ID, Name, and StartTime as well as Curable, Debuff and Dispellable flags. To access the Name of the first Effect on a character you would use:

player=Turbine.Gameplay.LocalPlayer:GetInstance();
effectList=player:GetEffects();
if effectList~=nil and effectList[1]~=nil then
 Turbine.Shell.WriteLine("first effect:"..tostring(effectList[1]:GetName()));
end

The Entity class is a base class from which all entities such as Player inherit the GetName and RegisterForClickHandling methods. GetName is obvious enough. RegisterForClickHandling allows developers to register an entity to process the right mouse popup menu for its entity type. The Equipment object is similar in functionality to the Backpack with a very important limitation, you can not programatically drop an item into the Equipment collection (so plugins can not programatically equip items at this time). NOTE: The current (as of November 12, 2011) API documentation incorrectly indicates that the enumeration of equipment slots is Turbine.Gameplay.EquipmentSlot when in fact it is Turbine.Gameplay.Equipment, so the "Back" equipment slot is "Turbine.Gameplay.Equipment.Ba ck". If you try using the naming indicated in the API docs you will get an error. The Item object represents a distinct item stack, either in the Backpack or Equipment collections. The LocalPlayer object is how we access an instance of the current character. You must create an instance of the LocalPlayer before you can access any of the player methods or properties. The Party object represents a fellowship or Raid. The Player object represents one element of a Party collection.

The "Turbine.UI" library exposes the fundamental UI elements such as Window, TextBox, Label, etc. BlendMode is an enumeration of the possible blend modes that can be used with a background or backcolor. These values will determine how the control visually interacts with its container. The Button object is a generic button which can be used to create custom buttons. For most uses, the Turbine.UI.Lotro.Button class is more useful in that it will inherit the current skin's background and border style. The Checkbox object is a generic checkbox control which can be used to create custom checkboxes. For most uses, the Turbine.UI.Lotro.Checkbox class is more useful. The Color object is used to represent any color object which can be assigned to a control. The color object has four members representing Alpha, Red, Green and Blue. Each member can have a value from 0 to 1. If you create a color but only pass 3 arguments they will define Red, Green and Blue and the Alpha channel will default to 1. Content alignment is an enumeration of the possible positioning values for the SetTextAlignment method The Context Menu object creates a popup menu The Control object is the most fundamental object class that can be instantiated. This basic UI element is very flexible and can act as a container or background. Any element that doesn't require text should probably be created as a control as this element can display images and/or colors and respond to mouse, keyboard and update events. The ControlList object represents the child objects of a container control. The Display object exposes the screen display properties and current mouse information. The DragDropInfo class is a virtual class, use the Turbine.UI.Lotro.DragDropInfo class instead. FontStyle is an enumeration of font styles. Currently only Outlined and default are defined. The Label object is a control that allows displaying text. The ListBox object is a very useful container control that creates horizontal or vertical element arrays. The MenuItem object represents an element of a Context Menu The MenuItemList object is the collection of menu items in a context menu MouseButton is the enumeration of possible values for the Button argument of mouse event handlers Orientation is the enumeration of possible layout orientation values The ScrollableControl is a subclass for the generic Control class. Do NOT directly instantiate an instance of this class as it will crash the client. This is a virtual class from which other scrollable classes inherit the scrolling methods and events. The ScrollBar object represents a scroll control which can either be bound to a scrollable control in which case it will automatically display and provide scrolling as needed, or it can be used as a stand alone control which can have minumum, maximum and current values programatically controlled and will respond to scroll events. The TextBox object is the basic control for entering and displaying user modifiable text. The Turbine.UI.Lotro.TextBox object is usually preferable as it will inherit skin attributes from the client. The TreeNode object represents a single node of a Tree View control The TreeNodeList object is the collection of nodes of a Tree View control The TreeView object is a tree style display control The Window object is a basic window control which can be used to create custom windows. The Turbine.UI.Lotro.Window object is usually preferable as it will inherit skin attributes from the client.

The "Turbine.UI.Lotro" library exposes classes that can inherit the current skin attributes from the client. These are generally specialized versions of the fundamental elements that provide things such as borders. Action is a partial enumeration of the possible values of the key event arguments. For a more complete listing, see the Turbine forums thread: http://forums.lotro.com/showthread.php?364427-Turbine.UI.Lotro.Action The BaseItemControl is a virtual class from which the ItemControl class inherits members and properties The Button object will create a button object that will inherit the current skin attributes unless the developer overrides the background, color or font The CheckBox object will create a check box object that will inherit the current skin attributes unless the developer overrides the background, color or font The DragDropInfo object which represents the object being dropped in drag drop event handlers The EffectDisplay object will create a visual display element for an Effect object. The EquipmentSlot object creates a visual display element for an Equipment object - due to limited functionality, this is only really useful for creating a custom character panel Font is a partial enumeration of possible fonts. For a list of additional fonts see the Turbine forums thread: http://forums.lotro.com/showthread.php?397086-Some-undocumented-fonts-and-problem-with-TrajanPro25 The GoldButton object is a somewhat custom "gold" button. The GoldWindow object is a somewhat custom "gold" window. The ItemContol object will create a visual display element for an Item object (this is basically a limited version of a QuickSlot control which can only contain items) The LotroUI class is a virtual class from which instances of the LotroUIElement class inherits the IsEnabled, Reset and SetEnabled methods. The LotroUIElement object represents a built-in UI element which Lua can override, currently only the 5 backpacks and the vitals display are supported The QuickSlot object is a multi-purpose container control which can contain an Alias, Hobby, Item, Pet, or Skill shortcut. Most of the user interactable "bars" plugins are built using these controls. The ScrollBar object will create a scroll control which can either be bound to a scrollable control in which case it will automatically display and provide scrolling as needed, or it can be used as a stand alone control which can have minumum, maximum and current values programatically controlled and will respond to scroll events. This control will inherit the current skin attributes unless the developer overrides those attributes. The TextBox object will create an editable text display object that will inherit the current skin attributes unless the developer overrides the background, color or font The Window object will create a basic window with title bar, borders and "close" button using the current skin attributes unless the developer overrides the background, font or colors.

The Turbine API docs cover most of the methods and events associated with the above objects. However, there are a couple of undocumented (or insufficiently documented) methods and events: Missing/incomplete enumerations The Turbine.Gameplay.Class and Turbine.Gameplay.Race enumerations are incomplete. Some additional values can be found at http://forums.lotro.com/showthread.php?370289-A-few-more-missing-enumeration-values The Turbine.Gameplay.ItemClass enumeration is incomplete. Some additional values (currently also outdated) can be found at http://forums.lotro.com/showthread.php?381143-Turbine.Gameplay.ItemCategory-enumeration The Turbine.UI.Lotro.Action enumeration is incomplete. Some additional values can be found at http://forums.lotro.com/showthread.php?364427-Turbine.UI.Lotro.Action The Turbine.UI.Lotro.Font enumeration is incomplete. Some additional values can be found at http://forums.lotro.com/showthread.php?397086-Some-undocumented-fonts-and-problem-with-TrajanPro25

There are a couple of quirks in the way backgrounds and colors work. Once a background is set, you can not set it back to the default "no background". The same holds true for background color, once you apply a background color, it will override the background unless you use the SetBackColorBlendMode() to blend the image with the color. There is no way to get back to the version that had the background image without a background color.

:SetStretchMode()
There is an incredibly useful yet still undocumented method of the control object, SetStretchMode(). This method will allow dynamically scaling the display size of a control's background image. When applied to a container control, all of the control's child controls will resize with the control. This is SO incredibly useful, I find it hard to believe that Turbine has still not documented this method. There are five basic stretch modes, 0, 1, 2, 3 and 4. StretchMode=0 will turn off scaling of a control (and incidentally set the alpha to 0). Any background image will be cropped or tiled. This is the only setting where the image will be properly bounded by a parent control. StretchMode=1 will scale an image based on the size it had when the stretch mode was assigned and its current size. When using StretchMode=1 it is important to set the control to the image's original size BEFORE assigning StretchMode=1, then set the size to the desired stretched size after assigning StretchMode=1. This stretchmode can cause an image to exceed the bounds of its parent. If this happens, the control will only respond to mouse events within the bounds of its parent even though the control is rendered outside those bounds. StretchMode=2 will scale a control to the size of its background image. When StretchMode=2 is initially assigned, the control will resize to fit the image size. If the control is subsequently resized, the background will be stretched to fit the control. Note that in StretchMode=2, the control will not respond to any mouse events even if mouse visibility is true. StretchMode=3 is similar to StretchMode=0 and will turn off scaling of a control. Any background image will be tiled or cropped but if the control exceeds the bounds of its parent, the image will not be properly cropped by the parent's bounds. StretchMode=4 is similar to StretchMode=1 except the control will not receive mouse events even if mouse visibility is set true. This is likely an accidental glitch. SetStretchMode has some side effects that seem to be unintentional. First, it can allow a control to display outside the bounds of its parent control - usually after control1:SetParent(control2), control1 will have its origin relative to control2 AND will be bounded by (will not draw outside) control2. When using a StretchMode other than 0, the child control's origin will be based on its parent but it will not be bounded by its parent, it will draw beyond its parent's canvas. This can lead to some interesting effects. The second side effect is probably related to the first issue. When using SetStretchMode, the control may not respond correctly to SetBlendMode(). I haven't documented all of the combinations, but suffice it to say that if your control is not behaving as intended, try to avoid using SetBlendMode() with SetStretchMode() as it seems to simply interfere with the proper working of SetStretchMode(). SetStretchMode can impact a control's ability to receive mouse events, notably using modes 2 and 4 will disable mouse events for the control. Lastly, any StretchMode other than 0 will prevent the control from properly rotating with its parent Window when :SetRotation() is used. Any or all of these side effects may be accidental glitches that may be fixed in the future as this is an undocumented method. Using SetStretchMode on a Turbine.UI.Lotro.Window object will stretch the border and titlebar graphics so if you want the entire window to stretched, use a standard Turbine.UI.Window control.

SetStretchMode Example1
Retrieving the size of an image of unknown size (useful for properly resizing a control on the fly to fit its background). In this example we will size the control to the image's size and then retrieve that size. First, assign the background. Setting StretchMode=2 will resize the container to the size of its background allowing us to retrieve the image size if needed.

Window1=Turbine.UI.Lotro.Window();
Window1:SetText("SetStretchMode Example1");
Window1:SetSize(200,200);
Window1:SetPosition((Turbine.UI.Display:GetWidth()-Window1:GetWidth())/2,(Turbine.UI.Display:GetHeight()-Window1:GetHeight())/2);
Control1=Turbine.UI.Control();
Control1:SetParent(Window1);
Control1:SetBackground(0x410f83cc); -- a built in resource that I happen to know is 54x62
Control1:SetStretchMode(2); -- sizes window to image size so that we can determine the image dimensions
local width, height=Control1:GetSize(); -- we can retrieve the image's actual size at this point if we desire it
Turbine.Shell.WriteLine("Width="..tostring(width)..", height="..tostring(height));
Control1:SetPosition((Window1:GetWidth()-width)/2,(Window1:GetHeight()-height)/2);
Window1:SetVisible(true);

SetStretchMode Example2
Scaling an image to a new size. This time we will pick on the Ettenmoors map, shrinking it down to 200x200 size. Now it looks as small as it sometimes feels

Window1=Turbine.UI.Lotro.Window();
Window1:SetText("SetStretchMode Example1");
Window1:SetSize(200,200);
Window1:SetPosition((Turbine.UI.Display:GetWidth()-Window1:GetWidth())/2,(Turbine.UI.Display:GetHeight()-Window1:GetHeight())/2);
Control1=Turbine.UI.Control();
Control1:SetParent(Window1);
Control1:SetBackground(0x41008133);
Control1:SetStretchMode(2); -- sizes window to image size so that we can determine the image dimensions
Control1:SetSize(Window1:GetSize())
Window1:SetVisible(true);

:SetRotation()
Another wonderful but undocumented method is the Window:SetRotation() method. This method allows you to rotate a window around the X, Y, or Z axis. There are a few awkward glitches with this undocumented feature, but once you understand them you can easily adjust for them.

The first thing to remember about the SetRotation method is that its arguments are expressed in Degrees even though all Lua standard math functions are based on Radians. If you wish to perform angular math and then use the results in the SetRotation method you will have to manually convert your values from Radians to Degrees (easily done once you know you have to do it). The second issue has to do with mouse event handling. When a window is rotated, it will respond to mouse events at its original unrotated coordinates, so if you plan on handling mouse events you will have to size your window large enough to capture mouse events. The child controls that get rotated will respond properly within those bounds, you just need to be sure the window is large enough to cover any area the child controls may get rotated to. This takes a bit of getting used to but can be dealt with easily enough once you know to account for it. A rather peculiar issue is that the rotation gets reset to all zeros when the window is hidden, so the window must be visible to set its rotation and if you ever hide and redisplay it you have to manually track its rotation and reapply it. I usually create a property Window.rotation and read/assign it in the VisibleChanged() event handler.

Asynchronous Processing


Most programming languages implement a means of temporarily suspending processing or polling at intervals. LoTRO Lua allows elements to register an Update event handler which will fire once for each frame (so if you are getting 100 frames per second, the Update handler is called every 100th of a second). Simply define an .Update() function for the object and then execute the object:SetWantsUpdates(true) to register the event handler. It is fairly important from a performance standpoint to use object:SetWantsUpdates(false) when you do not need to poll for a state so that your even handler does not get called when there is nothing to process. Many plugins use an Update handler on their main window to process statements in the first frame after the plugin is loaded, such as assigning an Unload event handler.

The Unload Event Handler


Whenever the plugin's apartment is unloaded, the Lua system will fire the Unload event for any plugin in that apartment. To handle the Unload event, you must assign an unload event handler. This is a little tricky since you can't assign an event handler for an object until the object exists and the Plugins[] element for your plugin will not exist until it completes the loading state. To handle this, most plugins set an Update event handler in their main window with a semaphore (a flag) used to determine whether the plugin should be considered "loading". In the below example, UnloadMe is the Unload event handler for the window class "SomeWindow":

function UnloadMe()
 -- release any event handlers, callbacks, commands
 -- save any data that needs saving
end
SomeWindow=class(Turbine.UI.Window);
function SomeWindow:Constructor()
 Turbine.UI.Window.Constructor( self );
 self.loaded=false;
 self.Update=function()
  if not self.loaded then
   self.loaded=true;
   Plugins["MyPlugin"].Unload = function(self,sender,args)
    UnloadMe();
   end
   self:SetWantsUpdates(false);
  end
 end
 self:SetWantsUpdates(true);
end


Event handling


Events are handled in Lua by defining a function and assigning it to the event that it should handle. When a Turbine event is fired, the function that you assigned will be called and passed two arguments, the handle to the object raising the event and a table of additional arguments. For example, if you have an object named Control1 and it needs to process mouse click events, you would create a function to handle this by:

Control1.MouseClick = function(sender, args)
-- put code to handle event here
end

The sender parameter is particularly helpful if you are using a single function to respond to events for an array of controls. Turbine's API documents do not provide detailed information about the arguments but fortunately Lua provides a mechanism for examining tables. In the above example, all we know from the API is that there is a table that will be passed to our parameter "args". We can determine the list of arguments in the table by using the code:

for k,v in pairs(args) do
   Turbine.Shell.WriteLine("name:"..tostring(k)..", value:"..tostring(v));
end

There are several noteworthy things in this example. First, we can enumerate all of the name/value pairs of a table easily. Second, we can use the Turbine.Shell.WriteLine method to output debugging info to the Standard chat channel. Third, string concatenation is performed with the ".." operator. Fourth, when dealing with a value which might be nil or undefined, it is always safest to wrap it in a tostring() function to force the nil value into the string "nil" to avoid runtime errors. For this example, the output will be:

name:X, value:1
name:Y, value:1
name:Button, value:1

which indicates that for the MouseClick() event there are three arguments, named X, Y, and Button. You will then know that you can reference them in the event handler code as "args.X", "args.Y" and "args.Button". There are two basic types of objects that can register for event handling, I call them Private and Shared. The above example dealt with a private object, an instance of a control that we created. You can register event handlers for Private objects by simply assigning the function to the event. Shared objects however need to be treated with a bit more respect. In many cases, shared objects like the Backpack will have event handlers for several plugins simultaneously and all of those event handlers need to be processed. If you simply assign your function to the event, you will override the other plugins' event handlers or they will override yours. Fortunately, events can have a table of handlers. There is a good thread on the Turbine forums, http://forums.lotro.com/showthread.p...nce-and-events, that covers the discussion about implementing event handlers without stepping on other plugins and the solution presented by Pengoros. The net result of that discussion is the convention of using the following functions to register/unregister event handlers for shared objects (you should define these functions in your main .lua file or the first code file listed in the __init__.lua file):

function AddCallback(object, event, callback)
   if (object[event] == nil) then
       object[event] = callback;
   else
       if (type(object[event]) == "table") then
           table.insert(object[event], callback);
       else
           object[event] = {object[event], callback};
       end
   end
   return callback;
end
function RemoveCallback(object, event, callback)
   if (object[event] == callback) then
       object[event] = nil;
   else
       if (type(object[event]) == "table") then
           local size = table.getn(object[event]);
           for i = 1, size do
               if (object[event][i] == callback) then
                   table.remove(object[event], i);
                   break;
               end
           end
       end
   end
end

For example, to register an event handler for the local player effects list you would use the code:

player = Turbine.Gameplay.LocalPlayer.GetInstance();
playerEffects=player:GetEffects();
effectsHandler = function(sender, args)
 -- put event handling code here
end
AddCallback(playerEffects, "EffectAdded", effectsHandler);

when you no longer need to handle the event or in your plugins Unload event handler, you would unregister your event handler:

 RemoveCallback(playerEffects, "EffectAdded", effectsHandler);

The above example also illustrates how to properly get an instance of the LocalPlayer object before trying to access any of its elements. Note, in order to maintain the integrity of the event handler table of a shared object and allow garbage collection to process, you should always remove any callback that you added.

Getting All Fired Up (or how to fire your own events) If you build your own reusable modules, you may eventually find it necessary to allow users to define event handlers that you fire. This is actually a lot simpler than it might first appear. The only mildly complicated part is allowing your events to have tables of handlers. This can be achieved very easily. In the following example, the event "SomethingHappened" can be assigned a single function or a table of functions just like Turbine allows. We create the FireEvent function to handle actually firing the events. Staying with the Turbine convention, we also pass the arguments in a table, you could alternately choose to pass them individually, or not have any arguments as you see fit. Note, the "for i=1,size" loop could also be handle with a "for k,v in pairs(event)" iteration.

someModule.SomethingHappened = nil; -- unnecessary placeholder, I just put it in so that I remember that I have defined this event
someModule.FireEvent=function(sender, event, args)
   -- allows us to fire events as functions or tables of functions
   if type(event)=="function" then
       event(sender, args);
   else
       if type(event)=="table" then
           local size = table.getn(event);
           local i;
           for i=1,size do
               if type(event[i])=="function" then
                   event[i](sender, args);
               end
           end
       end
   end
end
-- then somewhere else in the module where the condition for firing the event arises, you simply call FireEvent
   acorn.Name="Acorn"; -- just some bogus stuff that we use as an example of argument passing
   if acorn.state==state.falling then -- some sample condition that defines when to fire the event
       local args={}; -- this will pass our custom arguments to the event, in this example we will pass "Object" and "State" but you can assign anything you feel is relevant to a potential handler
       args.Object=acorn;
       args.State="Falling";
       someModule:FireEvent(someModule.SomethingHappened, args); -- fires the actual event
   end

When a user creates an instance of your module, they assign a function to SomethingHappened:

chickenLittleEventHandler=function(sender, args)
   -- remember, we assigned ".Object" and ".State" as attributes of the parameter object in "someModule"
   if args.Object.Name()=="Acorn" and args.State=="Falling" then
       Turbine.Shell.WriteLine("The Sky Is Falling!");
   end
end
-- add a callback to the custom event just as you would for a Turbine event
AddCallback(someModule, "SomethingHappened", chickenLittleEventHandler);
-- and don't forget to remove your callback in your unload handler using RemoveCallback
myUnloadHandler=function()
   RemoveCallback(someModule, "SomethingHappened", chickenLittleEventHandler);
   -- etc
end

Now whenever the condition "acorn.state=state.falling " is evalated as true in "someModule", it will fire the event which will write "The Sky Is Falling" to the standard channel.

Lua is only skin deep


With a bit of planning, Lua can work very nicely with custom skins. The first step is to use the Turbine.UI.Lotro classes instead of the Turbine.UI classes when possible. The simple reason being that the .Lotro versions will automatically support skins unless the developer sets a custom background or color. If you simply must use the Turbine.UI classes, you can still include elements that are compatible with skins by using the built in graphic resources. These are assigned to a background the same way as custom resources (the object:SetBackground() method), you just use their Resource ID instead of a file path. Now comes the bad news. There is no Turbine documentation on the Lua Resource IDs for any of the skinnable graphical elements. However, there IS a plugin that has a partial library of the UI elements as well as lots of other interesting things, IRV the Image Resource Viewer. You can use this tool to view and identify resources as well as add them to the Library once you have identified them. For example, if you want to create a Turbine.UI.Window but create a Turbine.UI.Control object for the "close" button, you can assign that object Resource ID 0x41000196 and it will display the close button icon from the current skin (to support clicked and rollover states, you would use 0x41000197=pressed and 0x41000198=highlighted, changing the background image in the MouseEnter, MouseDown, MouseUp, MouseLeave and MouseClick events).

Where to go from here


There is a great deal more to Lua and plugins. I would recommend that you learn about the pcall Lua command for error handling. I would also recommend that you learn what a Lua environment is as well as what metatables are. There are some fascinating things that you can do with Lua that are beyond the scope of this document. With any luck, Turbine will continue to expose more of the UI, allowing us to further enhance this wonderful game.