Game Development Community

dev|Pro Game Development Curriculum

Eliminating root main.cs

by Kevin Bluck · 08/07/2005 (1:13 am) · 12 comments

When I was figuring out the purpose of setModPaths, I noted that it was illegal to specify the root folder, or for the resource manager to access a file in the root folder. This allegedly was for "security" reasons. But, it seemed to me, isn't this sort of a moot point? Any hacker end user has access to a human-readable text file that gives them not mere hooks into the startup, but unlimited ability to control the startup process using nothing more than notepad. That, of course, is the root main.cs It can't even be made into a compiled dso.

As I thought about it, I realized the only truly indispensable functions of main.cs were as follows:

1. Set the mod paths.
2. Execute the mod main.cs scripts.
3. Kick off OnStart().

Everything else that I could think of could be delegated into the mod packages. So why, I thought, is main.cs really necessary?

Looking into game/main.cc, I quickly found out that it really isn't. So I set about eliminating the need for root main.cs altogether.

I decided to modify the command-line syntax a bit from the GG convention. I would specify arguments in the form -arg:value, rather than GG's -arg value method. I think it makes processing arguments much easier without losing expressiveness. Admittedly, you can't use a switch$ to do it anymore, but this seems minor considering the elimination of the need to see if the next arg exists and if it matches.

I also established a purely arbitrary distinction between "game" and "mod". A "game" will have most of the real content, and you'll have exactly one. "Mods" will be tweaks to a game, and you may have zero or more. I assumed the conventional "common" as the first mod, followed by the game, followed by any mods in order.

tge [game] [[-mod:name] ...]

Any other arguments would also be of the form -arg[:value]

Open main.cc

Somewhere before initGame around line 285, add a new function:

/// Parse command line args to get mod paths, set paths, run main.cs for each.
bool parseArgs(int argc, const char **argv)
{
   // Set up mod list with common as default first mod.
   Vector<const char *> modList;
   modList.push_back( "common" );
   char* gamePath = NULL;

   // If the first argument is not dashed, assume it is the desired game.
   if( (argc >= 2) && ( dStrncmp( argv[1], "-", 1 ) != 0 ) )
   {
      gamePath = new char[ dStrlen( argv[1] ) + 1 ];
      dStrcpy( gamePath, argv[1] );
   }

   // If no game argument, use the contents of the default file.
   else
   {
      FileStream fs;
      if(fs.open("defaultgame.cfg", FileStream::Read))
      {
         U32 size = fs.getStreamSize();
         gamePath = new char[size + 1];
         fs.read(size, gamePath);
         gamePath[size] = 0;
         fs.close();
      }
   }

   // Add the game path to the list. No game path is an error.
   if( gamePath )
   {
	   modList.push_back( gamePath );
   }
   else
   {
      Platform::AlertOK("Error", "No game path specified! Please specify as first command-line argument.");
      return false;
   }

   // Set up the command line arg count for the console scripts...
   Con::setIntVariable("Game::argc", argc);

   // Iterate through each argument...
   for (U32 i = 0; i < argc; i++)
   {
      // Add each command line arg for the console scripts...
      Con::setVariable(avar("Game::argv%d", i), argv[i]);

      // If this argument is a mod name, add it to the list.
      if( dStrncmp( argv[i], "-m:", 3 ) == 0 )
      {
         modList.push_back( argv[i] + 3 );
      }

      // If this is the console argument, turn on console logging.
      if( dStrcmp( argv[i], "-console" ) == 0 )
      {
         Con::executef(2, "enableWinConsole", "true" );
      }

      // If this is the log argument, turn on console logging.
      if( dStrcmp( argv[i], "-log" ) == 0 )
      {
         Con::setLogMode( 5 );
      }

      // If this is the trace argument, turn on stack tracing.
      if( dStrcmp( argv[i], "-trace" ) == 0 )
      {
         Con::executef(2, "trace", "true" );
      }
   }

   // Actually set the mod paths using the accumulated list in the vector
   ResourceManager->setModPaths( modList.size(), modList.address() );

   // Iterate through list of mod paths and execute each main.cs in turn.
   for (U32 i = 0; i < modList.size(); i++)
   {
      dsize_t scriptBufLen = dStrlen( modList[i] ) + 18;
      char * script = new char[ scriptBufLen ];
      dSprintf( script, scriptBufLen, "exec(\"%s/main.cs\");", modList[i] );
      Con::executef(2, "eval", script );
      delete[] script;
   }

   // If we got here, all is well (enough).
   return true;
}


