Game Development Community

To send a string longer than 255 chars

by Scott Richards · in Torque Game Engine · 12/13/2009 (2:21 am) · 5 replies

So last week I got it into my head that it would be cool to record a bunch of gameplay statistics (how many times you killed which opponent with which weapon, how many shots fired and how many hit, that sort of thing). This information had to be gathered on the server (for accuracy) and then sent to each client to display at the end of the match. Rather than implement a special purpose commandToClient system to send all of the individual values (there were a lot of them), I decided to format the information into a single large string which I would then send. Problem is, as I'm sure some of you know, commandToClient clips strings to 255 chars. Still, I figured that creating a general purpose long-string-sending system was preferable to creating a special purpose stat-sending system. So that's what I did.

Of course, I rolled my own system before I even thought to check whether anybody had already written such a thing; that's just how my mind works. Running a quick search now though, I see a number of threads on the subject containing a number of suggestions, but no actual working code. Of course, what with this site search being what it is, I may well have missed something. Still, I thought I'd post my solution anyway. It might well be useful to someone.

I suppose I could and probably should make a resource instead of a forum post. BUT, this code is not "battle tested". In fact it's barely tested at all. It works for my immediate purposes, but there may well be problems I have not uncovered in my limited testing and usage. And so I thought I'd post it here, and ask anyone who might chance to test it to report on their results. Then I figure if it seems solid I could make it a resource.

This is a script-only solution. No engine modifications required. Should be drop-in-and-go compatible with many versions of Torque.. I think. Anyway, here's the code: It's in two parts for each the client and server. ... and I think I will split this into multiple posts lest I hit the post word limit.

About the author

I am an artist, a programmer, and a rogue developer, subbornly chasing a dream, trying to make the game I want to play.


#1
12/13/2009 (2:22 am)
//--------------------------------------------------------------
// stringToClient() (Server code)
// TorqueScript function for sending big strings (over 255 chars)
// written by Scott Richards (GameDev@scottris.net)
//--------------------------------------------------------------

function stringToClient(%client, %target, %name, %value, %completeCallback)
{
   if (!isObject(%client))
      return;
   
   %string = "StringOutgoingTo" @ %client @ %target @ %name;
   // Make sure this string is not actively sending
   if (isObject(%string))
   {
      error("Error: stringToClient(): Cannot send two strings to the same target with the same name at once.");
      return;
   }
   // Create new Sender object
   new ScriptObject(%string)
   {
      name = %name;
      value = %value;
      target = %target;
      callback = %completeCallback;
   };
   %string = %string.getId();
   RootGroup.add(%string);
   
   // Begin sending
   serverCmdAckStringPart(%client, %string, -1);
   
   // Que check for failed send
   schedule(6000, %string, _sendingStringToClientFailed, %client, %string);
}

function serverCmdAckStringPart(%client, %string, %seq)
{
   if (!isObject(%string))
      return; // oops!
   
   cancel(%string.retryPart);
   
   %seq++;
   if (%seq < 0) // condition indicates final receipt confirmation or failure
   {
      // call complete callback (name, error code {0 = none, -1 = timeout, 1 = bad target})
      if (%string.callback !$= "")
         eval(%string.callback @ "(%client, %string.name, " @ -%seq - 1 @ ");");
      // cleanup
      RootGroup.remove(%string);
      %string.delete();
      return;
   }
   
   // get next part
   %part = getSubStr(%string.value, %seq * 255, 255);
   if (strlen(%part) > 0)
      // send next part
      commandToClient(%client, 'readStringPart', %string, %seq, %part);
   else
      // Send done code
      commandToClient(%client, 'readStringPart', %string, -1, "", %string.name, %string.target);
      
   // Que check for part resend
   %string.retryPart = schedule(2000, %string, _sendingStringPartToClientFailed, %client, %string, %seq);
}

function _sendingStringPartToClientFailed(%client, %string, %seq)
{
   serverCmdAckStringPart(%client, %string, %seq - 1);
}

