Game Development Community

Applying ConsoleTypes to dynamic script variables

by Thomas "elfprince13" Dickerson · 06/14/2011 (2:04 pm) · 5 comments

There are circumstances where it is useful to check the values of variables which were created in script and dynamically allocated, rather than linked in the engine code to an existing C++ variable. This is particularly true for Torquescript arrays, which are little more than a way to tack values onto the end of a variable name in script.

Examples of cases such as these are present in the engine:
getMasterServerList reads as follows:
static Vector<MasterInfo> masterList;
   masterList.clear();

   for (U32 i = 0; i < 10; i++) {
      char buffer[50];
      dSprintf(buffer,sizeof(buffer),"Pref::Master%d",i);
      const char* master = Con::getVariable(buffer);
      if (master && *master) {
         NetAddress address;
         // Format for master server variable:
         //    regionMask:netAddress
         U32 region = 1; // needs to default to something > 0
         dSscanf(master,"%d:",&region);
         const char* madd = dStrchr(master,':') + 1;
         if (region && Net::stringToAddress(madd,&address)) {
            masterList.increment();
            MasterInfo& info = masterList.last();
            info.address = address;
            info.region = region;
         }
         else
            Con::errorf("Bad master server address: %s",master);
      }
   }
You'll notice that while this code accesses dynamically created variables, the way in which it does so is hardly dynamic - allowing for a maximum of 10 master server addresses to be accessed.

The following code from pushServerFavorites() is a little better, but not much:
S32 count = Con::getIntVariable( "$pref::Client::ServerFavoriteCount" );
   if ( count < 0 )
   {
      Con::setIntVariable( "$pref::Client::ServerFavoriteCount", 0 );
      return;
   }

   NetAddress addr;
   const char* server = NULL;
   char buf[256], serverName[25], addrString[256];
   U32 sz, len;
   for ( S32 i = 0; i < count; i++ )
   {
      dSprintf( buf, sizeof( buf ), "Pref::Client::ServerFavorite%d", i );
      server = Con::getVariable( buf );
      if ( server )
      {
         sz = dStrcspn( server, "t" );
         if ( sz > 0 )
         {
            len = sz > 24 ? 24 : sz;
            dStrncpy( serverName, server, len );
            serverName[len] = 0;
            dStrncpy( addrString, server + ( sz + 1 ), 255 );

            //Con::errorf( "Pushing server favorite "%s" - %s...", serverName, addrString );
            Net::stringToAddress( addrString, &addr );
            ServerInfo* si = findOrCreateServerInfo( &addr );
            AssertFatal(si, "pushServerFavorites - failed to create Server Info!" );
            si->name = (char*) dRealloc( (void*) si->name, dStrlen( serverName ) + 1 );
            dStrcpy( si->name, serverName );
            si->isFavorite = true;
            pushPingRequest( &addr );
         }
      }
   }
It allows as many favorites to be added as the script programmers want, but it requires that they update a separate variable to count them all. Thus, this method is prone to programmer error: add a favorite to the array without updating the count as well, and the engine won't know anything about it.

Additionally, though Torque provides a very useful feature for converting string information, from script source, into types and data-structures used by the engine through its ConsoleTypes interface. Unfortunately, use of this feature has traditionally required that the variables to be typed be declared within the C++ source, obstructing their use with dynamically created script variables (and in particular with script arrays). The code that follows will demonstrate how to work around this limitation, and how to work with script arrays in C++ without requiring an secondary variable to track the size of the array.

i = 0;
		do{
			tryNext = false;
			// We begin with business as usual
			dSprintf(varNameBuf, 512, "$pref::LDraw::Directory%d",i);
			varName = StringTable->insert(varNameBuf);
			// Check to see if the variable holds anything yet
			// if it doesn't, we'll consider it the end of the array,
			// but a more flexible system distinguishing between empty and undefined entries
			// could most likely be had simply be doing the gEvalState.globalVars.lookup(varName)
			// and associated NULLity test earlier.
			if(*Con::getVariable(varName) != ''){
				// Get a handle to the internal variable structure
				var = gEvalState.globalVars.lookup(varName);
				AssertFatal(var != NULL, "LDParse::checkLDrawDirectory - We found the variable already, this shouldn't be a problem");
				
				tryNext = true;

				// If we've handled this before, we'll already have done the type conversion
				if(var->type != TypeLDrawDir){
					// We'll copy the variable's string value, and free the currently held memory
					dStrcpy(varNameBuf, var->getStringValue());
					dFree(var->sval);
					var->sval = typeValueEmpty;
					
					// Now we allocate our own string
					char * dirStore = new char[dStrlen(varNameBuf)+1];
					
					// And take responsibility for it by saving it where it in a deque
					// Which will be cleaned up when the shutdown() function is called for this subsystem at closing time
					gLDrawDirectories.push_back(dirStore);
					var->dataPtr = dirStore;

					// Now we set the ConsoleType so our custom type checking will be used
					var->type = TypeLDrawDir;
					// And copy the original data back in, but this time the type checking will be applied
					var->setStringValue(varNameBuf);
				}
				// Now we can do whatever we want with Con::getVariable(varName), and it will conform to our custom ConsoleType
			}
			
			
		} while(tryNext && (++i)); // This second test is a sanity check of sorts. I can't imagine when it would happen, but lets make sure it doesn't.

About the author

Computer Science and Physics major at Saint Michael's College. Lead developer + project coordinator for FreeBuild. Administrator, Cemetech tech community. Webmaster for the Village2Village Projects and the Vermont Sustainable Heating Initiative.


#1
06/14/2011 (2:05 pm)
Note that the buggy forum software removed the backslash-zero from between the single quotes in
*Con::getVariable(varName) != ''
#2
06/17/2011 (10:13 pm)
lol i found you! XD

nice resource! ^_^
#3
09/07/2011 (8:33 pm)
I just noticed I forgot the definition of "var" in that last block of code. It is a Dictionary::Entry*.
#4
01/26/2012 (5:27 pm)
Keep in mind the following:

Polluting the global namespace with array variables is rarely a good idea. You can actually run into performance issues when there are too many variables, i.e. it can increase the time spent looking up variables, gradually making torque slower.

Sad to say i find it disappointing that there still is no standardized array system in TorqueScript, besides the variable name hack and the ArrayObject.
#5
01/26/2012 (7:33 pm)
In this case I was using it for a preference variable, and that's been done elsewhere in the code as well (the master server variables, etc). There are also all sorts of array variables in datablocks, in TSShapeConstructors, and they like. Keep in mind that none of those are in the global namespace. One of the earliest, easiest, tricks for passing an array around as a local is just to make a ScriptObject with your array as members. In any case, the methods used to access such variables from the engine side are all incredibly hackish, and unflexible. This offers a more "elegant" solution.