It is necessary to change the params of initGame to pass forward the command-line args:

/// Initalize game
bool initGame(int argc, const char **argv)
{


Replace this stuff at the end of initGame:

// Execute the main.cs script, the script is not compiled,
   // and is loaded here directly because access to the "root"
   // directory through the resource system is restricted.
   FileStream str;
   const char* mainCS = "main.cs";
   if(!str.open(mainCS, FileStream::Read))
   {
      char msg[1024];
      dSprintf(msg, sizeof(msg), "Failed to open \"%s\".", mainCS);
      Platform::AlertOK("Error", msg);
      return false;
   }

   U32 size = str.getStreamSize();
   char *script = new char[size + 1];
   str.read(size, script);
   str.close();

   script[size] = 0;
   Con::executef(2, "eval", script);
   delete[] script;
   return true;
}

with this

// Parse the command-line arguments. Return success.
   if( !parseArgs( argc, argv ) )
   {
      return false;
   }

   // Execute script OnStart function.
   Con::executef(1, "OnStart");
   return true;
}

lastly, replace this stuff at the end of the main() function:

// Set up the command line args for the console scripts...
   Con::setIntVariable("Game::argc", argc);
   U32 i;
   for (i = 0; i < argc; i++)
      Con::setVariable(avar("Game::argv%d", i), argv[i]);
   if (initGame() == false)
   {

with this:

if ( !initGame( argc, argv) )
   {

Delete your root main.cs entirely. It's no longer necessary. Be sure to copy out first any functionality inside there you want to keep, except of course for the mod loading stuff that is now obsolete. The obvious place to put any leftovers is in the common/main.cs.

You can also add a simple text file called defaultgame.cfg in your root folder. It simply contains the game name, and eliminates the need to pass a game name as a command-line argument.

You might notice that I've moved a few of the "debugging" switches into c++ as well. I like them there so that I can avoid pre-truncation of output. For example, starting trace in script means you miss everything up to wherever trace(true) is finally called. Put it here and you get everything from the very first line of script.

Naturally, you may not like some of these conventions. There is nothing magical about my command-line syntax preferences. Feel free to change them as you see fit.

But now, you can release your entire game as .dso if you like, with no root main.cs laying around tempting players to fool with it.

Enjoy,

--- Kevin

#1
08/07/2005 (5:59 am)
WHAT!!!

GET RID OF THE ROOT/MAIN.CS...

BLASPHEMY!!!!

:) :) :)

seriously, though... this was a very interesting read...
thx

--Mike
#2
08/07/2005 (8:37 am)
A far, far simpler and more flexible way of doing this would simply be to hold the main.cs in a char array that is compiled as part of the executable and executed in the same way that the existing code works. That would be less code to change would lose none of the flexibility of the existing system, unlike with your approach.

For example:

Assuming you have converted the main.cs to a C char array (there are many free utilities on the internet to do this, or you could write one in about 3 minutes, or you could do it by hand) and it's declared in a header as:

static const char gMainCS[] = { ... main.cs contents ... };

The only code changes required to main.cc, aside from including the header, are:

Note: This is based on 1.4 HEAD, so it will be slightly different for 1.3. You can find the code easily enough by either using your eyes or using Kevin's instructions above. The main difference is that the main.cs handling code has been split out into a runEntryScript() function rather then having it all in the initGame() function.

Find the code that looks like this:

