We have detected that cookies are not enabled on your browser. Please enable cookies to ensure the proper experience.
Page 1 of 3 1 2 3 LastLast
Results 1 to 25 of 58
  1. #1
    Join Date
    Mar 2007
    Posts
    1,590

  2. #2
    Join Date
    Mar 2007
    Posts
    1,590

    Before you begin

    The tools you will need are fairly simple. First, you will need a language reference. I personally use 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 http://www.lotrointerface.com/downlo...mentation.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 (I generally just use 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/lot..._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.
    Last edited by Garan; Oct 29 2011 at 09:55 PM.

  3. #3
    Join Date
    Mar 2007
    Posts
    1,590

    Getting Started

    Every plugin has at least two elements, a .plugin definition file and one or more .lua code files. The .plugin file must be in a subfolder of the "My Documents\The Lord of the Rings\Plugins" folder (there are slight variations on the path to My Documents based on operating system versions). The most common accepted standard for folder structure is:
    Plugins\
    AuthorName\
    PluginName\
    Resources

    "AuthorName" is a distinct folder used to group all plugins written by an author. The "AuthorName" folder usually contains only .plugin definition files. Each plugin then generally has a subfolder with a name based on the plugin. The "PluginName" folder generally contains all of the .lua code files for a plugin unless the plugin uses files from a shared library. The major benefit of using a shared library for common classes is that code can be maintained in one location. The major drawback of using a shared library is that any changes to the shared code can potentially cause undesirable effects in plugins. I generally prefer to keep a separate copy of all .lua files in each plugin folder so that I will not run into compatability issues (a distant cousin of that old plague called "DLL Hell") if someone wants to only update one of several plugins that might use a common class file.

    The .plugin file is an xml file with the structure:
    Code:
    <?xml version="1.0"?>
    <Plugin>
        <Information>
            <Name>PluginName</Name>
            <Author>AuthorName</Author>
            <Version>VersionNumber</Version>
            <Description>YourPluginDescription</Description>
            <Image>ImagePath</Image>
        </Information>
        <Package>pathToMainLuaFile</Package>
        <Configuration Apartment="ApartmentName" />
    </Plugin>
    "PluginName" is the name used to load the plugin with the "/plugins load PluginName" as well as how it will appear in game in the "/plugins list" and "/plugins refresh" commands. If you use a plugin manager (a plugin that controls loading other plugins) this is also the name that will be listed in the manager.

    "AuthorName" is the name of the plugin author and is only included for documentary/organizational purposes. This has no actual impact on the functioning of the plugin but can be accessed programatially using the Plugins table.

    "VersionNumber" is the version that will be displayed in the "/plugins list", "/plugins refresh" and plugin manager lists. This value can also be used programatically for tagging saved data and automatically processing data updates.

    "YourPluginDescription" is the text that will display in the Turbine Plugin Manager

    "ImagePath" is the path to a .JPG or .TGA file. Note, if the file is greater than 32x32 it will be cropped to 32x32. If the image is less than 32x32 it will be tiled. This image will be displayed in the Turbine Plugin Manager

    The "pathToMainLuaFile" value is the path relative to the Plugins folder to the main Lua code file. Note that the path uses "." as a folder separator instead of "\" or "/". This is the first file that will be loaded, parsed and processed.

    The Configuration setting is optional and will allow a plugin to run in its own Apartment or address space, meaning that it will get its own copy of all Turbine objects and global environment. The most common reasons for including a Configuration setting are to allow a plugin to be unloaded without affecting other plugins or to prevent other plugins from interfering with global values and event handlers. If your plugin does not need to be unloaded and if it uses safe event handlers (discussed later) then you probably do not need a separate apartment. Note that using a separate apartment will significantly increase the amount of memory used by the Lua system since multiple copies of the environment and global object must be created for each apartment.

    One important thing to remember, Plugins are not unloaded, Apartments are unloaded. That is, when you use the "/plugins unload ApartmentName" command you are unloading all of the plugins that share that apartment.
    Last edited by Garan; Jan 18 2015 at 12:46 PM. Reason: typo

  4. #4
    Join Date
    Mar 2007
    Posts
    1,590

    __init__.lua FILES

    Users can process a special file, __init__.lua by providing just the folder path in an import statement. That is, if there is a lua file with the path "\AuthorName\PluginName\__init __.lua", the file can be processed by using the import command, import "AuthorName.PluginName". Basically, if the parameter passed to the import command is a folder rather than a file, the client will try to load the file "__init__.lua" in the specified folder. Of course, the commands in the __init__.lua file could simply be included in the .lua file that has the import command and the __init__.lua file would no longer serve a purpose.
    Last edited by Garan; Jan 18 2015 at 12:51 PM.

  5. #5
    Join Date
    Mar 2007
    Posts
    1,590

    Loading a plugin

    When a user executes the "/plugins load PluginName" command, the .plugin file with a Name setting matching PluginName will be processed. If the .plugin file contains a Configuration setting with a distinct Apartment name a new global environment is created, otherwise the default global environment is used. The file in the Package setting is loaded, parsed and executed. Since Lua is a scripting language, each statement (which may span multiple lines) is processed in sequence. The parser will continue reading the file until it reaches the end of an executable statement at which point that statement is executed. A semi-colon can be used to terminate a statement but is not required. If the parser detects an error prior to completing processing the main file an error message will be generated and the plugin will not complete loading. Code within functions is compiled but variables and external references are not evaluated until the function is called so it is possible for an error to manifest well after the plugin is loaded and running.
    Last edited by Garan; Jan 18 2015 at 12:55 PM.

  6. #6
    Join Date
    Mar 2007
    Posts
    1,590

    Hello World

    At this point, you are probably ready for your first plugin. Tradition demands that we start with a simple Hello World plugin. The first thing to do is create a .plugin file. We shall call this one, HelloWorld.plugin and it should be saved in the MyDocuments\The Lord of the Rings\Plugins\YourName folder:
    Code:
    <?xml version="1.0"?>
    <Plugin>
     <Information>
      <Name>HelloWorld</Name>
      <Author>YourName</Author>
      <Version>1.00</Version>
     </Information>
     <Package>YourName.HelloWorld.Main</Package>
    </Plugin>
    Note that there is no Configuration tags as this example hardly requires the need to be unloaded separately nor does it need to load or save data in real time and has no event handlers for shared objects.

    The next step is to create the Main.lua file which should be saved in the MyDocuments\The Lord of the Rings\Plugins\YourName\HelloWo rld folder.
    Code:
    import "Turbine"; -- this imports the base Turbine namespace (needed for the objects under 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
    HelloWindow=Turbine.UI.Lotro.Window(); -- we call the constructor of the standard window object to create an instance
    HelloWindow:SetSize(200,200); -- sets the window size to 200 by 200 pixels
    HelloWindow:SetPosition(Turbine.UI.Display:GetWidth()/2-100,Turbine.UI.Display:GetHeight()/2-100); -- centers the window in the display
    HelloWindow:SetText("Hello World Window"); -- 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(180,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("Hello World"); -- sets the actual message text
    HelloWindow:SetVisible(true); -- display the window (windows are not visible by default)
    After creating the files, load the game and type "/plugins list". If you created the files in the correct location, you will see an entry for HelloWorld (1.00) in the list. This is a good first check to be sure that the files are in the correct location. If they are not, be sure to double check the path you are using is your documents folder, NOT the Program Files folder where the LoTRO programs are installed.

    Once you have verified that the plugin is in the list, enter "/plugins load HelloWorld". Note that while most things in Lua are case sensitive, the name of the plugin in the command is not. If you created the files correctly, you will be rewarded with a simple window displayed in the center of your display with the title, "Hello World", a border, a close button and most importantly the message "Hello World" in the middle of the window.
    Last edited by Garan; Jun 27 2022 at 07:25 PM. Reason: fixed missing import statement reported by Farothhen

  7. #7
    Join Date
    Mar 2007
    Posts
    1,590

    Programming with Class

    Lua is not really an object oriented language, but by creating a 'Class' object, Turbine has created a wrapper that makes Lua feel a little more object oriented. The class.lua file can be found in the Turbine samples but you should not depend on end users having the Turbine samples installed so it is usually best to include a copy in your own project folder and reference it from there.

    In the HelloWorld sample, I created an instance of a window by simply calling the constructor of that class:
    Code:
     HelloWindow=Turbine.UI.Lotro.Window()
    By using the class function, you can create a new class which inherits from an existing class and then create instances as needed. To create a "hello world" window class you would instead use:
    Code:
     import "Turbine"; -- this imports the base Turbine namespace (needed for the objects under 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 be extending
     import "YourName.HelloWorld.Class"
     HelloWindow=class(Turbine.UI.Lotro.Window)
     function HelloWindow:Constructor(x,y)
      if x==nil or y==nil then
       x=Turbine.UI.Display:GetWidth()/2-100;
       y=Turbine.UI.Display:GetHeight()/2-100;
      end
      Turbine.UI.Lotro.Window.Constructor(self);
      self:SetSize(200,200); -- sets the window size to 200 by 200 pixels
      self:SetPosition(x,y);
      self:SetText("Hello World Window"); -- assigns the title bar text
      self.Message=Turbine.UI.Label(); -- create a label control to display our message
      self.Message:SetParent(self); -- sets the label as a child of the main window
      self.Message:SetSize(180,20); -- sets the message size
      self.Message:SetPosition(10,90); -- places the message control in the vertical middle of the window with a 10 pixel left and right border
      self.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- centers the text in the message control both horizontally and vertically
      self.Message:SetText("Hello World"); -- sets the actual message text
      self:SetVisible(true); -- display the window (windows are not visible by default)
     end
    Notice that I made a small modification by including optional x,y coordinates in the constructor. Now you can create as many instances of the class as you'd like, creating them at different screen coordinates.
    Code:
     window1=HelloWindow(10,40);
     window2=HelloWindow(); -- will default to the screen center
     window3=HelloWindow(40,200);
    The above code will result in 3 instances of the "Hello World" window class at 3 different locations on your screen.
    Last edited by Garan; Jun 27 2022 at 07:31 PM. Reason: fixed missing import statements reported by Farothhen

  8. #8
    Join Date
    Mar 2007
    Posts
    1,590

    Function Calling or Method to the Madness

    Functions can be referenced either as Object.Function() or Object:Function(). This can be a bit confusing at first, but the biggest difference is that when you call the function with the ":" the object making the call is automatically prepended to the parameter list. Functions can also be overridden. For instance a Window can over ride the :SetPosition() method if it needs functionality not handled by the base class Window:SetPosition() method (or any other method). For example:
    Code:
     import "Turbine"; -- the base Turbine namespace
     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
     import "YourName.HelloWorld.Class" -- this will import Turbine's class function
     HelloWindow=class(Turbine.UI.Lotro.Window)
     function HelloWindow:Constructor(x,y)
      if x==nil or y==nil then
       x=Turbine.UI.Display:GetWidth()/2-100;
       y=Turbine.UI.Display:GetHeight()/2-100;
      end
      Turbine.UI.Lotro.Window.Constructor(self);
      -- override the built in SetPosition, adding a simple output statement. Obviously, there are many greater uses for this...
      self.SetPosition=function(sender, left, top)
       Turbine.UI.Window.SetPosition(self, left, top); -- pass the args on to the base class built in function
       Turbine.Shell.WriteLine("You positioned this instance at ("..tostring(left)..","..tostring(top)..")");
       -- perform some additional code here
      end
      self:SetSize(200,200); -- sets the window size to 200 by 200 pixels
      self:SetPosition(x,y);
      self:SetText("Hello World Window"); -- assigns the title bar text
      self.Message=Turbine.UI.Label(); -- create a label control to display our message
      self.Message:SetParent(self); -- sets the label as a child of the main window
      self.Message:SetSize(180,20); -- sets the message size
      self.Message:SetPosition(10,90); -- places the message control in the vertical middle of the window with a 10 pixel left and right border
      self.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- centers the text in the message control both horizontally and vertically
      self.Message:SetText("Hello World"); -- sets the actual message text
      self:SetVisible(true); -- display the window (windows are not visible by default)
     end
     window1=HelloWindow(10,40);
     window2=HelloWindow(); -- will default to the screen center
     window3=HelloWindow(40,200);
    The above code will not only result in three windows being created, but their initial positions will be displayed in the Standard chat channel. Not a really thrilling example, but hopefully you get the idea that base class methods can be overridden in this way (you can suppress the base functionality by simply not calling the base class method inside the new function).

    Note that you could replace the line:
    Code:
     self:SetPosition(x,y);
    with
    Code:
     self.SetPosition(self,x,y);
    and get the same result.
    Last edited by Garan; Jun 27 2022 at 07:31 PM.

  9. #9
    Join Date
    Mar 2007
    Posts
    1,590

    Plugin State and Saved Data

    At the very least, most plugins will want to store user preferences. In an attempt to reduce potential botting and real-time communication with external applications, Turbine limited file access to .plugindata files and implemented a delay on real-time data saving and loading. While this mechanism can be frustrating at times, it can be made to work for the vast majority of applications.

    There are three very important states that a plugin goes through in its life cycle. I refer to these as the loading state, the running state and the unloading state. The loading state is the period between when the "/plugins load " command is issued and when the main Lua file is completely loaded. Once the main Lua file is loaded and until the plugin's apartment is Unloaded the plugin is in its running state. If the user executes a "/plugins unload ApartmentName" command (where ApartmentName is blank or matches the specific plugin's apartment) or another plugin unloads the plugin's apartment programatically the plugin enters the unloading state which will end when the plugin is terminated.

    The PluginData.Load() method normally requires three parameters, dataScope, key, and dataLoadEventHandler where dataLoadEventHandler is a callback function that is called once the delay time has passed. However, when a plugin is in the loading or unloading state, the dataLoadEventHandler is not passed since the Plugin.Load() method will return the results immediately. The same is true for the PluginData.Save() method. Many plugins require real-time data access to their own saved data in order to function efficiently - for instance MoorMap has tens of thousands of data entries which would cause a major lag spike whenever the user changed maps if they were all retained in memory at once. To bypass the restriction, it is necessary to use a second plugin to manage loading and unloading the plugin as necessary, allowing it to enter the loading and unloading states as needed to access its saved data.

    If you ever get the error message "The data load event handler must be specified and a valid function" it means that you called the PluginData.Load() method during the running state (after the plugin completed loading) without the dataLoadEventHandler parameter.
    Last edited by Garan; Nov 05 2011 at 06:44 PM.

  10. #10
    Join Date
    Mar 2007
    Posts
    1,590

    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/downlo...anclients.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:
    Code:
    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
    Code:
     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 )
    Code:
     import "Turbine"; -- the base Turbine namespace
     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)
    Last edited by Garan; Jun 27 2022 at 07:32 PM. Reason: typo

  11. #11
    Join Date
    Mar 2007
    Posts
    1,590

    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:
    Code:
     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.p...I.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.p...th-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.p...eration-values...
    The Turbine.Gameplay.ItemClass enumeration is incomplete. Some additional values (currently also outdated) can be found at http://forums.lotro.com/showthread.p...ry-enumeration
    The Turbine.UI.Lotro.Action enumeration is incomplete. Some additional values can be found at http://forums.lotro.com/showthread.p...I.Lotro.Action
    The Turbine.UI.Lotro.Font enumeration is incomplete. Some additional values can be found at http://forums.lotro.com/showthread.p...th-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.
    Code:
    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
    Code:
    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.
    Last edited by Garan; Jan 18 2015 at 12:52 PM. Reason: typos

  12. #12
    Join Date
    Mar 2007
    Posts
    1,590

    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.
    Last edited by Garan; Nov 05 2011 at 06:48 PM.

  13. #13
    Join Date
    Mar 2007
    Posts
    1,590

    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":
    Code:
    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
    Last edited by Garan; Oct 29 2011 at 10:25 PM.

  14. #14
    Join Date
    Mar 2007
    Posts
    1,590

    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:
    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):
    Code:
    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:
    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:
    Code:
     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.
    Code:
    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:
    Code:
    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.
    * there was something else that was supposed to go here but my daughter wanted to show me her latest crayon creation and I totally lost track of what I was writing... it may someday get added *
    Last edited by Garan; Nov 24 2011 at 10:17 AM.

  15. #15
    Join Date
    Mar 2007
    Posts
    1,590

    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).
    Last edited by Garan; Oct 29 2011 at 10:27 PM.

  16. #16
    Join Date
    Mar 2007
    Posts
    1,590

    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.

  17. #17

    Re: Writing LoTRO Lua Plugins for Noobs

    Holy cow, you just wrote the book! (You also gave away some of the coolest secrets. Thanks for being generous enough to share them so clearly!)

    Awesome work, Garan!

  18. #18
    Join Date
    Jun 2011
    Posts
    1,232

    Re: Writing LoTRO Lua Plugins for Noobs

    Wow yes very nice. I now feel ashamed by noddy plugin
    Evernight - Walred (Champ), Walmur (RK), Walbert-2 (Cappy)

  19. #19
    Join Date
    Mar 2007
    Posts
    1,590

    Re: Writing LoTRO Lua Plugins for Noobs

    Thanks for the feedback guys. I rushed to post that initial stuff because we had a nasty storm on the way. Unfortunately the samples didn't get posted yet but at least all of the basic stuff is there. I have no power and my regular internet access is toast for at least a week, but I will get more samples posted when I get back running again. Gotta love a foot of snow before Halloween even hits

  20. #20
    Join Date
    Aug 2010
    Posts
    3,418

    Re: Writing LoTRO Lua Plugins for Noobs

    This is what "sticky" was invented for, hopefully it'll be applied. +rep too.



    "Sometimes survival comes down to not being hit. Actually, most times." -the chicken skill, Bob and Weave
    Link to our community LOTRO store google spreadsheet pricelist and conversion rates, please contribute too!: https://goo.gl/wxPqCm

  21. #21
    Join Date
    Mar 2007
    Posts
    1,590

    Re: Writing LoTRO Lua Plugins for Noobs

    Press my Buttons - I dare ya!

    This section is really targetted at beginners who may not be familliar with creating buttons and may need a bit of help getting started. The final code example does include a nifty trick for displaying a Quickslot control as if it were a button that more advanced users may find useful.

    As you can imagine, a plugin without buttons has limited use. There are some, like Wallet, which are solely there to provide a display of existing data, but most plugins at some point or other require interaction from a user. There are many ways to generate a UI element which the user can interact with by clicking the mouse. I will only cover three of them here, the Lotro Button, the standard Button and the generic Control. Each has it's benefits and it's drawbacks:

    Lotro Button - instance of Turbine.UI.Lotro.Button
    Advantages - automatically works with user skins. Handles text. Has good default functionality
    Drawbacks - will automatically generate graphics whether you need them or not, not as easy to use for mixing graphics and text. Minor unnecessary overhead if all you need is to catch a mouse click.

    standard Button - instance of Turbine.UI.Button
    Advantages - has all of the necessary default functionality of a button and allows easy use of custom button graphics or text.
    Drawbacks - you must provide your own graphics if you want them. Minor unnecessary overhead if all you need is to catch a mouse click. Will only work with skins if your graphics use ingame resource IDs instead of custom images that you provide

    generic Control - instance of Turbine.UI.Control
    Advantages - customizable graphic container with the least amount of overhead.
    Drawbacks - does not support text without inclusion of a child control. Developer must provide all graphics. Will only work with skins if your graphics use ingame resource IDs instead of custom images that you provide

    As you can see, as you add capability such as supporting text and user skins, you also inherit more overhead. This usually isn't an issue unless you are making an array with many (hundreds?) of buttons but it's still a good practice to use the least complicated tool for the job (less chance of incorporating bugs and other unwanted behaviors).

    So, how do we create a button and what can we do with it? In it's simplest form, a button is an area on the UI which will respond to mouse clicks by performing an action. We normally also consider buttons to have the behavior of looking as though they have been "pressed" when the mouse clicks on them. Lastly, we usually expect buttons to highlight or otherwise indicate when the mouse is moved over them. All of these behaviors can be achieved with all three types of buttons, it just requires a bit more work with the more generic ones. To start with, just create a standard button that says "Press Me". To do this, we will want to use either the Lotro Button or the standard Button since the generic Control does not support text without some additional help. We will create a window with two buttons and you can see how they differ, especially if you switch user skins.
    Code:
    import "Turbine" -- needed for the Shell.WriteLine
    import "Turbine.UI" -- needed for the standard Button
    import "Turbine.UI.Lotro" -- needed for the Lotro.Window and the Lotro.Button
    myWindow=Turbine.UI.Lotro.Window(); -- we will simply create an instance of the base class since we don't need more than one window and only need the default functionality
    myWindow:SetSize(400,400); -- give ourselves lots of room to play in ;)
    myWindow:SetPosition((Turbine.UI.Display:GetWidth()-myWindow:GetWidth())/2,(Turbine.UI.Display:GetHeight()-myWindow:GetHeight())/2); -- center window (I don't hardcode the height and width so that I can change them easier as I modify the project)
    myWindow:SetText("BUTTONS!"); -- set window title
    -- create labels for our buttons (I use an array just to save typing)
    local tmpIndex;
    myWindow.ButtonLabels={};
    for tmpIndex=1,2 do
        myWindow.ButtonLabels[tmpIndex]=Turbine.UI.Label();
        myWindow.ButtonLabels[tmpIndex]:SetParent(myWindow);
        myWindow.ButtonLabels[tmpIndex]:SetSize(190,20);
        myWindow.ButtonLabels[tmpIndex]:SetPosition(5,tmpIndex*25+20); -- position the labels in a column down the left of the window
    end
    myWindow.ButtonLabels[1]:SetText("Lotro.Button");
    myWindow.ButtonLabels[2]:SetText("Standard Button");
    -- here's the Lotro.Button
    myWindow.SampleButton1=Turbine.UI.Lotro.Button();
    myWindow.SampleButton1:SetParent(myWindow);
    myWindow.SampleButton1:SetSize(190,20); --note that the height & width of a Lotro.Button will automatically correct itself to fit the minimum decorations
    myWindow.SampleButton1:SetPosition(200,myWindow.ButtonLabels[1]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton1:SetText("Press Me!");
    -- provide a simplistic click handler
    myWindow.SampleButton1.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the Lotro.Button!");
    end
    myWindow.SampleButton2=Turbine.UI.Button();
    myWindow.SampleButton2:SetParent(myWindow);
    myWindow.SampleButton2:SetSize(147,20); -- the graphic we are using happen to be 147x20. if you use custom graphics you would set the button size to whatever size the graphic is
    myWindow.SampleButton2:SetPosition(200,myWindow.ButtonLabels[2]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton2:SetText("Press Me Too!");
    -- provide a simplistic click handler
    myWindow.SampleButton2.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the standard Button!");
    end
    myWindow:SetVisible(true); -- display the window
    The first thing you should notice is that the standard button doesn't look like a button at all. The reason is that it has no default "decorations", that is there's no border. Additionally, when the mouse moves over the Lotro.Button it highlights and when the mouse is pressed it appears to "press" while the standard button does not. In order to provide the standard button with this kind of functionality, we would have to provide three background images and manually control setting the background. I happen to know the resouce IDs for some in-game button backgrounds that we can use instead of creating our own images so I will use them, but you could easily replace them with custom images by providing the path to SetBackground() instead of a resource ID.

    So, let's modify our sample to allow the standard button to behave a bit more like a Lotro.Button:
    Code:
    import "Turbine" -- needed for the Shell.WriteLine
    import "Turbine.UI" -- needed for the standard Button
    import "Turbine.UI.Lotro" -- needed for the Lotro.Window and the Lotro.Button
    myWindow=Turbine.UI.Lotro.Window(); -- we will simply create an instance of the base class since we don't need more than one window and only need the default functionality
    myWindow:SetSize(400,400); -- give ourselves lots of room to play in ;)
    myWindow:SetPosition((Turbine.UI.Display:GetWidth()-myWindow:GetWidth())/2,(Turbine.UI.Display:GetHeight()-myWindow:GetHeight())/2); -- center window (I don't hardcode the height and width so that I can change them easier as I modify the project)
    myWindow:SetText("BUTTONS!"); -- set window title
    -- create labels for our buttons (I use an array just to save typing)
    local tmpIndex;
    myWindow.ButtonLabels={};
    for tmpIndex=1,2 do
        myWindow.ButtonLabels[tmpIndex]=Turbine.UI.Label();
        myWindow.ButtonLabels[tmpIndex]:SetParent(myWindow);
        myWindow.ButtonLabels[tmpIndex]:SetSize(190,20);
        myWindow.ButtonLabels[tmpIndex]:SetPosition(5,tmpIndex*25+20); -- position the labels in a column down the left of the window
    end
    myWindow.ButtonLabels[1]:SetText("Lotro.Button");
    myWindow.ButtonLabels[2]:SetText("Standard Button");
    -- here's the Lotro.Button
    myWindow.SampleButton1=Turbine.UI.Lotro.Button();
    myWindow.SampleButton1:SetParent(myWindow);
    myWindow.SampleButton1:SetSize(190,20); --note that the height & width of a Lotro.Button will automatically correct itself to fit the minimum decorations
    myWindow.SampleButton1:SetPosition(200,myWindow.ButtonLabels[1]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton1:SetText("Press Me!");
    -- provide a simplistic click handler
    myWindow.SampleButton1.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the Lotro.Button!");
    end
    myWindow.SampleButton2=Turbine.UI.Button();
    myWindow.SampleButton2:SetParent(myWindow);
    myWindow.SampleButton2:SetSize(190,20);
    myWindow.SampleButton2:SetPosition(200,myWindow.ButtonLabels[2]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton2:SetText("Press Me Too!");
    -- provide a simplistic click handler
    myWindow.SampleButton2.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the standard Button!");
    end
    -- this is the code that handles the background
    myWindow.SampleButton2:SetBlendMode(Turbine.UI.BlendMode.Overlay); -- this will cause any transparent parts of the background image to display your window's background instead of being completely transparent and displaying the game UI display
    myWindow.SampleButton2:SetBackground(0x410001a5)
    myWindow.SampleButton2.MouseEnter=function()
        myWindow.SampleButton2:SetBackground(0x410001a6)
    end
    myWindow.SampleButton2.MouseLeave=function()
        myWindow.SampleButton2:SetBackground(0x410001a5)
    end
    myWindow.SampleButton2.MouseDown=function()
        myWindow.SampleButton2:SetBackground(0x410001aa)
    end
    myWindow.SampleButton2.MouseUp=function()
        myWindow.SampleButton2:SetBackground(0x410001a6)
    end
    myWindow:SetVisible(true); -- display the window
    This time, the standard button has a background and will highlight and press with the mouse as expected. The only difference you should have noticed is the size - we had to set the button to the fixed size of the graphic - and the font color. The Lotro.Button inherited the font color of the skin, while our standard button has to have it's font color set by the developer.

    So far, we have wanted to display text on our buttons. If we want to display both text and a graphic, then the standard Button is probaby the best way to go since it supports text and you can assign your own graphics easily. If you only want to display a graphic, then you should probably use a generic Control object since you do not need text support. Here's a sample showing each of the three options, text only, text and graphic, and graphic only. This sample will use other built in image resources, in this case the "left arrow" button:
    Code:
    import "Turbine" -- needed for the Shell.WriteLine
    import "Turbine.UI" -- needed for the standard Button
    import "Turbine.UI.Lotro" -- needed for the Lotro.Window and the Lotro.Button
    myWindow=Turbine.UI.Lotro.Window(); -- we will simply create an instance of the base class since we don't need more than one window and only need the default functionality
    myWindow:SetSize(400,400); -- give ourselves lots of room to play in ;)
    myWindow:SetPosition((Turbine.UI.Display:GetWidth()-myWindow:GetWidth())/2,(Turbine.UI.Display:GetHeight()-myWindow:GetHeight())/2); -- center window (I don't hardcode the height and width so that I can change them easier as I modify the project)
    myWindow:SetText("BUTTONS!"); -- set window title
    -- create labels for our buttons (I use an array just to save typing)
    local tmpIndex;
    myWindow.ButtonLabels={};
    for tmpIndex=1,3 do
        myWindow.ButtonLabels[tmpIndex]=Turbine.UI.Label();
        myWindow.ButtonLabels[tmpIndex]:SetParent(myWindow);
        myWindow.ButtonLabels[tmpIndex]:SetSize(190,20);
        myWindow.ButtonLabels[tmpIndex]:SetPosition(5,tmpIndex*25+20); -- position the labels in a column down the left of the window
    end
    myWindow.ButtonLabels[1]:SetText("Lotro.Button");
    myWindow.ButtonLabels[2]:SetText("Standard Button");
    myWindow.ButtonLabels[3]:SetText("Generic Control");
    -- here's the Lotro.Button
    myWindow.SampleButton1=Turbine.UI.Lotro.Button();
    myWindow.SampleButton1:SetParent(myWindow);
    myWindow.SampleButton1:SetSize(190,20); --note that the height & width of a Lotro.Button will automatically correct itself to fit the minimum decorations
    myWindow.SampleButton1:SetPosition(200,myWindow.ButtonLabels[1]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton1:SetText("Press Me!");
    -- provide a simplistic click handler
    myWindow.SampleButton1.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the Lotro.Button!");
    end
    myWindow.SampleButton2=Turbine.UI.Button();
    myWindow.SampleButton2:SetParent(myWindow);
    myWindow.SampleButton2:SetSize(190,20);
    myWindow.SampleButton2:SetPosition(200,myWindow.ButtonLabels[2]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton2:SetText("Press Me Too!");
    -- provide a simplistic click handler
    myWindow.SampleButton2.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the standard Button!");
    end
    -- this is the code that handles the background
    myWindow.SampleButton2:SetBlendMode(Turbine.UI.BlendMode.Overlay); -- this will cause any transparent parts of the background image to display your window's background instead of being completely transparent and displaying the game UI display
    myWindow.SampleButton2:SetBackground(0x410001c8)
    myWindow.SampleButton2.MouseEnter=function()
        myWindow.SampleButton2:SetBackground(0x410001c9)
    end
    myWindow.SampleButton2.MouseLeave=function()
        myWindow.SampleButton2:SetBackground(0x410001c8)
    end
    myWindow.SampleButton2.MouseDown=function()
        myWindow.SampleButton2:SetBackground(0x410001ca)
    end
    myWindow.SampleButton2.MouseUp=function()
        myWindow.SampleButton2:SetBackground(0x410001c9)
    end
    -- the generic button control
    myWindow.SampleButton3=Turbine.UI.Control();
    myWindow.SampleButton3:SetParent(myWindow);
    myWindow.SampleButton3:SetSize(20,20);
    myWindow.SampleButton3:SetPosition(200,myWindow.ButtonLabels[3]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    -- provide a simplistic click handler
    myWindow.SampleButton3.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the generic Control!");
    end
    -- this is the code that handles the background
    myWindow.SampleButton3:SetBlendMode(Turbine.UI.BlendMode.Overlay); -- this will cause any transparent parts of the background image to display your window's background instead of being completely transparent and displaying the game UI display
    myWindow.SampleButton3:SetBackground(0x410001c8)
    myWindow.SampleButton3.MouseEnter=function()
        myWindow.SampleButton3:SetBackground(0x410001c9)
    end
    myWindow.SampleButton3.MouseLeave=function()
        myWindow.SampleButton3:SetBackground(0x410001c8)
    end
    myWindow.SampleButton3.MouseDown=function()
        myWindow.SampleButton3:SetBackground(0x410001ca)
    end
    myWindow.SampleButton3.MouseUp=function()
        myWindow.SampleButton3:SetBackground(0x410001c9)
    end
    myWindow:SetVisible(true); -- display the window
    Now that you know how to create buttons that perform basic interactions, let's look at something slightly different. Suppose you want to have a button that performs the same functionality as a quickslot. You can't assign an image to a Quickslot control and you can't programmatically use Items, Skills or Aliases in a button event handler. The solution is to be sneaky. What we do is hide a Quickslot under a Control and let the mouse events pass through to the Quickslot:
    Code:
    import "Turbine" -- needed for the Shell.WriteLine
    import "Turbine.UI" -- needed for the standard Button
    import "Turbine.UI.Lotro" -- needed for the Lotro.Window and the Lotro.Button
    import "Turbine.Gameplay" -- needed for the player name
    myWindow=Turbine.UI.Lotro.Window(); -- we will simply create an instance of the base class since we don't need more than one window and only need the default functionality
    myWindow:SetSize(400,400); -- give ourselves lots of room to play in ;)
    myWindow:SetPosition((Turbine.UI.Display:GetWidth()-myWindow:GetWidth())/2,(Turbine.UI.Display:GetHeight()-myWindow:GetHeight())/2); -- center window (I don't hardcode the height and width so that I can change them easier as I modify the project)
    myWindow:SetText("BUTTONS!"); -- set window title
    -- create labels for our buttons (I use an array just to save typing)
    local tmpIndex;
    myWindow.ButtonLabels={};
    for tmpIndex=1,4 do
        myWindow.ButtonLabels[tmpIndex]=Turbine.UI.Label();
        myWindow.ButtonLabels[tmpIndex]:SetParent(myWindow);
        myWindow.ButtonLabels[tmpIndex]:SetSize(190,20);
        myWindow.ButtonLabels[tmpIndex]:SetPosition(5,tmpIndex*25+20); -- position the labels in a column down the left of the window
    end
    myWindow.ButtonLabels[1]:SetText("Lotro.Button");
    myWindow.ButtonLabels[2]:SetText("Standard Button");
    myWindow.ButtonLabels[3]:SetText("Generic Control");
    myWindow.ButtonLabels[4]:SetText("Quickslot button");
    -- here's the Lotro.Button
    myWindow.SampleButton1=Turbine.UI.Lotro.Button();
    myWindow.SampleButton1:SetParent(myWindow);
    myWindow.SampleButton1:SetSize(190,20); --note that the height & width of a Lotro.Button will automatically correct itself to fit the minimum decorations
    myWindow.SampleButton1:SetPosition(200,myWindow.ButtonLabels[1]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton1:SetText("Press Me!");
    -- provide a simplistic click handler
    myWindow.SampleButton1.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the Lotro.Button!");
    end
    myWindow.SampleButton2=Turbine.UI.Button();
    myWindow.SampleButton2:SetParent(myWindow);
    myWindow.SampleButton2:SetSize(190,20);
    myWindow.SampleButton2:SetPosition(200,myWindow.ButtonLabels[2]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton2:SetText("Press Me Too!");
    -- provide a simplistic click handler
    myWindow.SampleButton2.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the standard Button!");
    end
    -- this is the code that handles the background
    myWindow.SampleButton2:SetBlendMode(Turbine.UI.BlendMode.Overlay); -- this will cause any transparent parts of the background image to display your window's background instead of being completely transparent and displaying the game UI display
    myWindow.SampleButton2:SetBackground(0x410001c8)
    myWindow.SampleButton2.MouseEnter=function()
        myWindow.SampleButton2:SetBackground(0x410001c9)
    end
    myWindow.SampleButton2.MouseLeave=function()
        myWindow.SampleButton2:SetBackground(0x410001c8)
    end
    myWindow.SampleButton2.MouseDown=function()
        myWindow.SampleButton2:SetBackground(0x410001ca)
    end
    myWindow.SampleButton2.MouseUp=function()
        myWindow.SampleButton2:SetBackground(0x410001c9)
    end
    -- the generic button control
    myWindow.SampleButton3=Turbine.UI.Control();
    myWindow.SampleButton3:SetParent(myWindow);
    myWindow.SampleButton3:SetSize(20,20);
    myWindow.SampleButton3:SetPosition(200,myWindow.ButtonLabels[3]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    -- provide a simplistic click handler
    myWindow.SampleButton3.MouseClick=function()
        Turbine.Shell.WriteLine("You pressed the generic Control!");
    end
    -- this is the code that handles the background
    myWindow.SampleButton3:SetBlendMode(Turbine.UI.BlendMode.Overlay); -- this will cause any transparent parts of the background image to display your window's background instead of being completely transparent and displaying the game UI display
    myWindow.SampleButton3:SetBackground(0x410001c8)
    myWindow.SampleButton3.MouseEnter=function()
        myWindow.SampleButton3:SetBackground(0x410001c9)
    end
    myWindow.SampleButton3.MouseLeave=function()
        myWindow.SampleButton3:SetBackground(0x410001c8)
    end
    myWindow.SampleButton3.MouseDown=function()
        myWindow.SampleButton3:SetBackground(0x410001ca)
    end
    myWindow.SampleButton3.MouseUp=function()
        myWindow.SampleButton3:SetBackground(0x410001c9)
    end
    -- get the localPlayerName for later...
    local localPlayerName=Turbine.Gameplay.LocalPlayer:GetInstance():GetName();
    -- the quickslot button!
    myWindow.SampleButton4=Turbine.UI.Lotro.Quickslot();
    myWindow.SampleButton4:SetParent(myWindow);
    myWindow.SampleButton4:SetSize(20,20);
    myWindow.SampleButton4:SetPosition(200,myWindow.ButtonLabels[4]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton4:SetShortcut(Turbine.UI.Lotro.Shortcut(Turbine.UI.Lotro.ShortcutType.Alias,"/tell "..localPlayerName.." OUCH, that HURT! :p"))
    myWindow.SampleButton4.ShortcutData="/tell "..localPlayerName.." OUCH, that HURT! :p"; --save the alias text for later
    myWindow.SampleButton4:SetAllowDrop(false); -- turn off drag and drop so the user doesn't accidentally modify our button action
    myWindow.SampleButton4.DragDrop=function()
        -- even though we turned off drop operations, there is a bug that allows the quickslot to drop on itself effectively wiping out the shortcut
        local sc=Turbine.UI.Lotro.Shortcut(Turbine.UI.Lotro.ShortcutType.Alias,"");
        sc:SetData(myWindow.SampleButton4.ShortcutData);
        myWindow.SampleButton4:SetShortcut(sc);
    end
    myWindow.SampleButton4.Backdrop=Turbine.UI.Control(); -- note, if the icon has no transparencies then this backdrop is not needed
    myWindow.SampleButton4.Backdrop:SetParent(myWindow);
    myWindow.SampleButton4.Backdrop:SetSize(20,20);
    myWindow.SampleButton4.Backdrop:SetPosition(200,myWindow.ButtonLabels[4]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton4.Backdrop:SetZOrder(myWindow.SampleButton4:GetZOrder()+1); -- force the icon to be displayed above the quickslot
    myWindow.SampleButton4.Backdrop:SetBackColor(Turbine.UI.Color(1,0,0,0));
    myWindow.SampleButton4.Backdrop:SetMouseVisible(false); -- prevent the icon from interacting with the mouse so that all mouse events fall through to the quickslot behind it
    myWindow.SampleButton4.Icon=Turbine.UI.Control();
    myWindow.SampleButton4.Icon:SetParent(myWindow);
    myWindow.SampleButton4.Icon:SetSize(20,20);
    myWindow.SampleButton4.Icon:SetPosition(200,myWindow.ButtonLabels[4]:GetTop()); --yeah, I got lazy and hardcoded the left side ;)
    myWindow.SampleButton4.Icon:SetZOrder(myWindow.SampleButton4:GetZOrder()+2); -- force the icon to be displayed above the quickslot
    myWindow.SampleButton4.Icon:SetMouseVisible(false); -- prevent the icon from interacting with the mouse so that all mouse events fall through to the quickslot behind it
    myWindow.SampleButton4.Icon:SetBlendMode(Turbine.UI.BlendMode.Overlay); -- this will cause any transparent parts of the background image to display your window's background instead of being completely transparent and displaying the game UI display
    -- this is the code that handles the background, note that the handles are assigned to the quickslot but they manipulate the icon
    myWindow.SampleButton4.Icon:SetBackground(0x410001c8)
    myWindow.SampleButton4.MouseEnter=function()
        myWindow.SampleButton4.Icon:SetBackground(0x410001c9)
    end
    myWindow.SampleButton4.MouseLeave=function()
        myWindow.SampleButton4.Icon:SetBackground(0x410001c8)
    end
    myWindow.SampleButton4.MouseDown=function()
        myWindow.SampleButton4.Icon:SetBackground(0x410001ca)
    end
    myWindow.SampleButton4.MouseUp=function()
        myWindow.SampleButton4.Icon:SetBackground(0x410001c9)
    end
    myWindow:SetVisible(true); -- display the window
    One important aspect of the Quickslot button is that we have to prevent the user from accidentally dragging another shortcut onto our button or accidentally wiping out our shortcut by dragging it out of the quickslot. We protect it by turning off Drag/Drop operations and then fixing the glitch that allows it to drop on itself by reassigning the shortcut in the DragDrop event handler.

    Now you know lots of fun ways to make things go Click. Bear in mind that any object that is derived from the base Control object will inherit the abilty to display a graphic and respond to mouseclicks so there are many, many more ways to create buttons, but hopefully you have a better idea now of how to create one of the basic UI elements.
    Last edited by Garan; Dec 01 2011 at 12:07 PM.

  22. #22
    Join Date
    May 2009
    Posts
    0

    Re: Writing LoTRO Lua Plugins for Noobs

    I thought this was supposed to be for noobs haha.

    Nice work man, many thanks.

    *So over my head*

  23. #23
    Join Date
    Jun 2007
    Posts
    206

    Re: Writing LoTRO Lua Plugins for Noobs

    This has been really helpful. Two things that I think would be helpful additions:

    1. How to parse text that's coming in over the various channels
    2. How to save/load data (there's some discussion about when you would save/load, but not much about the rules on how to save/load)

    Of course, there are other ways to figure these out (which I've been pursuing!), but I thought I'd mention them as other things that I think people will be interested in learning.

    Anyway, Garan, you're my hero.

  24. #24
    Join Date
    Mar 2007
    Posts
    1,590

    Re: Writing LoTRO Lua Plugins for Noobs

    Quote Originally Posted by Hipo View Post
    I thought this was supposed to be for noobs haha.

    Nice work man, many thanks.

    *So over my head*
    Quote Originally Posted by Zelxyb View Post
    This has been really helpful. Two things that I think would be helpful additions:

    1. How to parse text that's coming in over the various channels
    2. How to save/load data (there's some discussion about when you would save/load, but not much about the rules on how to save/load)

    Of course, there are other ways to figure these out (which I've been pursuing!), but I thought I'd mention them as other things that I think people will be interested in learning.

    Anyway, Garan, you're my hero.
    Thanks for the nice comments and feedback.

    Hipo, if any of it seems over your head then I need to rewrite something. Send me a PM if there's something in the original sections that isn't clear or doesn't follow a good progression and gets confusing - I peridodically go back and refine the original stuff, for instance I recently added a bit about firing events in the event handler post. The samples (everything after the "Where to go from here post") are expected to be a little tougher to grasp, but with the code provided I hope people will take the time to work through anything they don't grasp right away by modifying the code, adding their own comments etc. until they do understand it. All of the code samples should be functional as they have actually been created and tested, either as whole plugins or through my debug window, so I hope that if people don't immediatly understand them that they create their own copies of the samples and then dissect them by adding their own WriteLine statements or other tweaks until they are comfortable with the idea being presented. Another point to bear in mind is that there typically is more than one way to approach a problem in Lua and the information presented here is just one (hopefully relatively simple) way of doing it. As I mentioned early on, one of the best ways to learn Lua is to take someone else's code and play with it until you understand what it's doing.

    Zelxyb, I will be touching on the Chat object in the next installment, "It's my party but I'll cry if I want to..." (three guesses what THAT one covers ) but not in detail yet, that will be in a more advanced topic. I haven't decided on how I want to cover Saving/Loading in a sample yet, but it will certainly rear it's ugly head eventually - ok, maybe ugly is a bit harsh, but even its mother would be hard pressed to love it hmmm... that gives me an idea...
    Last edited by Garan; Nov 25 2011 at 11:07 AM.

  25. #25
    Join Date
    Mar 2007
    Posts
    1,590

    It's my Party and I'll cry if I want to...

    ...or An Introduction to Apartments and Child Plugins with a smattering of Chat monitoring.

    This (rather large) installment covers some advanced functionality but hopefully in a simple enough format for even Noobs to follow. The sample plugin discussed here can be downloaded from LoTROInterface at http://www.lotrointerface.com/downlo...nfo.php?id=652

    So... we were all excited to learn that we now had an object that we could use to access our Party. Then we found that it didn't quite work as advertised Specifically, the MemberAdded and MemberRemoved events don't always fire and the member list gets corrupted. To get around this, we will delve into two interesting and useful concepts, child plugins and cross Apartment communication.

    Although there isn't really any parent/child relationship, I call a plugin a Child Plugin when it is programmatically loaded and unloaded to help with some specific task that requires a new environment or access to the loading/unloading state. This can be particularly helpful when working around a built-in object like the Party or Backpack that gets corrupted or out of sync. By using a new Apartment, the child has access to a brand new, synchronized version of the object. Additionally, when you need to get real-time access to your own data, a Child Plugin can do that (or your "main" plugin can act like a child and be loaded/unloaded as needed by a handler or parent plugin).

    Cross Apartment communication is a bit tricky, but can be successfully achieved in several ways. The first is using the very existance of a plugin in the Plugins[] collection to signify that its loading is complete and some process can now safely continue - we will use this in this example. Another form of communication can be achieved using specially encoded shell commands. By creating a shell command with a specific name, information can be encoded and sent to another plugin in any apartment - this sample will show how to achieve this with party member names. A third form of communication can be achieved not only cross Apartment, but also cross client with the help of a user click. Simply create a quickslot that generates a chat message, usually a tell to a specific client, and have a plugin monitoring the chat messages on the recipient end and with the help of a user click, the plugins can send messages to each other via the in-game chat (the multi-player Cards game client uses this functionality in its Elevenses game - not yet published). There is a fourth option which is fairly easy to implement but can be intrusive/disruptive to the user. Anything written by the Turbine.Shell.WriteLine method will appear in the Standard chat channel and any plugin in any Apartment will receive this message if it has a Chat event handler monitoring the Standard channel. As mentioned though, if you are passing a lot of data or doing it frequently, this method can be very disruptive as it will flood the chat window (if the user has the Standard channel selected in their filters).

    So, how does a child plugin help get around the corruption of the Party object? Well, as I mentioned, each time a new environment is loaded, a whole new Party object is created and it is in synch with the game (basically the same as unloading and reloading our plugin to get back in synch). If a child plugin can be loaded in a new Apartment and the Party object information retrieved, it will be in synch without having to unload and reload ourselves. Once we retrieve the Party info, we can then unload the child apartment to save resources and then it can be loaded again as needed. Retrieving the Party info is where the cross-apartment communication comes in. Since we can't share global data (in this case, a good thing) we have to find another way of letting our parent know what we've found. To do this, we use two of the methods already mentioned, using the existance of the child in the Plugins[] collection to indicate that the data is available and using a shell command to publish and receive the actual data.

    To get started, we will need to create an author folder, plugin folder and two plugins, our main plugin and our child plugin, I will name them PartySample and MyParty respectively. For simplicity, I will literally call the author folder "YourName". Feel free to substitute your actual name wherever YourName appears in the following code. The plugin folder we will call "PartySample".

    In your "Plugins" folder, create a folder named "YourName". In your "Plugins/YourName" folder, create a folder named "PartySample". In your "Plugins/YourName/PartySample" folder, create a folder named "Resources". This last folder will hold any images we need, in this case just a pair of 32x32 icons for the plugins.

    Use your favorite image editor to create two 32x32 images, one for the main plugin icon, one for the child plugin icon, and save them as "main.jpg" and "child.jpg" in the "Plugins/YourName/PartySample/Resources" folder. These will be used by the Plugin Manager available in Update 5 or currently live on Bullroarer.

    In your "Plugins/YourName" folder, create the following two .plugin files:
    PartySample.plugin
    Code:
    <?xml version="1.0"?>
    <Plugin>
     <Information>
      <Name>PartySample</Name>
      <Author>Garan</Author>
      <Version>1.0</Version>
      <Description>This plugin sample displays the list of party name. This is just a sample to show how to use the MyParty plugin.</Description>
      <Image>YourName/PartySample/Resources/main.jpg</Image>
     </Information>
     <Package>YourName.PartySample.Main</Package>
    </Plugin>
    MyParty.plugin
    Code:
    <?xml version="1.0"?>
    <Plugin>
     <Information>
      <Name>MyParty</Name>
      <Author>Garan</Author>
      <Version>1.0</Version>
      <Description>This plugin is used internally by PartySample and should NOT be loaded by the user or plugin manager.</Description>
      <Image>YourName/PartySample/Resources/child.jpg</Image>
     </Information>
     <Package>YourName.PartySample.MyParty</Package>
     <Configuration Apartment="MyParty" />
    </Plugin>
    Note that the MyParty plugin includes a Configuration element with a distinct Apartment attribute, "MyParty". This will cause the MyParty plugin to get its own new environment when it is loaded.

    Now, create the MyParty.lua file in the "Plugins/YourName/PartySample" folder:
    Code:
    import "Turbine"
    import "Turbine.UI"
    import "Turbine.Gameplay"
    -- all this plugin does is provide a snapshot of the party names.
    -- Party Plugin loads this plugin, reads the party names and once complete it unloads this plugin.
    -- This helps alleviate some of the bugs in the Turbine Party object.
    local party=Turbine.Gameplay.LocalPlayer:GetInstance():GetParty();
    local lIndex;
    local shellCommand=Turbine.ShellCommand();
    local shellString="";
    local loaded=false;
    local member;
    if party~=nil then
     member=party:GetLeader(); -- retrieve the leader name
     -- the prefix starts with a "0" so that it will sort alphabetically to the beginning of the command list
     -- then next two letters "MP" just signify that it is from MyParty
     -- the "L" signifies the "Leader" name
     -- the second "0" is a placeholder to be consistent with the Member records that have an Index value
     -- the underscore is just a separator to make debugging easier
     -- the last part is the actual name
     -- this will match the string pattern "0MP([LM])%d+_(.+)" which has two "captures", the first is "[LM]" which indicates either an "L" or an "M" and the second, ".+", is one or more of any non-control character
     -- captures are used with the string.match command to fill variables with the portion of a string matching their pattern
     shellString="0MPL0_"..member:GetName();
     for lIndex=1,party:GetMemberCount() do
      -- we build a string with semi-colon separated "command" names, each command can have many names (I haven't found an upper limit) so we can generate all of our entries with a single actual shell command
      member=party:GetMember(lIndex);
      -- the only differences in these records is that the "L" is replaced with an "M" which will indicate a "Member" name and the second "0" is replaced with the actual index
      -- note that we don't actually use the index, but it is encoded anyway in case we find a future use for it
      shellString=shellString..";0MPM"..tostring(lIndex).."_"..tostring(member:GetName());
     end
     -- now we use that string with all of the encoded values to create shell commands entries tied to a single shell command object
     Turbine.Shell.AddCommand(shellString,shellCommand);
    end
    tmpWindow=Turbine.UI.Window()
    tmpWindow.Update=function()
     if (Plugins["MyParty"] ~= nil) and (not loaded) then
      loaded=true;
      Plugins["MyParty"].Unload = function(self,sender,args)
       -- after the plugin completes loading, we create the "Unload" event and use it to remove our one shell command
       Turbine.Shell.RemoveCommand(shellCommand)
      end
      tmpWindow:SetWantsUpdates(false);
     end
    end
    Read through the comments to see how the plugin actually encodes the Party member names and creates the shell commands.

    Now, create the lua file that provides the party wrapper functionality for the main plugin. This file is purposely written to be reusable in other plugins, so you can create copies of it in your own projects if you so desire. The party wrapper will create an object that gets initialized by loading the MyParty plugin, processing the shell commands that contain the Party member names, and then unloading the MyParty plugin, avoiding any ties to the environment that had the Party object. Once initialized, it uses a Chat event handler to watch for any messages pertaining to a party and updates it's membership list accordingly. If the character joins a party after the plugin is loaded, it simply resets the flags for the MyParty plugin and reprocesses it. If the character leaves their party or is dismissed, it clears all of the party info.

    This wrapper is locale aware, that is, it accounts for the different client messages based on whether you are running the EN,DE or FR client. The message patterns are stored in the ResStr table and are generated depending on the existance of the various versions of the "help" command.

    Another interesting point is that the wrapper can support multiple host applications - the wrapper's events can hold tables of functions the same way that Turbines events do so it supports the AddCallback and RemoveCallback functions.

    This file should be saved in the "Plugins/YourName/PartySample" folder as PartyWrapper.lua
    Code:
    import "Turbine";
    import "Turbine.UI";
    import "Turbine.Gameplay";
    -- the generic AddCallback and RemoveCallback functions that allow supporting multiple handlers for each event.
    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]);
                local i;
                for i = 1, size do
                    if (object[event][i] == callback) then
                        table.remove(object[event], i);
                        break;
                    end
                end
            end
        end
    end
    -- This is the default locale and ResStr settings representing the "EN" client
    -- We create the ResStr table to hold all of the resource strings used in the plugin. In this case they happen to only be the patterns used to match the client chat messages.
    locale = "en";
    ResStr={};
    ResStr[1]="You have joined a .+%.";
    ResStr[2]="You leave your .+%.";
    ResStr[3]="Your .+ has been disbanded%.";
    ResStr[4]="You have been dismissed from your .+%.";
    ResStr[5]="You are now the leader of the .+%.";
    ResStr[6]="(.+) is now the leader of the .+%.";
    ResStr[7]="You dismiss (.+) from the .+%.";
    ResStr[8]="(.+) has joined your .+%.";
    ResStr[9]="(.+) has left your .+%.";
    ResStr[10]="(.+) has been dismissed from your .+%.";
    -- this tests for the "DE" client by checking for the existance of the German version of the Help command
    if Turbine.Shell.IsCommand("hilfe") then
     -- if the German Help command, Hilfe, exists then override the locale and ResStr table with the "DE" values
     locale = "de";
     ResStr[1]="Ihr habt Euch einer .+ angeschlossen%.";
     ResStr[2]="Ihr verlasst .+%.";
     ResStr[3]=".+ wurde aufgel"..string.char(195)..string.char(182).."st%.";
     ResStr[4]="Ihr wurdet aus .+ ausgeschlossen%.";
     ResStr[5]="Ihr f"..string.char(195)..string.char(188).."hrt jetzt die .+ an%.";
     ResStr[6]="(.+) f"..string.char(195)..string.char(188).."hrt jetzt die Gruppe von Gef"..string.char(195)..string.char(164).."hrten an%.";
     ResStr[7]="Ihr schlie"..string.char(195)..string.char(159).."t (.+) aus .+ aus%.";
     ResStr[8]="(.+) hat sich .+ angeschlossen%.";
     ResStr[9]="(.+) hat .+ verlassen%.";
     ResStr[10]="(.+) wurde aus.+ausgeschlossen%.";
    elseif Turbine.Shell.IsCommand("aide") then
     locale = "fr";
     ResStr[1]="Vous avez rejoint .+%."
     ResStr[2]="Vous quittez votre .+%."
     ResStr[3]="Votre .+ s'est rompue."
     ResStr[4]="Vous avez "..string.char(195)..string.char(169).."t"..string.char(195)..string.char(169).." renvoy"..string.char(195)..string.char(169).." de votre .+%."
     ResStr[5]="Vous "..string.char(195)..string.char(170).."tes "..string.char(195)..string.char(160).." pr"..string.char(195)..string.char(169).."sent le chef d.+%."
     ResStr[6]="(.+) est "..string.char(195)..string.char(160).." pr"..string.char(195)..string.char(169).."sent le chef d.+%."
     ResStr[7]="Vous renvoyez (.+) d.+%."
     ResStr[8]="(.+) a rejoint votre .+%."
     ResStr[9]="(.+) a quitt"..string.char(195)..string.char(169).." votre .+%."
     ResStr[10]="(.+) ne fait plus partie de votre .+%."
    end
    -- This is the function that handles the actual firing of events
    -- When called, we pass the object raising the event, "sender", the name of the event being raised, and the "args" parameter which contains either a single argument or a table of arguments to be passed to the event handlers
    function FireEvent(sender,event,args)
     -- allows us to fire events as functions or tables of functions
     if type(event)=="function" then
      -- if the event only has a single function assigned, then we call that function passing it the sender and args arguments
      event(sender,args);
     else
      if type(event)=="table" then
       -- if the event has a table of functions assigned to it, then we call each function passing it the sender and args arguments
       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
    -- This creates the generic control that we use to represent the partywrapper - we use a control since we need to have an Update event handler
    PartyWrapper=Turbine.UI.Control();
    -- chat handle
    PartyWrapper.chat=Turbine.Chat;
    -- This is the event handler for the chat object - this handler will keep the party names synchronized by processing any messages dealing with the Party
    ChatReceived=function(sender, args)
     message=args.Message
     -- we are only interested in messages in the "Standard" channel
     if args.ChatType==Turbine.ChatType.Standard then
      -- Now compare the message to each of the possible Party messages and if a match is found, either take the appropriate action and/or fire the appropriate event
      if string.find(message,ResStr[1]) then
       FireEvent(PartyWrapper,PartyWrapper.JoinedParty,nil);
      elseif string.find(message,ResStr[2]) or string.find(message,ResStr[3]) or string.find(message,ResStr[4]) then
       PartyWrapper.leaderName="";
       while #PartyWrapper.memberName>0 do
        table.remove(PartyWrapper.memberName);
       end
       FireEvent(PartyWrapper,PartyWrapper.LeftParty,nil);
      elseif string.find(message,ResStr[5])~=nil then
       local args={};
       args.OldLeader=PartyWrapper.leaderName;
       PartyWrapper.leaderName=Turbine.Gameplay.LocalPlayer:GetInstance():GetName();
       args.NewLeader=PartyWrapper.leaderName;
       FireEvent(PartyWrapper,PartyWrapper.LeaderChanged,args);
      else
       local member=string.match(message,ResStr[6]);
       if member~=nil then
        local args={};
        args.OldLeader=PartyWrapper.leaderName;
        PartyWrapper.leaderName=member;
        args.NewLeader=PartyWrapper.leaderName;
        FireEvent(PartyWrapper,PartyWrapper.LeaderChanged,args);
       else
        member=string.match(message,ResStr[7]);
        if member==nil then
         member=string.match(message,ResStr[9]);
        end
        if member==nil then
         member=string.match(message,ResStr[10]);
        end
        if member~=nil then
         local memberIndex;
         for memberIndex=1,#PartyWrapper.memberName do
          if PartyWrapper.memberName[memberIndex]==member then
           table.remove(PartyWrapper.memberName,memberIndex);
           local args={};
           args.MemberName=member;
           FireEvent(PartyWrapper,PartyWrapper.MemberRemoved,args);
           break;
          end
         end
         if #PartyWrapper.memberName==1 then
          -- we dismissed the only other member of the party, effectively disbanding
          PartyWrapper.leaderName="";
          table.remove(PartyWrapper.memberName);
          FireEvent(PartyWrapper,PartyWrapper.LeftParty,nil);
         end
        else
         member=string.match(message,ResStr[8]);
         if member~=nil then
          local memberIndex;
          local found=false;
          for memberIndex=1,#PartyWrapper.memberName do
           if PartyWrapper.memberName[memberIndex]==member then
            found=true;
            break;
           end
          end
          if not found then
           PartyWrapper.memberName[#PartyWrapper.memberName+1]=member;
           local args={};
           args.MemberName=member;
           FireEvent(PartyWrapper,PartyWrapper.MemberAdded,args);
          end
         else
         end
        end
       end
      end
     end
    end
    -- This is the unload event handler. Since we only process this when our apartment is being unloaded we know the wrapper will be unloaded too, so all we have to clean up is our own chat event handler
    PartyWrapper.Unload=function(sender)
     RemoveCallback(PartyWrapper.chat, "Received", ChatReceived)
    end
    -- Set the name of the plugin specific child party plugin - you could theoretically have more than one plugin with distinct apartments depending on how you reuse the plugin. We called this one "MyParty"
    PartyWrapper.childPlugin="MyParty";
    PartyWrapper.loaded=false; -- this will be set to true once the Lua file is fully parsed and processed and the Plugin[] entry is created
    PartyWrapper.initialized=false; -- this will be set to true once the Child plugin has been detected, indicating that the initial party data has been generated and processed
    PartyWrapper.leaderName=""; -- this is our internal storage for the party leader's name
    PartyWrapper.memberName={}; -- this table will hold our replica of the party member's names
    PartyWrapper.Update=function()
     if not PartyWrapper.loaded then
      -- if we are not yet flagged as loaded, the first time Update get's called we load the child plugin and set the loaded flag
      Turbine.PluginManager.LoadPlugin(PartyWrapper.childPlugin);
      PartyWrapper.loaded=true;
     elseif PartyWrapper.loaded then
      for tmpIndex=1,#Turbine.PluginManager:GetLoadedPlugins() do
       -- once we start loading the child plugin, we have to wait until it finishes intializing before we can retrieve the names
       if Turbine.PluginManager:GetLoadedPlugins()[tmpIndex].Name==PartyWrapper.childPlugin then
        -- turn off updates as soon as possible so that we don't waste machine cycles and don't accidentally process the names twice
        PartyWrapper:SetWantsUpdates(false);
        -- at this point, we know that the data (if any) is available so we get the list of shell command names
        cmds=Turbine.Shell.GetCommands();
        if cmds~=nil and type(cmds)=="table" then
         local cmdIndex;
         -- now we do an alphabetic asort on the command names so that we can limit the number of commands that we compare to our data pattern
         table.sort(cmds,function(arg1,arg2)if arg1<arg2 then return(true) end end);
         -- clear out the local replica since we're loading it from scratch with data from the child plugin
         while #PartyWrapper.memberName>0 do
          table.remove(PartyWrapper.memberName);
         end
         -- now iterate through the alphabetic list of command names
         for cmdIndex=1,#cmds do
          -- if we get to the chat command for user channel 1, we know that there are no more encoded data values since they all start with a "0"
          if cmds[cmdIndex]>="1" then
           break
          else
           -- try to match the command to our encoded pattern, loading the variables if the patten matches
           local leader,index,name=string.match(cmds[cmdIndex],"0MP([LM])(%d+)_(.+)");
           index=tonumber(index);
           --if we got data, process it
           if leader~=nil then
            if leader=="L" then
             -- this command name contained the party leaders name
             PartyWrapper.leaderName=name;
            else
             -- this command name contained a party member record
             PartyWrapper.memberName[index]=name;
            end
           end
          end
         end
        end
        -- once we've processed all of the potential commands, flag the wrapper as initialized
        PartyWrapper.initialized=true;
        -- we're done with the encoded commands, so unload the child plugin and let it clean up the command names
        Turbine.PluginManager.UnloadScriptState(PartyWrapper.childPlugin);
        -- now that we're initialized, add the Chat event handler that will keep us synchronized
        AddCallback(PartyWrapper.chat,"Received",ChatReceived)
       end
      end
     end
    end
    -- The wrapper is ready to get initialized, turn on update event handling
    PartyWrapper:SetWantsUpdates(true);
    -- This method exposes the number of Party Members
    PartyWrapper.GetMemberCount=function()
     return #PartyWrapper.memberName;
    end
    -- This method exposes the Party Leader Name
    PartyWrapper.GetLeaderName=function()
     return PartyWrapper.leaderName;
    end
    -- This method exposes the name of the member at the specified index
    PartyWrapper.GetMemberName=function(sender,index)
     local name=nil;
     index=tonumber(index);
     if index~=nil then
      index=math.floor(index)
      if index>0 and index<=#PartyWrapper.memberName then
       name=PartyWrapper.memberName[index];
      end
     end
     return name;
    end
    Finally, we need some basic plugin to make use of this reusable wrapper. The following code creates a simple party member list display - it isn't fancy and doesn't hide with the Esc or F12 keys, but it will give a decent example of using the Party Wrapper.

    This file should be saved as "main.lua" in the "Plugins/YourName/PartySample" folder.
    Code:
    import "Turbine"
    import "Turbine.UI"
    import "Turbine.UI.Lotro"
    import "YourName.PartySample.PartyWrapper"
    -- the generic AddCallback and RemoveCallback functions that allow supporting multiple handlers for each event.
    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]);
                local i;
                for i = 1, size do
                    if (object[event][i] == callback) then
                        table.remove(object[event], i);
                        break;
                    end
                end
            end
        end
    end
    -- Create a window to hold the list of names
    sampleWindow=Turbine.UI.Lotro.Window();
    sampleWindow:SetBackColor(Turbine.UI.Color(0,0,0,0));
    sampleWindow:SetSize(400,400);
    sampleWindow:SetText("Party Sample")
    -- Create the actual listbox that will display the list of names
    sampleWindow.PartyList=Turbine.UI.ListBox();
    sampleWindow.PartyList:SetParent(sampleWindow);
    sampleWindow.PartyList:SetSize(sampleWindow:GetWidth()-32,sampleWindow:GetHeight()-110);
    sampleWindow.PartyList:SetPosition(10,50);
    sampleWindow.PartyList:SetBackColor(Turbine.UI.Color(.1,.1,.1));
    -- Bind a vertical scrollbar to the listbox
    sampleWindow.VScroll=Turbine.UI.Lotro.ScrollBar();
    sampleWindow.VScroll:SetOrientation(Turbine.UI.Orientation.Vertical);
    sampleWindow.VScroll:SetParent(sampleWindow);
    sampleWindow.VScroll:SetPosition(sampleWindow:GetWidth()-22,50);
    sampleWindow.VScroll:SetWidth(12);
    sampleWindow.VScroll:SetHeight(sampleWindow.PartyList:GetHeight());
    sampleWindow.PartyList:SetVerticalScrollBar(sampleWindow.VScroll);
    -- create a label to display the count of the players currently in the Fellowship/Raid
    sampleWindow.Count=Turbine.UI.Label();
    sampleWindow.Count:SetParent(sampleWindow);
    sampleWindow.Count:SetSize(200,20);
    sampleWindow.Count:SetPosition(sampleWindow:GetWidth()/2-100,sampleWindow:GetHeight()-55);
    sampleWindow.Count:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter);
    sampleWindow.Count:SetText("Count:0");
    -- Create a button that allows forcing a refresh if the display ever got out of synch (so far has never been used)
    sampleWindow.RefreshButton=Turbine.UI.Lotro.Button();
    sampleWindow.RefreshButton:SetParent(sampleWindow);
    sampleWindow.RefreshButton:SetSize(150,20);
    sampleWindow.RefreshButton:SetPosition(sampleWindow:GetWidth()/2-75,sampleWindow:GetHeight()-30);
    sampleWindow.RefreshButton:SetText("Force Refresh");
    sampleWindow.RefreshButton.MouseClick=function()
     -- force a refresh of the wrapper
     PartyWrapper.loaded=false;
     PartyWrapper.initialized=false;
     PartyWrapper:SetWantsUpdates(true);
     sampleWindow:SetWantsUpdates(true);
    end
    -- This is where we query the wrapper for the count, leader name and member names
    sampleWindow.RefreshList=function()
     -- start by clearing any old data
     sampleWindow.PartyList:ClearItems();
     local count=PartyWrapper:GetMemberCount();
     -- update the count display
     sampleWindow.Count:SetText("Count:"..tonumber(count));
     if count>0 then
      -- store the leader name so that we can set the matching member name to a different color
      local leader=PartyWrapper:GetLeaderName();
      local tmpIndex;
      for tmpIndex=1,count do
       -- iterate through the membernames, creating a label for each one and adding it to the listbox
       local tmpRow=Turbine.UI.Label();
       tmpRow:SetParent(sampleWindow.PartyList);
       tmpRow:SetSize(sampleWindow.PartyList:GetWidth(),20);
       local name=PartyWrapper:GetMemberName(tmpIndex);
       if name==leader then
        tmpRow:SetForeColor(Turbine.UI.Color(0,1,0));
       else
        tmpRow:SetForeColor(Turbine.UI.Color(0,.2,1));
       end
       tmpRow:SetText(name);
       sampleWindow.PartyList:AddItem(tmpRow);
      end
     end
    end
    sampleWindow.loaded=false;
    sampleWindow.Update=function()
     if Plugins["PartySample"]~=nil and sampleWindow.loaded==false then
      -- when we first load we want to create our unload handler
      sampleWindow.loaded=true;
      Plugins["PartySample"].Unload=function()
       -- when we unload we want to be sure to remove all of our event handlers and shell commands
       RemoveCallback(PartyWrapper, "LeaderChanged", LeaderChanged);
       RemoveCallback(PartyWrapper, "MemberAdded", MemberAdded);
       RemoveCallback(PartyWrapper, "MemberRemoved", MemberRemoved);
       RemoveCallback(PartyWrapper, "JoinedParty", JoinedParty);
       RemoveCallback(PartyWrapper, "LeftParty", LeftParty);
       PartyWrapper:Unload(); -- this will unregister the chat event handler - this assumes we are the only plugin using the wrapper... should change this to allow for other plugins
       Turbine.Shell.RemoveCommand(sampleWindow.shellCommand);
      end
     end
     if PartyWrapper.initialized then
      -- if the wrapper is flagged as initialized, the we want to refresh our list and stop handling updates until the wrapper raises an event
      sampleWindow.loaded=true;
      sampleWindow:SetWantsUpdates(false);
      sampleWindow:RefreshList();
     end
    end
    -- These are the event handlers that will be assigned to the possible events that the wrapper can raise. 
    LeaderChanged=function(sender,args)
     sampleWindow:RefreshList();
    end
    MemberAdded=function()
     sampleWindow:RefreshList();
    end
    MemberRemoved=function()
     sampleWindow:RefreshList();
    end
    JoinedParty=function()
     PartyWrapper.loaded=false;
     PartyWrapper.initialized=false;
     PartyWrapper:SetWantsUpdates(true);
     sampleWindow:SetWantsUpdates(true);
    end
    LeftParty=function()
     sampleWindow:RefreshList();
    end
    -- add the handlers to the wrappers events
    AddCallback(PartyWrapper, "LeaderChanged", LeaderChanged);
    AddCallback(PartyWrapper, "MemberAdded", MemberAdded);
    AddCallback(PartyWrapper, "MemberRemoved", MemberRemoved);
    AddCallback(PartyWrapper, "JoinedParty", JoinedParty);
    AddCallback(PartyWrapper, "LeftParty", LeftParty);
    --  create a "/PartySample toggle" shell command to allow the user to redisplay the window if they close it
    sampleWindow.shellCommand=Turbine.ShellCommand();
    sampleWindow.shellCommand.Execute = function(sender, cmd, args)
     if string.lower(args)=="toggle" then
      sampleWindow:SetVisible(not sampleWindow:IsVisible());
     end
    end
    Turbine.Shell.AddCommand("partySample",sampleWindow.shellCommand);
    -- turn on updates so that we can get initialized
    sampleWindow:SetWantsUpdates(true);
    -- display the window
    sampleWindow:SetVisible(true);
    While the "Main" plugin in this sample isn't terribly useful, it serves as a good example of how to perform cross apartment communication, how to programatically load and unload a plugin, how to fire custom events, using the Chat event handler to monitor the chat channels and even a bit of internationalization.

    The MyParty plugin and the PartyWrapper files should lend themselves quite nicely for reuse in any plugin that wants to track the party member names and leader but doesn't want to get caught up in the possible client crash issues currently surrounding the Party object and the failures to fire MemberAdded/MemberRemoved events.
    Last edited by Garan; May 29 2015 at 09:48 AM. Reason: typo

 

 
Page 1 of 3 1 2 3 LastLast

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  

This form's session has expired. You need to reload the page.

Reload