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.
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.
#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
-------------------------------------------------------------------
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:
Usage
-------------------------------------------------------------------
Example: given the setup above, from the server you'd call:
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.
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
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.
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!
Torque 3D Owner Scott Richards
//-------------------------------------------------------------- // 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(); }