if (useDefaultScript)
   {
      if (!str.open(defaultScriptName, FileStream::Read))
      {
         char msg[1024];
         dSprintf(msg, sizeof(msg), "Failed to open \"%s\".", defaultScriptName);
         Platform::AlertOK("Error", msg);
         return false;
      }
   }
   
   U32 size = str.getStreamSize();
   char *script = new char[size + 1];
   str.read(size, script);
   str.close();
   script[size] = 0;

   Con::executef(2, "eval", script);
   delete[] script;

And change it to this:

// if(useDefaultScript) block removed or commented out
  
   char *script;
   if(! useDefaultScript)
   {
	   U32 size = str.getStreamSize();
	   script = new char[size + 1];
	   str.read(size, script);
	   str.close();
	   script[size] = 0;
   }
   else
   {
	   script = (char *)gMainCS;
   }

   Con::executef(2, "eval", script);
   
   if(! useDefaultScript)
      delete[] script;

Warning: I havent tested this code, but it should work, barring any stupid typos.

You can still specify an alternate main.cs on the command line for easily testing changes without a recompile, but the normal main.cs will be compiled into the executable.

Edit: Stupidly hit submit before finishing.
#3
08/08/2005 (4:28 am)
There is the Hack to remove main.cs with 3 lines of code resource which discussed the way Tom suggested.

Edit link to state it was a "resource"
#4
08/08/2005 (4:05 pm)
"3 lines of code" :-) One of those is a r-e-a-l-l-y long line.

I agree that method is "easier". But its a kludge. I figured if I was going to hack around in the startup code, I might as well make it debuggable C++.

I disagree that there is any necessity to offer a means to an alternate root main.cs There is nothing except for initial mod path loading which can not be done just as well if not better as a mod using the normal package mechanism. Using root main.cs for anything else is merely a preference, or more likely just a habit. But then anybody with a real preference for switching out root main.cs files probably won't like this resource anyway.

As it happens, the above is based on an old version of main.cc, v.1.2.2 to be exact. Since 1.3.0 the changes are now even easier.

Replace the entire runEntryScript() function at about 214 with this:

/// Does command-line parsing necessary to kick off game.
/// Determines mod list from the command line and executes
/// the "main.cs" entry script for each. Also handles some
/// debugging arguments.