function _sendingStringToClientFailed(%client, %string)
{
   // If that client still exists, callback a fail
   if (isObject(%client))
   {
      // call complete callback (name, error code {0 = none, -1 = timeout, 1 = bad target})
      if (%string.callback !$= "")
         eval(%string.callback @ "(%client, %string.name, -1);");
   }
   RootGroup.remove(%string);
   %string.delete();
}

//--------------------------------------------------------------
// stringToServer() (Server code)
// TorqueScript function for sending big strings (over 255 chars)
// written by Scott Richards (GameDev@scottris.net)
//--------------------------------------------------------------

function serverCmdReadStringPart(%client, %id, %seq, %part, %name, %target)
{
   %string = "StringIncomingFrom" @ %client @ %id;
   // Create buffer object
   if (!isObject(%string))
   {
      new ScriptObject(%string)
      { 
         value = "";
         partsReceived = 0;
      };
      %string = %string.getId();
      RootGroup.add(%string);
      schedule(6000, %string, _serverReceivingStringFailed, %string);
   }
   else
   {
      %string = %string.getId();
   }
   if (%seq > -1)
   {
      if (%seq < %string.partsReceived)
         return; // ignore this part, we already have it.
      
      if (%seq == %string.partsReceived)
      {
         // the sequence is correct. accept the part.
         %string.partsReceived++;
         // Append string part
         %string.value = %string.value @ %part;
      }
      // Ack part
      commandToClient(%client, 'ackStringPart', %id, %string.partsReceived - 1);
   }
   else // %seq == -1 indicates the string is finished.
   {
      // Finished! Now verify target object
      if (isObject(%target) && %target.receiveString(%client, %name, %string.value))
      {
         // send final confirmation
         commandToClient(%client, 'ackStringPart', %id, -2); // special sequence number confirms final receipt 
      }
      else
      {
         // send special error code
         commandToClient(%client, 'ackStringPart', %id, -3);
         error("Server received string from client" SPC %client SPC "with invalid target" SPC %target);
      }
      // cleanup
      RootGroup.remove(%string);
      %string.delete();
   }
}

function _serverReceivingStringFailed(%string)
{
   RootGroup.remove(%string);
   %string.delete();
}
#2
12/13/2009 (2:23 am)
//--------------------------------------------------------------
// stringToClient() (Client code)
// TorqueScript function for sending big strings (over 255 chars)
// written by Scott Richards (GameDev@scottris.net)
//--------------------------------------------------------------

function clientCmdReadStringPart(%id, %seq, %part, %name, %target)
{
   %string = "StringIncoming" @ %id;
   // Create buffer object
   if (!isObject(%string))
   {
      new ScriptObject(%string)
      { 
         value = "";
         partsReceived = 0;
      };
      %string = %string.getId();
      RootGroup.add(%string);
      schedule(6000, %string, _clientReceivingStringFailed, %string);
   }
   else
   {
      %string = %string.getId();
   }
   if (%seq > -1)
   {
      if (%seq < %string.partsReceived)
         return; // ignore this part, we already have it.
      
      if (%seq == %string.partsReceived)
      {
         // the sequence is correct. accept the part.
         %string.partsReceived++;
         // Append string part
         %string.value = %string.value @ %part;
      }
      // Ack part
      commandToServer('ackStringPart', %id, %string.partsReceived - 1);
   }
   else // %seq == -1 indicates the string is finished.
   {
      // Finished! Now verify target object
      if (isObject(%target) && %target.receiveString(%name, %string.value))
      {
         // send final confirmation
         commandToServer('ackStringPart', %id, -2); // special sequence number confirms final receipt 
      }
      else
      {
         // send special error code
         commandToServer('ackStringPart', %id, -3);
         error("Client received string with invalid target" SPC %target);
      }
      // cleanup
      RootGroup.remove(%string);
      %string.delete();
   }
}

function _clientReceivingStringFailed(%string)
{
   RootGroup.remove(%string);
   %string.delete();
}

//--------------------------------------------------------------
// stringToServer() (Client code)
// TorqueScript function for sending big strings (over 255 chars)
// written by Scott Richards (GameDev@scottris.net)
//--------------------------------------------------------------

