...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.