I must be missing something..
by Dusty Monk · 10/21/2007 (8:47 pm) · 3 comments
So I've been putting in a fairly significant number of hours this week on the loot system, and all week I had planned on how I was going to write this incredibly long and mind-bogglingly dull post on all of its inner workings. That was the plan. That was the plan all the way right up until Sunday morning, when I ran into a snag.
By the end of the day I decided the snag made for a more interesting post. I may double-post this week and share the rundown of the loot system then. Though, truth be told nothing I'm doing there is really rocket science.
So I was literally at the very end of the process. I had one last little polish routine to write. All I needed was the routine that notified the client that he had successfully looted an item. Scratch that. It broadcast a notification to all the clients in scope that jimmy had looted a nice loaf of bread. The only real reason for this notification was just so that the clients could chat the loot message. The actual transfer of looted item from mob to player had already been done on the server, and would be replicated to the client by way of the pack/unpack update replication. So it really was just a polish routine.
Furthermore, I had just written a routine just like it. The routine that broadcast to all users that jimmy had *started* looting, the "begin_loot" notification, was already in place and working swimmingly. So it was a matter of cut and paste, change a few variables, and test it. I did so, booted up the game, ran over killed a mob, happily looted it and smiled in satisfaction of a job well done.
Except.. one thing. The name of the mob the player had looted the item from didn't show up in the chat line. Odd. I used the mob name in the begin_loot notification -- it resolved and displayed just fine. I did a cursory examination of the routine, but there were no obvious mistakes. Everything looked kopasetic.
And there it was -- that first tingling you get when you realize that.. perhaps.. you *won't* be done in the next ten minutes. That perhaps this is bigger than just a typo, and something.. some assumption you're making.. is fundamentally wrong.
Skip ahead past the next several hours as I added prolific amounts of echo statements to determine exactly what was going on. That's how I debug script. I never bothered to lean the crazy telnet debugger thingy, so when it comes to script debugging, I do it the old fashioned way. Prolific echo statements. They were added to both the routine that worked, and the routine that didn't work, and the results carefully compared.
I'm sure you've guessed it by now, but yap, I had a ghost ID resolution problem. Ugh. Okay.. so here's what was going wrong. First, I've already got a well established and working protocol for converting ID's that go from the server to the client. Lots of script routines I have do this, and I've verified multiple times with multiple clients connected that the ID's resolve correctly, so I know this works. It goes like this:
On the server, when I'm sending an ID of something instanced (like, oh say the player, or a mob), I use the connection to the interested client and convert the ID to a ghost index using "getGhostID" on the specific connection. Here's some code. This is the routine that sends the begin_loot notification to all the clients:
By the way, I don't intend to broadcast to every client in the zone for real. The comment about scope above is a reminder to myself to eventually reduce the scope of clients in some meaningful way. For now, we broadcast to every client in the zone. On the client side, when I get the ID, I convert it to an object ID using "ResolveGhostID" on the client's connection to the server. This gives me the local clientside ID of the thing the server's trying to tell me about. So it's like this:
So why was this tried and true protocol that worked perfectly for the "Begin Loot" notification (and many others) suddenly not working for my "Loot Item" notification? Well.. remember last week when I said I don't actually pass the ID up to the server of the mob I'm trying to loot? Yeah. I resolve mouse coords on the server, and the server picks out the mob the player is trying to loot. This is important. This means the mob ID originates on the server. So the Mob ID used for the begin_loot notification originated on the server, and is translated correctly going down to the client.
But in the lootItem case, I'm using the resolved client ID sent from the server by the begin_loot notification. See, it goes something like this. The client says "Hey server! I want to loot something that's here". The Server says "That's mob 1634 on the server. It's ghost ID is 3, Here's your begin_loot notification, and oh by the way you got 5 credits". The client says "Sweet. Ghost ID 3 huh? That's mob 1597 for me. I'm going to display the loot dialog now, and tell the loot dialog it's referencing the loot on mob 1597." Then the player clicks on an item in the dialog, and the dialog knows that it's linked to mob 1597, so the dialog sends a message back up to the server: "Yo! I've looked at the menu and I'd like item at index 2 in the list, off of mob 1597..." and DOH! See what I did there? I passed the resolved client ID back up to the server as the ID of the mob I wanted.
Whats worse is in the common case of the server and the client running in the same process.. this is okay!. That is, the server will say "Mob 1597, huh? Okay I got that.. and yap, it's got loot.. so I'll transfer the mob's loot to you. No problem." It wasnt until I made the server go "I'll send you a take_item notification back so you can chat it up. Mob 1597.. getGhostID.. ermm.. um.. hey WTF!"
So what I needed was a way to convert my resolved client id on the client side back into a ghost ID, to pass up to the server, so the server could convert that ghost ID into a server ID. I brought up the netconnection class, and started rummaging about for the function I needed. And.. ermm.. it doesn't exist. No seriously! I spent a long time looking for it, and to my knowledge, there is no function that, on the client side, will take a client-side ID and convert it back to the ghost ID to send to the server. There is a function on the server side, to take a ghost Id and convert it back to a server ID. It's "resolveObjectFromGhostIndex" on the net connection. So in the conversion process I showed before, in going back from the client to the server, there's a piece missing -- like so:
So at this point I have two options. I could write the routine myself. It's fairly trivial, I think. On my connection, I could just rip through the array of mLocalGhosts, find the ID that matches the ID in question, and return the index into the array. OR.. I could, instead of squirrelling away the resolved client index, squirrel away the ghost index itself. That way, I only resolve ghost index just before I need it, and when the loot dialog sends the request back to the server, it sends the same ghost index that it was given. Then the server can resolve that index. Ultimately, this latter thing is what I did. I did this because, ultimately, I think its safer (and I wanted to say ultimately again). I got to thinking, there has to be a *reason* this routine, which seems like it would be very much needed, doesn't exist in the engine. And, I seem to recall reading that the server sometimes like's to shuffle ID's around as object's come into scope and go out of scope. I looked at all the code I could find that used these function, and in all cases, it seemed like the server would send the ghost ID down to the client, the client would hold on to the ghost ID, and resolve it into a client ID just before it needed it. So in the end, that's what I did. On the client side, when the client recieves the begin_loot notification, it saves the Ghost ID of the mob being looted, and hands that to the loot dialog. When the loot dialog pops a message up to the server saying "loot this please from this mob", it sends the ghost index back up. For the sake of closure, here's the final, adjusted routine that receives the begin_loot notification on the client side:
And there ya go. Another mystery solved. If you have any insight, personal knowlege or just thoughts on client ID's, ghost ID's, and why there isn't an easy way (that I could find -- maybe I just missed it?) to convert from a clientside ID to the appropriate ghost ID, please feel free to share. For instance, what ID do I send to the server if I want to specify a particular instanced object, and I don't already have a ghost ID? Guess we'll cross that bridge when we get there. And yes, I did finally finish the loot system. I'm very happy with it. I'm already grinding faction. Woo!
Final note -- I'm looking for a place to host some fraps videos for future blogs. I don't want to use YouTube because, well, it's just *too* public. I don't want to share the videos with the entire damn world -- mostly just to this community. Spaces has a max 1GB storage, and only allows files of up to 50 MB's. If you have a favorite video hosting site that's not YouTube, I'd love to hear about it.
Word.
By the end of the day I decided the snag made for a more interesting post. I may double-post this week and share the rundown of the loot system then. Though, truth be told nothing I'm doing there is really rocket science.
So I was literally at the very end of the process. I had one last little polish routine to write. All I needed was the routine that notified the client that he had successfully looted an item. Scratch that. It broadcast a notification to all the clients in scope that jimmy had looted a nice loaf of bread. The only real reason for this notification was just so that the clients could chat the loot message. The actual transfer of looted item from mob to player had already been done on the server, and would be replicated to the client by way of the pack/unpack update replication. So it really was just a polish routine.
Furthermore, I had just written a routine just like it. The routine that broadcast to all users that jimmy had *started* looting, the "begin_loot" notification, was already in place and working swimmingly. So it was a matter of cut and paste, change a few variables, and test it. I did so, booted up the game, ran over killed a mob, happily looted it and smiled in satisfaction of a job well done.
Except.. one thing. The name of the mob the player had looted the item from didn't show up in the chat line. Odd. I used the mob name in the begin_loot notification -- it resolved and displayed just fine. I did a cursory examination of the routine, but there were no obvious mistakes. Everything looked kopasetic.
And there it was -- that first tingling you get when you realize that.. perhaps.. you *won't* be done in the next ten minutes. That perhaps this is bigger than just a typo, and something.. some assumption you're making.. is fundamentally wrong.
Skip ahead past the next several hours as I added prolific amounts of echo statements to determine exactly what was going on. That's how I debug script. I never bothered to lean the crazy telnet debugger thingy, so when it comes to script debugging, I do it the old fashioned way. Prolific echo statements. They were added to both the routine that worked, and the routine that didn't work, and the results carefully compared.
I'm sure you've guessed it by now, but yap, I had a ghost ID resolution problem. Ugh. Okay.. so here's what was going wrong. First, I've already got a well established and working protocol for converting ID's that go from the server to the client. Lots of script routines I have do this, and I've verified multiple times with multiple clients connected that the ID's resolve correctly, so I know this works. It goes like this:
On the server, when I'm sending an ID of something instanced (like, oh say the player, or a mob), I use the connection to the interested client and convert the ID to a ghost index using "getGhostID" on the specific connection. Here's some code. This is the routine that sends the begin_loot notification to all the clients:
//-----------------------------------------------------------------------------
// looter -- person doing the looting
// target - the thing that is being looted
// credit - the amount of money that was taken from the corpse immediately
function beginLootNotifyAll(%looter, %target, %credit)
{
// Note -- Same comment about scoping above.
%count = ClientGroup.getCount();
echo("beginLootNotifyAll: On Server sending to Client..");
echo("beginLootNotifyAll: original target is: " @ %target);
for (%clientIndex = 0; %clientIndex < %count; %clientIndex++)
{
%client = ClientGroup.getObject(%clientIndex);
//echo("Using client connection: (" @ %client.name @ ").");
if( !%client.isAIControlled() )
{
%LooterID = %client.getGhostID(%looter);
%TargetID = %client.getGhostID(%target);
//echo("combatNotifyAll: attacker resolve is: " @ %AttackerID);
echo("beginLootNotifyAll: GhostID for Target is: " @ %TargetID);
commandToClient( %client, 'beginLootNotify', %LooterID, %TargetID, %credit);
}
}
}By the way, I don't intend to broadcast to every client in the zone for real. The comment about scope above is a reminder to myself to eventually reduce the scope of clients in some meaningful way. For now, we broadcast to every client in the zone. On the client side, when I get the ID, I convert it to an object ID using "ResolveGhostID" on the client's connection to the server. This gives me the local clientside ID of the thing the server's trying to tell me about. So it's like this:
Server --> Client ------ ------ (getGhostID) (resolveGhostID) Server ID -> Ghost ID Ghost ID -> Client ID
So why was this tried and true protocol that worked perfectly for the "Begin Loot" notification (and many others) suddenly not working for my "Loot Item" notification? Well.. remember last week when I said I don't actually pass the ID up to the server of the mob I'm trying to loot? Yeah. I resolve mouse coords on the server, and the server picks out the mob the player is trying to loot. This is important. This means the mob ID originates on the server. So the Mob ID used for the begin_loot notification originated on the server, and is translated correctly going down to the client.
But in the lootItem case, I'm using the resolved client ID sent from the server by the begin_loot notification. See, it goes something like this. The client says "Hey server! I want to loot something that's here". The Server says "That's mob 1634 on the server. It's ghost ID is 3, Here's your begin_loot notification, and oh by the way you got 5 credits". The client says "Sweet. Ghost ID 3 huh? That's mob 1597 for me. I'm going to display the loot dialog now, and tell the loot dialog it's referencing the loot on mob 1597." Then the player clicks on an item in the dialog, and the dialog knows that it's linked to mob 1597, so the dialog sends a message back up to the server: "Yo! I've looked at the menu and I'd like item at index 2 in the list, off of mob 1597..." and DOH! See what I did there? I passed the resolved client ID back up to the server as the ID of the mob I wanted.
Whats worse is in the common case of the server and the client running in the same process.. this is okay!. That is, the server will say "Mob 1597, huh? Okay I got that.. and yap, it's got loot.. so I'll transfer the mob's loot to you. No problem." It wasnt until I made the server go "I'll send you a take_item notification back so you can chat it up. Mob 1597.. getGhostID.. ermm.. um.. hey WTF!"
So what I needed was a way to convert my resolved client id on the client side back into a ghost ID, to pass up to the server, so the server could convert that ghost ID into a server ID. I brought up the netconnection class, and started rummaging about for the function I needed. And.. ermm.. it doesn't exist. No seriously! I spent a long time looking for it, and to my knowledge, there is no function that, on the client side, will take a client-side ID and convert it back to the ghost ID to send to the server. There is a function on the server side, to take a ghost Id and convert it back to a server ID. It's "resolveObjectFromGhostIndex" on the net connection. So in the conversion process I showed before, in going back from the client to the server, there's a piece missing -- like so:
Server <-- Client ------ ------ (resolveObjectFromGhostIndex) (?????????) Server ID <- Ghost ID Ghost ID <- Client ID
So at this point I have two options. I could write the routine myself. It's fairly trivial, I think. On my connection, I could just rip through the array of mLocalGhosts, find the ID that matches the ID in question, and return the index into the array. OR.. I could, instead of squirrelling away the resolved client index, squirrel away the ghost index itself. That way, I only resolve ghost index just before I need it, and when the loot dialog sends the request back to the server, it sends the same ghost index that it was given. Then the server can resolve that index. Ultimately, this latter thing is what I did. I did this because, ultimately, I think its safer (and I wanted to say ultimately again). I got to thinking, there has to be a *reason* this routine, which seems like it would be very much needed, doesn't exist in the engine. And, I seem to recall reading that the server sometimes like's to shuffle ID's around as object's come into scope and go out of scope. I looked at all the code I could find that used these function, and in all cases, it seemed like the server would send the ghost ID down to the client, the client would hold on to the ghost ID, and resolve it into a client ID just before it needed it. So in the end, that's what I did. On the client side, when the client recieves the begin_loot notification, it saves the Ghost ID of the mob being looted, and hands that to the loot dialog. When the loot dialog pops a message up to the server saying "loot this please from this mob", it sends the ghost index back up. For the sake of closure, here's the final, adjusted routine that receives the begin_loot notification on the client side:
function clientCmdBeginLootNotify(%looter, %target, %credits)
{
%realLooter = ServerConnection.resolveGhostID(%looter);
if (%realLooter == 0)
{
error("clientCmdBeginLootNotify - Unable to resolve looter ID: " @ %looter);
return;
}
echo("clientCmdBeginLootNotify: On Client receiving from Server..");
echo("clientCmdBeginLootNotify: received GhostID for target is: " @ %target);
%realTarget = ServerConnection.resolveGhostID(%target);
if (%realTarget == 0)
{
error("clientCmdBeginLootNotify - Unable to resolve Target ID: " @ %target);
}
echo("clientCmdBeginLootNotify: resolved Mob ID for target is: " @ %realTarget);
// Chat the response
onChatMessage(%realLooter.getShapeName() @ " takes a cash card worth " @ %credits @ " credits off of the corpse of " @ %realTarget.getShapeName() @ ".");
// Open up the Loot Panel.. ermm.. Salvage I mean.
// Get Control. If the looter is the same as the controller, then play the loot animation, and open up
// the panel.
%player = ServerConnection.getControlObject();
if (!isObject(%player))
return;
if (%player == %realLooter)
{
// Pass the UNRESOLVED id, not the resolved one. The unresolved ID is the
// one we want to hold on to, and send back to the server, if need be, for specific
// requests. Only resolve this ghostID to a local ID just before you are about to use it.
SalvagePanel.open(%target);
}
}And there ya go. Another mystery solved. If you have any insight, personal knowlege or just thoughts on client ID's, ghost ID's, and why there isn't an easy way (that I could find -- maybe I just missed it?) to convert from a clientside ID to the appropriate ghost ID, please feel free to share. For instance, what ID do I send to the server if I want to specify a particular instanced object, and I don't already have a ghost ID? Guess we'll cross that bridge when we get there. And yes, I did finally finish the loot system. I'm very happy with it. I'm already grinding faction. Woo!
Final note -- I'm looking for a place to host some fraps videos for future blogs. I don't want to use YouTube because, well, it's just *too* public. I don't want to share the videos with the entire damn world -- mostly just to this community. Spaces has a max 1GB storage, and only allows files of up to 50 MB's. If you have a favorite video hosting site that's not YouTube, I'd love to hear about it.
Word.
About the author
Dusty Monk is founder and president of Windstorm Studios, an independant game studio. Formerly a sr. programmer at Ensemble Studios, Dusty has worked on AAA titles such as Age of Empires II & III, and Halo Wars.
#2
To get ghost index you need to use:
on server:
%index = %client.getGhostID(%obj);
on client:
%index = ServerConnection.getGhostID(%obj);
To resolve it:
on server:
%obj = %client.resolveGhostID(%index);
on client:
%obj = ServerConnection.resolveObjectFromGhostIndex(%index);
Btw, you can have a look at this resource, it can really help you with working on resolving ghosts :)
Use same two console functions and it will automatically use the appropriate engine functions for solving.
.GetGhostIndex(%obj) and .ResolveGhost(%index)
Really simple! :)
And, to send a command to only "visible" clients around you can use this resource.
10/22/2007 (5:57 am)
Hey Devon, nice post!To get ghost index you need to use:
on server:
%index = %client.getGhostID(%obj);
on client:
%index = ServerConnection.getGhostID(%obj);
To resolve it:
on server:
%obj = %client.resolveGhostID(%index);
on client:
%obj = ServerConnection.resolveObjectFromGhostIndex(%index);
Btw, you can have a look at this resource, it can really help you with working on resolving ghosts :)
Use same two console functions and it will automatically use the appropriate engine functions for solving.
.GetGhostIndex(%obj) and .ResolveGhost(%index)
Really simple! :)
And, to send a command to only "visible" clients around you can use this resource.
#3
@bank - Ahh.. so you're saying I can used getGhostID on either the client side or the server side, just need to make sure I'm talking to the right connection. Well I guess that makes sense. I'll check it out. And I'm bookmarking your resource on reducing the scope size. Took a brief look at it, and it pretty much jives with what I had on mind. Though I'm probably going to have all sorts of rules about who hears gets various notifications and under what circumstances. For instance you might want combat notifications to go to everyone in scope, but the chat message only to go to people in your group. Etc etc. Still this seems like a good place to start. Thanks tons!
10/22/2007 (3:32 pm)
@Phil - Thanks, I'll check it out! @bank - Ahh.. so you're saying I can used getGhostID on either the client side or the server side, just need to make sure I'm talking to the right connection. Well I guess that makes sense. I'll check it out. And I'm bookmarking your resource on reducing the scope size. Took a brief look at it, and it pretty much jives with what I had on mind. Though I'm probably going to have all sorts of rules about who hears gets various notifications and under what circumstances. For instance you might want combat notifications to go to everyone in scope, but the chat message only to go to people in your group. Etc etc. Still this seems like a good place to start. Thanks tons!
Torque 3D Owner Phil Carlisle