Writing LotRO Lua Plugins for Noobs
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.
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.
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)
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!
Re: Writing LoTRO Lua Plugins for Noobs
Wow yes very nice. I now feel ashamed by noddy plugin ;)
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 :(
Re: Writing LoTRO Lua Plugins for Noobs
This is what "sticky" was invented for, hopefully it'll be applied. +rep too.
http://img714.imageshack.us/img714/3...ickensig00.jpg
"Sometimes survival comes down to not being hit. Actually, most times." -the chicken skill, Bob and Weave
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.
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*
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.
Re: Writing LoTRO Lua Plugins for Noobs
Quote:
Originally Posted by
Hipo
I thought this was supposed to be for noobs haha.
Nice work man, many thanks.
*So over my head*
Quote:
Originally Posted by
Zelxyb
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 :p hmmm... that gives me an idea...
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.