bool parseArgs(int argc, const char **argv) 
{
   // Set up mod list with common as default first mod.
   Vector<const char *> modList;
   modList.push_back( "common" );
   char* gamePath = NULL;

   // If the first argument is not dashed, assume it is the desired game.
   if( (argc >= 2) && ( dStrncmp( argv[1], "-", 1 ) != 0 ) )
   {
      gamePath = new char[ dStrlen( argv[1] ) + 1 ];
      dStrcpy( gamePath, argv[1] );
   }

   // If no game argument, use the contents of the default file.
   else
   {
      FileStream fs;
      if(fs.open("defaultgame.cfg", FileStream::Read))
      {
         U32 size = fs.getStreamSize();
         gamePath = new char[size + 1];
         fs.read(size, gamePath);
         gamePath[size] = 0;
         fs.close();
      }
   }

   // Add the game path to the path list and set as a console variable as well. No game path is an error.
   if( gamePath )
   {
     modList.push_back( gamePath );
     Con::setVariable( "$Game::Path", gamePath );
   }
   else
   {
      Platform::AlertOK("Error", "No game path specified! Please specify as first command-line argument.");
      return false;
   }

   // Set up the command line arg count for the console scripts...
   Con::setIntVariable("Game::argc", argc);

   // Iterate through each argument...
   for (U32 i = 0; i < argc; i++)
   {
      // Add each command line arg for the console scripts...
      Con::setVariable(avar("Game::argv%d", i), argv[i]);

      // If this argument is a mod name, add it to the list.
      if( dStrncmp( argv[i], "-m:", 3 ) == 0 )
      {
         modList.push_back( argv[i] + 3 );
      }

      // If this is the console argument, turn on console logging.
      if( dStrcmp( argv[i], "-console" ) == 0 )
      {
         Con::executef(2, "enableWinConsole", "true" );
      }

      // If this is the log argument, turn on console logging.
      if( dStrcmp( argv[i], "-log" ) == 0 )
      {
         Con::setLogMode( 5 );
      }

      // If this is the trace argument, turn on stack tracing.
      if( dStrcmp( argv[i], "-trace" ) == 0 )
      {
         Con::executef(2, "trace", "true" );
      }

      // If this is the debugger argument, turn on the debug telnet server 
      // and wait for client to connect.
      if( ( dStrncmp( argv[i], "-debug:", 3 ) == 0 ) && ( TelDebugger ) )
      {
         TelDebugger->setDebugParameters( 6060, argv[i] + 7, true );
      }

      // If this is the save journal argument, save events to journal file.
      if( dStrncmp( argv[i], "-js:", 4 ) == 0 )
      {
         Game->saveJournal( argv[i] + 4 );
      }

      // If this is the play journal argument, play back events from journal file.
      if( dStrncmp( argv[i], "-jp:", 4 ) == 0 )
      {
         Game->playJournal( argv[i] + 4, false );
      }

      // If this is the debug journal argument, play back events from journal file
      // and break on the first event.
      if( dStrncmp( argv[i], "-jd:", 4 ) == 0 )
      {
         Game->playJournal( argv[i] + 4, true );
      }
   }

   // Actually set the mod paths using the accumulated list in the vector
   ResourceManager->setModPaths( modList.size(), modList.address() );

   // Iterate through list of mod paths and execute each main.cs in turn.
   for (U32 i = 0; i < modList.size(); i++)
   {
      dsize_t scriptBufLen = dStrlen( modList[i] ) + 18;
      char * script = new char[ scriptBufLen ];
      dSprintf( script, scriptBufLen, "exec(\"%s/main.cs\");", modList[i] );
      Con::executef(2, "eval", script );
      delete[] script;
   }

   // If we got here, all is well (enough). Clean up and return true.
   delete[] gamePath;
   return true;
}

Replace this line at about 337:

// run the entry script and return.
   return runEntryScript(argc, argv);

with this:

// Parse the command-line arguments. Return false if failure.
   if( !parseArgs( argc, argv ) )
   {
      return false;
   }

   // Execute scripted OnStart function.
   Con::executef(1, "OnStart");
   return true;

That's it.
#5
08/08/2005 (5:00 pm)
Quote:I agree that method is "easier". But its a kludge. I figured if I was going to hack around in the startup code, I might as well make it debuggable C++.

"My" way (for want of a better term) is still debuggable using the script debugger and it requires far less code modifications, which are prone to introducing unnecessary bugs. I don't really think that there's anything wrong with it being a little on the kludgey side. It is there to solve a task, and it does so with simplicity and without affecting the existing system with more then a pin prick.

Quote:I disagree that there is any necessity to offer a means to an alternate root main.cs There is nothing except for initial mod path loading which can not be done just as well if not better as a mod using the normal package mechanism. Using root main.cs for anything else is merely a preference, or more likely just a habit. But then anybody with a real preference for switching out root main.cs files probably won't like this resource anyway.

The "necessity" is not to provide an alternate main.cs, it is more to leave existing code untouched. In this case it has the nice side effect of being able to override the compiled in main.cs for quick testing of changes. Yes, with your method that is not really needed, but for "my" method it's very useful and at the best possible price: free.

Personally, I don't see the point in changing the startup code as drastically as you are mentioning, and I really don't see the point in using a non-standard convention for command line arguments. Yes, as you say, that is down to personal preference, but in the case of command line arguments you should keep one additional thing in mind: the end user of your game. They will be the people who have to live with your decision, and will not be expecting to have to use colons just as much as they will not be expecting something that looks like a text box to be a button. Perhaps that is just picking at holes, but I felt it is worth mentioning since bad/nonstandard/unexpected UI is something that really annoys me :)