function stringToServer(%target, %name, %value, %completeCallback)
{
   if (!isObject(ServerConnection))
      return;
   
   %string = "StringOutgoingToServer" @ %target @ %name;
   // Make sure this string is not actively sending
   if (isObject(%string))
   {
      error("Error: stringToServer(): Cannot send two strings to the same target with the same name at once.");
      return;
   }
   // Create new Sender object
   new ScriptObject(%string)
   {
      name = %name;
      value = %value;
      target = %target;
      callback = %completeCallback;
   };
   %string = %string.getId();
   RootGroup.add(%string);
   
   // Begin sending
   clientCmdAckStringPart(%string, -1);
   
   // Que check for failed send
   schedule(6000, %string, _sendingStringToServerFailed, %string);
}

function clientCmdAckStringPart(%string, %seq)
{
   if (!isObject(%string))
      return; // oops!
   
   cancel(%string.retryPart);
   
   %seq++;
   if (%seq < 0) // condition indicates final receipt confirmation or failure
   {
      // call complete callback (name, error code {0 = none, -1 = timeout, 1 = bad target})
      if (%string.callback !$= "")
         eval(%string.callback @ "(%string.name, " @ -%seq - 1 @ ");");
      // cleanup
      RootGroup.remove(%string);
      %string.delete();
      return;
   }
   
   // get next part
   %part = getSubStr(%string.value, %seq * 255, 255);
   if (strlen(%part) > 0)
      // send next part
      commandToServer('readStringPart', %string, %seq, %part);
   else
      // Send done code
      commandToServer('readStringPart', %string, -1, "", %string.name, %string.target);
      
   // Que check for part resend
   %string.retryPart = schedule(2000, %string, _sendingStringPartToServerFailed, %string, %seq);
}

function _sendingStringPartToServerFailed(%client, %string, %seq)
{
   clientCmdAckStringPart(%string, %seq - 1);
}

function _sendingStringToServerFailed(%string)
{
   // If that connection still exists, callback a fail
   if (isObject(ServerConnection))
   {
      // call complete callback (name, error code {0 = none, -1 = timeout, 1 = bad target})
      if (%string.callback !$= "")
         eval(%string.callback @ "(%string.name, -1);");
   }
   RootGroup.remove(%string);
   %string.delete();
}
#3
12/13/2009 (2:25 am)
Setup
-------------------------------------------------------------------
You will need an object to receive the string, as well as a callback function if you want to act on success/failure.

Example:
Client-side:
new guiMLTextCtrl(Foo)
{
   .whatever.
}

function Foo::receiveString(%this, %name, %value)
{
   if (%name $= "TestString")
      %this.setText(%value);
}
Server-side:
function stringToClientCallback(%client, %stringName, %errorCode)
{
   if (%errorCode == 0)
      echo("hurray! success sending" SPC %stringName);
   else if (%errorCode == 1)
      error(%client SPC "did not have the target object ready for" SPC %stringName);
   else if (%errorCode == -1)
      error(%client SPC "timed out receiving" SPC %stringName); // try agin?
   else
      error(%client SPC "reported unknown error receiving" SPC %stringName); // shoudn't happen.
}

Usage
-------------------------------------------------------------------
Example: given the setup above, from the server you'd call:
stringToClient(%client, "Foo", "TestString", %testString, "stringToClientCallback");

Notes
-------------------------------------------------------------------
-Included also is a stringToServer() function for going the other way. The setup and usage is similar, except there is a %client argument passed to the receiveString() function and none in the stringToServer() or callback functions.

-Only two clientCmd/serverCmd tags are used to avoid cluttering the string table unecessarily.

-Strings are split into 255 char parts, with the sender waiting for an acknowledgement to each part before sending the next. This is not the fastest way to send a really large string, by any means. At 8 pps you'd be sending at approximately 2 KBps per string; roughly the upload speed of a dialup modem. The upshot is: you're unlikely to choke even a slow connection; And you should be able to send strings all day across a broadband connection without disturbing a game in progress.

-The callback functions are optional.
#4
12/13/2009 (4:02 am)
@Scott Richards:

I can write a MiniApp for your script. This way everyone can easily try out your solution without to implement the script first.

Interested? Let me know.
#5
12/13/2009 (9:22 pm)
Hey I like this..i will give it a try tomorrow and tell you my results. Thank you!