- The Situation
- The GUI Window
- Adding the Window to the Tome
- Adding the GUI Window
- Adding the Server Side Code
- Adding the GUI Client Side Code
- Adding the GUI Client Skill Link Cached Information
- Adding the Information Window Code
- Adding the client to server calls
- Adding new SkillLinkInfo? table to Genesis
- Adding the offline update code
- Adding the online updates
- Thats a Wrap
--Xerves
The Situation
I am adding into my game something called a Skill Link. It is a kind of passive skill that can be linked together to affect spells/stats/combat/etc. I used a bit of the macro code to get the GUI part working since I wanted it to work properly offline and online. That part wasn't horribly complicated, but once I dugg a little more into it you then understand that now you have a new table that is linked to the player and to the class skill table for reference.
Anyway, I am going to go over everything needed here from start to finish. Some detailed pieces of code for some sections and some sections more of a general overview of what is needed.
The GUI Window
This part is fairly straight forward. Create the new .gui file. I used the Macro gui file as a template. Changed mainly the names (MACRO to SKILLLINK) and changed the number scheme slight. Had to replace the 3 commands inside the gui file
command = "Py::OnSLButtonClick(5,0);"; altCommand = "Py::OnSLButtonAltClick(5,0);"; rightClickCommand = "ToggleItemInfoWnd();";
This tells the game the player clicked on the 6th row (arrays start with 0) and this is used mainly to tell the game it was the 6th player. The 0 is the slot of the button (first one, again 0). Right Click then pulls open the Item Information Window.
Finally you will need this at the bottom.
//--- OBJECT WRITE END ---
PyExec("mud/client/gui/skillLinkWnd.py");
If you forgot to change this or add it your code will not work.
Adding the Window to the Tome
You can add a key command and/or a button on the tome. In order to add to the tome you can simply modify TomeGui? offline or through the GUI editor. You need to add a new GuiBitmapButtonCtrl? to the end and extend the whole window about 20 pixels or so to fit the button in. My ButtonCtrl? looked like this
new GuiBitmapButtonCtrl() {
profile = "MenuButtonProfile";
horizSizing = "right";
vertSizing = "bottom";
position = "353 29";
extent = "22 22";
minExtent = "8 2";
visible = "1";
command = "ToggleSkillLinkWnd();";
toolTip = "Skill Links";
mouseOver = "0";
hotKey = "-1";
toggleLocked = "0";
pulseRed = "0";
pulseGreen = "0";
groupNum = "-1";
buttonType = "PushButton";
bitmap = "~/data/ui/elements/tomebuttons";
number = "-1";
hasStateBitmaps = "0";
u0 = "0.21";
u1 = "0.2";
v0 = "0.82";
v1 = "0.2";
};
You will notice it is using a bitmap and then it pulls a button out of it (the u0/u1/v0/v1 is the coordinates). This is the coordinates of the 2nd button on that last row. Also, a command of ToggleSkillLinkWnd?() is called when it is pushed. That code needs to be placed in the GUI file at game.mmo/client/ui/TomeGui.gui. It looks like this
function ToggleSkillLinkWnd()
{
if (SkillLinkWnd.isAwake())
canvas.popDialog(SkillLinkWnd);
else
canvas.pushDialog(SkillLinkWnd);
Tome_CommandText.makeFirstResponder(true);
}
Adding the GUI Window
* A few things I have missed in the past here. In mud/client/gui/macro.py it has some window location code. If you don't add your window here it doesn't get saved to the client and you have to reopen it and move it :-(. You should notice a handful of windows at the top of this file. Need to add a default position in DEFAULT_WINDOW_POSITIONS, a few lines down there is an active array, you add it here and I set it to 1 (not sure if that is needed or not). A few lines down you have CheckWindowPositions? and another list to add it to. Even further down there is SaveWindowSettings? and two places (windows and awindows). After that just save since the rest is Macro Code.
* Also you need to add it to game.mmo\client\init.cs.
Adding the Server Side Code
A few references to this is made soon, so I am going to throw this in now. We need to actually create a few values on the character to store this information. So in mud/world/character.py add the following class/sql object.
class SkillLinkInfo(Persistent):
character = ForeignKey('Character')
skillname = StringCol(default="")
slot = IntCol(default = 0)
def destroySelf(self):
Persistent.destroySelf(self)
This is the new table for Skill Link Information. It points back to the character, has a skillname, and a slot (button). The destroySelf overload is really not needed I don't believe, I just left it in.
In the Character class you need to add your reference to SkillLinkInfo? along with the skill link slots available
skillLinkInfo = MultipleJoin('SkillLinkInfo')
skillLinks = IntCol(default = 2)
This will join the two tables together and allow you to loop through skillLinkInfo. It also sets the default amount of slots to 2.
Adding the GUI Client Side Code
Well the Client GUI piece is pretty much done. You should have a window and you can open it via the tome. Now you actually need some code to control it. I am going to post bits of the code and explain what is going on. Please remember this is part of the RPG logic that runs on the client and will make calls back to the server for information/commands (or get information already pushed).
# Copyright (C) 2004-2007 Prairie Games, Inc
# Please see LICENSE.TXT for details
from tgenative import *
from mud.tgepython.console import TGEExport
SKILLLINKWND = None
class SkillLinkWnd:
def __init__(self):
self.charPages = {}
self.window = TGEObject("SkillLinkWnd_Window")
self.charButtons = {}
self.slButtons = {}
for x in xrange(0,6):
self.charButtons[x] = TGEObject("SKILLLINKWND_CHAR%i"%x)
self.slButtons[x] = dict((y,TGEObject("SKILLLINKWND_SL_%i_%i"%(x,y))) for y in xrange(0,10))
This is the beginning of the file. Typically you will see a global value for the window in caps, same here and it is set to None for the time being. It is commonly called in the other server gui functions to pull data from. Then comes the declare of the class and the init (creation). Some values are set such as the window and the buttons are assigned to the TGE names in the GUI.
Now, something needs to happen with this GUI. code/mud/client/playermind.py contains a lot of calls to refresh certain parts of the GUI. If you find this code in the file
#LEADERWND.tick() # don't even call instead of just returning
BUFFWND.tick()
gui.macroWnd.SetFromCharacterInfos(self.charInfos)
I placed a new call to my GUI code
SKILLLINKWND.SetFromCharacterInfos(self.charInfos)
I need to import the SKILLLINKWND value at the very top of the file otherwise it won't like that one bit. This simply tells the client to refresh the window with the SetFromCharacterInfos? function. That function is listed below...
def SetFromCharacterInfos(self,cinfos):
numc = len(cinfos)
self.window.extent = '418 %i'%(34+34*numc)
for x in xrange(0,6):
self.charButtons[x].visible = False
for y in xrange(0,10):
self.slButtons[x][y].visible = False
self.slButtons[x][y].setBitmap("")
for cindex,cinfo in cinfos.iteritems():
picCtrl = self.charButtons[cindex]
for y in xrange(0, cinfo.SKILLLINKS):
self.slButtons[cindex][y].visible = True
for slot in cinfo.SKILLLINKINFO.iterkeys():
skillname = cinfo.SKILLLINKINFO[slot]
if cinfo.NSKILLS.has_key(skillname):
icon = cinfo.NSKILLS[skillname].ICON
if icon.startswith("SPELLICON_"):
split = icon.split("_")
index=int(split[2])
u0=(float(index%6)*40.0)/256.0
v0=(float(index/6)*40.0)/256.0
u1=(40.0/256.0)
v1=(40.0/256.0)
self.slButtons[cindex][slot].setBitmapUV("~/data/ui/icons/spells0%s"%split[1],u0,v0,u1,v1)
else:
self.slButtons[cindex][slot].setBitmap("~/data/ui/icons/%s"%icon)
else:
print "SkillLinkWnd Error: %s has an invalid skill of %s in a skill link slot"%(cinfo.NAME, skillname)
picCtrl.visible = True
if cinfo.DEAD:
picCtrl.setBitmap("~/data/ui/icons/dead")
else:
picCtrl.setBitmap("~/data/ui/charportraits/%s"%cinfo.PORTRAITPIC)
A lot is happening here. First off it is setting the size of the window based on the size of the parth and then it is setting all of the buttons invisible so they don't show up (the player can have 2-10 buttons visible at any time depending on how many skill link slots are open for that player. It then will start to loop through all the players in the party and set the picture of the player up and then start to make the buttons visible. Then it is looping through some client information about skills and skill links the player has setup (the skill links part will be shown later). Once it finds everything it maps the button's icon based on the skill's icon.
Next...the button click code
def OnSLButtonClick(args):
from partyWnd import PARTYWND
from mud.client.gui.macro import CURSORMACROTYPE,CURSORMACROINFO
cindex = int(args[1])
mindex = int(args[2])
if CURSORMACROTYPE == 'SKILL':
PARTYWND.mind.perspective.callRemote("PlayerAvatar","assignSkillLink",cindex, CURSORMACROINFO, mindex)
TGEObject("SKILLLINKWND_SL_%i_%i"%(cindex,mindex)).SetValue(0)
cursor = TGEObject("DefaultCursor")
cursor.bitmapName = ""
cursor.u0=cursor.v0 = 0
cursor.u1=cursor.v1 = 1
cursor.sizeX =-1
cursor.sizeY=-1
cursor.cursorControl = ""
return
This imports some cursor information from the macro code (to make sure the cursor has a skill and its information). The args are parsed for a character and slot value and then the cursor is checked to make sure it has a skill. If it does then a call is made to the server to assign it.
The next part of the code sets the button value backed to unpressed and clears the cursor back to normal.
Finally we have the Alt Click to remove it.
def OnSLButtonAltClick(args):
from partyWnd import PARTYWND
cindex = int(args[1])
mindex = int(args[2])
PARTYWND.mind.perspective.callRemote("PlayerAvatar","removeSkillLink",cindex, mindex)
return
Same bit of information here. A call is made to the server to remove it.
Ok the last final bit here
def PyExec():
global SKILLLINKWND
SKILLLINKWND = SkillLinkWnd()
TGEExport(OnSLButtonClick,"Py","OnSLButtonClick","desc",3,3)
TGEExport(OnSLButtonAltClick,"Py","OnSLButtonAltClick","desc",3,3)
This exports to TGE the commands so they work properly and sets the global SKILLLINKWND to the class. Leaving this out will make the GUI not function.
Finally, you might want to add the code to the init code here:
code/mud/client/gui/init.py
Adding the GUI Client Skill Link Cached Information
This part adds the information for Skill Links to the Client (cinfo.SKILLLINKINFO). This allows the client to know what skill links are assigned and what the name of the skill is. In /mud/world/shared/playdata.py in the class CharacterInfo? you need to add two chunks of code. In getStateToCacheAndObserveFor you add the following
state['SKILLLINKS'] = character.skillLinks
skilllinkinfo = {}
for s in character.skillLinkInfo:
skilllinkinfo[s.slot] = s.skillname
state['SKILLLINKINFO'] = skilllinkinfo
This is the number of skill links the character has and the 2nd part is the actual skill links attached to the player curently. The slot is the button and it contains the skillname.
A bit further down there is the Refresh and this needs to be added
skilllinkinfo = {}
for s in character.skillLinkInfo:
skilllinkinfo[s.slot] = s.skillname
if skilllinkinfo != state['SKILLLINKINFO']:
changed['SKILLLINKINFO'] = state['SKILLLINKINFO'] = skilllinkinfo
if state['SKILLLINKS']!=character.skillLinks:
state['SKILLLINKS']=changed['SKILLLINKS']=character.skillLinks
When a change is made triggering a refresh all of the cached values are compared to see if a chance is made. If you don't do this the GUI will never have refreshed data. Mainly what happens here is the changed array is sent to the client and then merged into the cached data so only the changed data is sent.
Adding the Information Window Code
This has some of the code I have released for viewing skill information in the Information Window. In order to make it work you need to find the following in itemInfoWnd.py
if ghost:
if isSpell:
self.setSpell(ghost)
Before it add
if not found:
skilllinkwnd = TGEObject("SkillLinkWnd")
if int(skilllinkwnd.isAwake()):
from skillLinkWnd import SKILLLINKWND
for cindex,buttons in SKILLLINKWND.slButtons.iteritems():
if cindex >= len(PARTYWND.charInfos):
continue
for slot, button in buttons.iteritems():
ncinfo = PARTYWND.charInfos[cindex]
if int(button.mouseOver) and ncinfo.SKILLLINKINFO.has_key(slot):
found = True
ghost = ncinfo.NSKILLS[ncinfo.SKILLLINKINFO[slot]]
isSkill = True
break
This checks to see if the window is open and then it loops throught he buttons to see if the player has the mouse over the button (also checks to make sure the button has valid information in it). If so it then sets the window up with the information for that skill with the SKILLLINKINFO (which has the name of the skill).
Adding the client to server calls
PARTYWND.mind.perspective.callRemote("PlayerAvatar","removeSkillLink",cindex, mindex)
This is a client to server call. The client is sending a command to remove a skill link. It is sending it to PlayerAvatar? (the player container that contains all of the characters). So we need to add this call. In mud/world/playeravatar.py find this function
def perspective_repairParty(self,cindex):
Above it add these two
def perspective_assignSkillLink(self,cindex,skill,slot):
if cindex > len(self.player.party.members) - 1:
return
char = self.player.party.members[cindex]
char.assignSkillLink(skill,slot)
def perspective_removeSkillLink(self,cindex,slot):
if cindex > len(self.player.party.members) - 1:
return
char = self.player.party.members[cindex]
char.removeSkillLink(slot)
These are the perspective calls and contain the player avatar and the player/party information. You will notice a sanity check to make sure a valid character is called and if it is then the actual character is pulled from the party and then a call is made on the character. To add those calls you need to add it to /mud/world/character.py to the Character class.
def assignSkillLink(self, skillname, slot):
for skill in self.skills:
if skill.skillname == skillname:
sfnd = 0
for s in self.skillLinkInfo:
if (slot == s.slot):
sfnd = 1
s.skillname = skillname
self.mob.updateClassStats()
if (sfnd == 0):
SkillLinkInfo(character = self, skillname = skillname, slot = slot)
self.mob.updateClassStats()
self.player.sendGameText(RPG_MSG_GAME_GAINED,"%s added <a:Skill%s>%s</a> as a Skill Link!\\n"%(self.name,GetTWikiName(skillname),skillname))
return
self.player.sendGameText(RPG_MSG_GAME_DENIED,"%s does not possess <a:Skill%s>%s</a> or it does not exist in the game!\\n"%(self.name,GetTWikiName(skillname),skillname))
return
def removeSkillLink(self, slot):
for s in self.skillLinkInfo:
if (slot == s.slot):
skillname = s.skillname
self.player.sendGameText(RPG_MSG_GAME_GAINED,"%s removed <a:Skill%s>%s</a> as a Skill Link!\\n" (self.name,GetTWikiName(skillname),skillname))
s.destroySelf();
self.mob.updateClassStats()
return;
Thie first function assigns the skill link to the button. You will notice if one isn't found a SkillLinkInfo? is called creating one (this is a new row created in the DB and also a mapping to the player). You will also notice updateClassStats is called, this will update stats and kick off a server to client refresh of player data. Lastly a message is sent to the player.
The 2nd function removes the Skill Link by destroying it since it isn't needed.
Adding new SkillLinkInfo? table to Genesis
Every time you compile a baseline database is created and it is used later on to compare the current databases to make syncs. If you forget to do this things simply won't work. You will find genesis.py in the root of your /tmmokit directory. It might not be in your IDE. You need to make to changes.
First in the table imports add the new class we just created to the end of the mud.world.character import.
from mud.world.character import Character,CharacterSpell,StartingGear,CharacterSkill,CharacterAdvancement,CharacterDialogChoice,CharacterVaultItem,CharacterFaction,SkillLinkInfo
This imports the SkillLinkInfo? class.
Next we add in the table to the end of table array in CreateTables?. This will drop the table and create it for the baseline. Once this is done your table will start working. A bit down you will see some more code for the DBDict part of the code if you were going that route (we aren't here).
Adding the offline update code
This next part takes care of the offline (single player) piece of the database. There is a separate piece for online play and this part will only fix the offline part. What is going on here is when the game is compiled a database update is forced on the first time the player logs in. It will call WorldUpdate and it will start to compare the working database with the new baseline database just created and look for "differences". If you don't update this depending on how it is linked (by name or id) it could cause a variety of errors and typically will bug out the client. So it is important this is done.
Now on to /mud/world/worldupdate.py.
Need to add SkillLinkInfo? to the character import near the top of the file. Also need to import ClassSkill? now.
from mud.world.skill import ClassSkill
I changed the CharacterSkillCopier? over to this to fix skills that get deleted.
class CharacterSkillCopier:
def __init__(self,cur,id):
self.dbAttr = {}
self.skillname = ""
t = mixedToUnder("CharacterSkill")
cur.execute("SELECT * from %s WHERE id = %i;"%(t,id))
for name,value in zip(WSCHEMA["CharacterSkill"],cur.fetchall()[0]):
if name == "skillname":
skillname = value
self.dbAttr[str(name)]=value
self.skillname = skillname
def install(self,char):
try:
sp = list(ClassSkill.selectBy(skillname="%s"%self.skillname))
if (len(sp) < 1):
print "CharacterSkill: %s no longer exists"%self.skillname
return
except:
print "CharacterSkill: %s no longer exists"%self.skillname
return
FilterColumns(CharacterSkill,self.dbAttr)
self.dbAttr["characterID"]=char.id
CharacterSkill(**self.dbAttr)
This will go through all of the character skills and compare them to the class skills. If the skill is not found it isn't added back into the game for the character.
Now with that fix, we add a very similiar function right below it for the Skill Links.
class SkillLinkInfoCopier:
def __init__(self,cur,id):
self.dbAttr = {}
t = mixedToUnder("SkillLinkInfo")
cur.execute("SELECT * from %s WHERE id = %i;"%(t,id))
for name,value in zip(WSCHEMA["SkillLinkInfo"],cur.fetchall()[0]):
if name == "skillname":
skillname = value
self.dbAttr[str(name)]=value
self.skillname = skillname
def install(self,char):
try:
sp = list(ClassSkill.selectBy(skillname="%s"%self.skillname))
if (len(sp) < 1):
print "SkillLinkInfo: %s no longer exists"%self.skillname
return
except:
print "SkillLinkInfo: %s no longer exists"%self.skillname
return
FilterColumns(SkillLinkInfo,self.dbAttr)
self.dbAttr["characterID"]=char.id
SkillLinkInfo(**self.dbAttr)
This pretty much does the same thing to make sure or Skill Link is still a valid skill. Since this is stored on the server (macros are stored on the client) we need to do this.
I bit further down the copier code is ran...find this
for r in cur.fetchall():
f = CharacterSkillCopier(cur,r[0])
skills.append(f)
Need to add the new skill link copier
#SkillLinks
skilllinks = self.skillLinkInfo = []
cur.execute("select id from skill_link_info where character_id = %i;"%id)
for r in cur.fetchall():
f = SkillLinkInfoCopier(cur,r[0])
skilllinks.append(f)
A bit further down finally the good skill links are installed. Find this
for s in self.skills:
s.install(char)
Add the following afterwards
#skilllinks
for s in self.skillLinkInfo:
s.install(char)
Adding the online updates
Ok this took me forever since I didn't really understand it very well. I know more than I want to know about it now :-(. What is going on here is that with the offline database the characters are simply stored in the world database so unless you delete a character it never leaves that database. However, online you have to deal with the buffer system setup. Instead of leaving the characters in the database it dumps the information into a compressed buffer and then does a lot of converting when it is pushed/pulled from the buffer to the active database. I would imagine this is to conserve bandwidth, database processing, and size. It is however very confusing at first and took me awhile to figure out what the hell and where it was going on. No worry though, you have this document now :-).
First we need to add some deletion code in mud/world/character.py. You really need this for offline too, I just chose this point for the example...
Find the following
for o in self.vaultItems:
o.destroySelf()
After it add
for o in self.skillLinkInfo:
o.destroySelf()
This will delete the skillLinkInfo from the database when the character is deleted. This is important because when the character is swapped out of the active online world database to the buffer table, it is deleted :-). If you forget this you will get duplicates since the active database will just keep collecting these and it will be assigning it to players that it should not.
Once this is done, time to move on to the actual updating bits.
In mud/characterserver/convertdb.py add our new table to CHARACTER_TABLES since there is some referenced character information. With my SkillLinkInfo? it looks like skill_link_info.
A bit further down you will find the following
cursor.execute("DELETE from character where id = %i;"%cid)
cursor.execute("DELETE from character_spell where character_id = %i;"%cid)
cursor.execute("DELETE from character_skill where character_id = %i;"%cid)
After it add
cursor.execute("DELETE from skill_link_info where character_id = %i;"%cid)
Now on to /mud/worldserver/charutil.py. Need to add skill_link_info to CHARACTER_TABLES. A bit further down there are 3 references to an array called ctables. skill_link_info needs to be added to all 3.
Ok now on to the big moving force.../mud/characterserver/upgradedb.py. This is the file you run to keep the tiem synced up properly with Genesis. This also is the script that does most of the checking to make sure the player buffers are current and don't have items/skills/spells/etc that no longer exist. A handful of changes are needed here and some were missing for skills so I added them in and they are in the example here.
Find thing you need to do is add SkillLinkInfo? to the character import statement at the top. Next there is CHARACTER_TABLES. Need to add skill_link_info here. Next is TLOOKUP with the matchup of the table to the class name. Add "skill_link_info":SkillLinkInfo? there.
Next find the following
TRANS_SPELLPROTO = {}
TRANS_FACTION = {}
TRANS_ADVANCEMENTPROTO = {}
Below it add
TRANS_CLASSSKILL = {}
Next a few lines down find this
TTRANS['spell_proto']=TRANS_SPELLPROTO TTRANS['faction']=TRANS_FACTION TTRANS['advancement_proto']=TRANS_ADVANCEMENTPROTO
After it add this
TTRANS['class_skill']=TRANS_CLASSSKILL
This will be used to update the skills. Now to setup the buffer Translation. Next in DoTranslation? add this global declare before nindexes = NINDEXES['%s'%table]
global NEWCONN
A bit further down find the following
except KeyError:
#traceback.print_exc()
ncursor.execute("DELETE FROM %s WHERE id = %i"%(table,TTRANS[table][values[0]]))
if nindexes.has_key("name"):
print "Removing named row %s from %s"%(values[1],table),values
else:
print "Removing anonmyous row from %s"%table,values
After it add this code to check for skills and skill links that no longer exist
if (table == 'character_skill' or table == 'skill_link_info'):
ncursor.execute("SELECT skillname,id from %s;"%table)
for name,id in ncursor.fetchall():
nncursor = NEWCONN.cursor()
try:
nncursor.execute('SELECT id from class_skill WHERE skillname = "%s";'%name)
nid = nncursor.fetchone()[0]
except:
print "Skill %s no longer exists. Removing from table %s"%(name, table)
ncursor.execute('DELETE FROM %s WHERE skillname = "%s";'%(table,name))
This is being run on the decompressed character and will delete the state information. Before this you will find some much more nifty code that takes care of characters and spells. This is a bit more bruteish, but it works.
Quite a bit further down find this
cursor.execute("SELECT name,id from spell_proto;")
for name,oid in cursor.fetchall():
try:
ncursor.execute('SELECT id from spell_proto WHERE name = "%s";'%name)
nid = ncursor.fetchone()[0]
TRANS_SPELLPROTO[oid]=nid
except:
print "SpellProto %s no longer exists"%name
After it add a bit for skills
cursor.execute("SELECT skillname,id from class_skill;")
for name,oid in cursor.fetchall():
try:
ncursor.execute('SELECT id from class_skill WHERE skillname = "%s";'%name)
nid = ncursor.fetchone()[0]
TRANS_CLASSSKILL[oid]=nid
except:
print "ClassSkill %s no longer exists"%name
This will delete any stale skills.
Thats a Wrap
Well that is everything. Hope that helps someone.