As you say, at the end of the day this is all just down to preference. It has been worth hashing this out for any future reader of this resource so they are informed of the alternative(s).
#6
08/08/2005 (11:37 pm)
meh, why dont you guys just move main.cs to another dir, in C++ hardcode an exec for the new location.
#7
08/09/2005 (8:36 am)
Quote:"My" way (for want of a better term) is still debuggable using the script debugger and it requires far less code modifications, which are prone to introducing unnecessary bugs. I don't really think that there's anything wrong with it being a little on the kludgey side. It is there to solve a task, and it does so with simplicity and without affecting the existing system with more then a pin prick.

But you're making just as many changes as I am. I parse command-line args; you parse them too. I call onStart(); so do you. The major difference is that my changes are in C++, whereas yours are mostly contained in a densely formatted static char buffer expressed in an entirely different language. But nevertheless our two versions of this code accomplish more or less the same tasks entirely within main.cc, so how can your changes somehow be significantly "less"? They're not; they're just factored differently.

Jason brings up a good point; if one's only concern is to prevent users from casually editing the source, why can't root main.cs be compiled and loaded as a dso? As it happens, I have other objectives of which this is only a small part, but those who simply don't want the human-readable main.cs might consider that angle.
#8
08/09/2005 (8:48 am)
Quote:But you're making just as many changes as I am. I parse command-line args; you parse them too. I call onStart(); so do you. The major difference is that my changes are in C++, whereas yours are mostly contained in a densely formatted static char buffer expressed in an entirely different language. But nevertheless our two versions of this code accomplish more or less the same tasks entirely within main.cc, so how can your changes somehow be significantly "less"? They're not; they're just factored differently.

I think you are looking at this wrongly. My changes are using existing code, with only a couple of lines of code changed. Your changes require writing a lot of new code and is far, far more prone to error.

Quote:Jason brings up a good point; if one's only concern is to prevent users from casually editing the source, why can't root main.cs be compiled and loaded as a dso? As it happens, I have other objectives of which this is only a small part, but those who simply don't want the human-readable main.cs might consider that angle.

Because Con::executef() uses the resource manager, which would mean the main.cs would be unable to do what it needs to do. That is, after all, the entire reason why it is plain text in the first place.

T.
#9
08/09/2005 (3:29 pm)
I like this change, the more examples the better!
#10
04/20/2006 (3:02 am)
Very good resource, but I am always trying to avoid touching the C++ sources of Torque and so I found another way to avoid users from changing the main.cs...at least in some way. I just put the main.cs into a subfolder called "GameData" which also contains the mods. Now in the root folder there is a 1-line main.cs:

exec("GameData/main.cs");

So now users can of course create new content, but they cannot really change the actual game ;)
Just for the lazy ones who wish to protect their work without having to recompile TGE :D
#11
06/16/2006 (5:13 am)
This is actually a pretty important idea.
If you leave a single .cs file accessible bad things can happen.
Of course, creating a custom .cs file and deleting the existing .dso can get the same results.
Removing the .cs compile feature for a client release may be the best solution but may affect things like prefs.cs or config.cs.
I'm still investigating.
But think of someone like Gonzo T. Clown using exec("./myhack.cs"); while peeing in your wheaties and juggling your beer.
//myhack.cs
trace(1);
//.dump();
//or whatever your evil little heart desires.
From there, gleaning serverCmd functions and more is possible.
I feel that no matter how much is exploitable and how much is secure, the true factor is how good your server code is.
Food for thought for anyone releasing to the public. ;)

Ari
"Measure twice, cut once."
#12
09/03/2006 (11:28 pm)
Pref.cs and Config.cs DON't need to be compiled. You don't need to compile those at all.
I setup a simple system that read those in from a different file name (a .txt or .cfg name is fine)
as that is all those two are. They simply store information on settings the Client has changed
that you ALLOW the client to change. And I converted them to a single file as well.

So removing the following bits would reduce hacking ability even more:
Compile/Trace/Dump

These three things are for Debugging purposes, not for running the app.
The less a hacker has access to, the better, and of course the more robust your server code
the better as well.

Mythic