CHAPTER 7
THE CORE FOLDER

The core folder contains files that are commonly used by every Torque 3D project.

Image art contains the textures, shapes, audio, and GUIs that are common to all Torque projects. This is also the name GarageGames normally uses for what I have been calling the assets folder in the previous chapters—except that the assets folder we have been using is not located in the core folder.

Image fonts is where cached font files (.uft) are generated. If you are using custom fonts, you need to make sure that their cached versions are not deleted from this folder when you ship your game. This way, your customers do not need to worry about installing your custom fonts on their computers.

Image profile contains scripts responsible for setting global capability strings based on the graphics card vendor and model.

Image scripts contains the TorqueScript (.cs) files that are responsible for common functionality, such as on-screen cursors, system controls, mission loading, and so on. This is also the name GarageGames uses for what I have been calling the control folder in previous chapters.

Image unifiedShell is a special case folder that allows you to create a unified interface by piecing together your menu system. If you are making a game that will be shipped for multiple platforms, like Macintosh, PC, and X-Box, you should use the interface mechanisms found in this folder.

Now, for the last several chapters I have been keeping the contents of the core folder tree out of the limelight. I hope you haven’t started thinking that it is some deep, dark, keep-it-in-the-family-only secret, because it isn’t. The reason for maintaining the obscurity is because we’ve been looking at the areas of scripting that you will most likely want to change to suit your game development needs, and that means stuff not in the core folder.

Having said that, there may be areas in the core scripts that you will want to customize or adjust in one way or another. To that end, we are going to spend this chapter patrolling the core code to get the lay of the land.

You can gain access to this code for yourself in the core folder tree of any of the Emaga versions you installed in the previous chapters.

GAME INITIALIZATION

As you may recall from earlier chapters, the core code base is treated as if it were just another add-on or Mod. It is implemented as a package in the core/main.cs module. For your game, you will need to use this package or make your own like it. This is in order to gain access to many of the more mundane features of Torque, especially the “administrivia”-like functions that help make your game a finished product but that are not especially exciting in terms of game play features.

Here are the contents of the core/main.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
// Constants for referencing video resolution preferences
$WORD::RES_X = 0;
$WORD::RES_Y = 1;
$WORD::FULLSCREEN = 2;
$WORD::BITDEPTH = 3;
$WORD::REFRESH = 4;
$WORD::AA = 5;
//-----------------------------------------------------------------------------
// CorePackage
// Adds functionality for this mod to some standard functions.
//-----------------------------------------------------------------------------
package CorePackage
{
//-----------------------------------------------------------------------------
// onStart
// Called when the engine is starting up. Initializes this mod.
//-----------------------------------------------------------------------------
function onStart()
{
   Parent::onStart();
   $isFirstPersonVar = 1;

   // Uncomment to enable AdvancedLighting on the Mac (T3D 2009 Beta 3)
   //$pref::machax::enableAdvancedLighting = true;

   // Uncomment to disable ShaderGen, useful when debugging
   //$ShaderGen::GenNewShaders = false;

   // Uncomment to dump disassembly for any shader that is compiled to disk.
   // These will appear as shadername_dis.txt in the same path as the
   // hlsl or glsl shader.
   //$gfx::disassembleAllShaders = true;

   // Uncomment useNVPerfHud to allow you to start up correctly
   // when you drop your executable onto NVPerfHud
   //$Video::useNVPerfHud = true;

   // Uncomment these to allow you to force your app into using
   // a specific pixel shader version (0 is for fixed function)
   //$pref::Video::forcePixVersion = true;
   //$pref::Video::forcedPixVersion = 0;

   if ($platform $= "macos")
     $pref::Video::displayDevice = "OpenGL";
   else
    $pref::Video::displayDevice = "D3D9";
   // Initialise stuff.
   exec("./scripts/client/core.cs");
   initializeCore();
   exec("./unifiedShell/main.cs");
   exec("./scripts/client/client.cs");
   exec("./scripts/server/server.cs");
   exec("./scripts/gui/guiTreeViewCtrl.cs");
   exec("./scripts/gui/messageBoxes/messageBox.ed.cs");
 // Level Chooser GUI
   exec("./art/gui/chooseLevelDlg.gui");
   exec("./scripts/gui/chooseLevelDlg.cs");
   exec("./scripts/gui/optionsDlg.cs");
   exec("./art/gui/optionsDlg.gui");
   exec("./scripts/gui/loadingGui.cs");
   exec("./art/gui/loadingGui.gui");
   echo(" % - Initialized Core");
}
//-----------------------------------------------------------------------------
// onExit
// Called when the engine is shutting down. Shutdowns this mod.
//-----------------------------------------------------------------------------
function onExit()
{
   // Shutdown stuff.
   shutdownCore();
   Parent::onExit();
}

function loadKeybindings()
{
   $keybindCount = 0;
   // Load up the active projects keybinds.
   if(isFunction("setupKeybinds"))
      setupKeybinds();
}

//-----------------------------------------------------------------------------
// displayHelp
// Prints the command line options available for this mod.
//-----------------------------------------------------------------------------
function displayHelp() {
     // Let the parent do its stuff.
     Parent::displayHelp();

     error("Core Mod options:
" @
         " -fullscreen           Starts game in full screen mode
" @
         " -windowed             Starts game in windowed mode
" @
         " -autoVideo            Auto detect video, but prefers OpenGL
" @
         " -openGL               Force OpenGL acceleration
" @
         " -directX              Force DirectX acceleration
" @
         " -voodoo2              Force Voodoo2 acceleration
" @
         " -prefs <configFile>   Exec the config file
");
}

//-----------------------------------------------------------------------------
// parseArgs
// Parses the command line arguments and processes those valid for this mod.
//-----------------------------------------------------------------------------
function parseArgs()
{
     // Let the parent grab the arguments it wants first.
Parent::parseArgs();

// Loop through the arguments.
for (%i = 1; %i < $Game::argc; %i++)
{
    %arg = $Game::argv[%i];
    %nextArg = $Game::argv[%i+1];
    %hasNextArg = $Game::argc - %i > 1;

    switch$ (%arg)
    {
      case "-fullscreen":
        setFullScreen(true);
        $argUsed[%i]++;

      case "-windowed":
        setFullScreen(false);
        $argUsed[%i]++;

      case "-openGL":
        $pref::Video::displayDevice = "OpenGL";
        $argUsed[%i]++;

      case "-directX":
        $pref::Video::displayDevice = "D3D";
        $argUsed[%i]++;

      case "-voodoo2":
        $pref::Video::displayDevice = "Voodoo2";
        $argUsed[%i]++;

      case "-autoVideo":
        $pref::Video::displayDevice = "";
        $argUsed[%i]++;

      case "-prefs":
        $argUsed[%i]++;
        if (%hasNextArg) {
           exec(%nextArg, true, true);
           $argUsed[%i+1]++;
           %i++;

        }
        else
       error("Error: Missing Command Line argument. Usage: -prefs <path/script.cs>");
      }
  }
}

};
activatePackage(CorePackage);

As you can see, core/main.cs provides a number of functions that provide utility functionality for work that most games commonly perform. By defining these functions inside a package, in this case, corePackage, we have default access to these functions and yet can override them if we want to, with our own versions of those functions inside packages of our own.

Notice that early on in onStart there are a number of statements that are commented out, with commented descriptions accompanying them that give direction about when and how to use these statements. For example, if you want to get a dump to a text file of the code contained in the built-in shaders that ship with T3D, you can uncomment the line:

//$gfx::disassembleAllShaders = true;

Then run your game. Each shader will then be dumped into a text file of its own, for you to examine. The shader’s text file of code will be found in the same folder where the compiled shader resides, somewhere inside the game/shaders folder.

A key statement in the onStart function is the call to initializeCore(). That function is defined in the file core/scripts/client/core.cs, presented here for your perusal:

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// initializeCore
// Initializes core game functionality.
//-----------------------------------------------------------------------------
function initializeCore()
{
   // Not Reentrant
   if( $coreInitialized == true )
   return;

   // Core keybindings.
  GlobalActionMap.bind(keyboard, tilde, toggleConsole);
  GlobalActionMap.bind(keyboard, "ctrl p", doScreenShot);

GlobalActionMap.bindcmd(keyboard, "alt enter",
"Canvas.attemptFullscreenToggle();","");

  GlobalActionMap.bindcmd(keyboard, "alt k", "cls();", "");

  // Very basic functions used by everyone.
  exec("./audio.cs");
  exec("./canvas.cs");
  exec("./cursor.cs");
  exec("./persistenceManagerTest.cs");

  // Content.
  exec("~/art/gui/profiles.cs");
  exec("~/scripts/gui/cursors.cs");

  exec( "./audioEnvironments.cs" );
  exec( "./audioDescriptions.cs" );
  exec( "./audioStates.cs" );
  exec( "./audioAmbiences.cs" );

  // Seed the random number generator.
  setRandomSeed();

  // Set up networking.
  setNetPort(0);

  // Initialize the canvas.
  initializeCanvas();

  // Start processing file change events.
  startFileChangeNotifications();

  // Core Guis.
  exec("~/art/gui/remapDlg.gui");
  exec("~/art/gui/console.gui");
  exec("~/art/gui/consoleVarDlg.gui");
  exec("~/art/gui/netGraphGui.gui");
  // Gui Helper Scripts.
  exec("~/scripts/gui/help.cs");

  // Random Scripts.
  exec("~/scripts/client/screenshot.cs");
  exec("~/scripts/client/scriptDoc.cs");
  //exec("~/scripts/client/keybindings.cs");
  exec("~/scripts/client/helperfuncs.cs");
  exec("~/scripts/client/commands.cs");

  // Client scripts
  exec("~/scripts/client/devHelpers.cs");
  exec("~/scripts/client/metrics.cs");
  exec("~/scripts/client/recordings.cs");
  exec("~/scripts/client/centerPrint.cs");

  // Materials and Shaders for rendering various object types
  loadCoreMaterials();


  exec("~/scripts/client/commonMaterialData.cs");
  exec("~/scripts/client/shaders.cs");
  exec("~/scripts/client/materials.cs");
  exec("~/scripts/client/terrainBlock.cs");
  exec("~/scripts/client/water.cs");
  exec("~/scripts/client/imposter.cs");
  exec("~/scripts/client/scatterSky.cs");
  exec("~/scripts/client/clouds.cs");

  // Initialize all core post effects.
  exec("~/scripts/client/postFx.cs");
  initPostEffects();

  // Initialize the post effect manager.
  exec("~/scripts/client/postFx/postFXManager.gui");
  exec("~/scripts/client/postFx/postFXManager.gui.cs");
  exec("~/scripts/client/postFx/postFXManager.gui.settings.cs");
  exec("~/scripts/client/postFx/postFXManager.persistance.cs");
  PostFXManager.settingsApplyDefaultPreset(); // Get the default preset settings

  // Set a default cursor.
  Canvas.setCursor(DefaultCursor);

  loadKeybindings();

  $coreInitialized = true;
}

//-----------------------------------------------------------------------------
// shutdownCore
// Shuts down core game functionality.
//-----------------------------------------------------------------------------
function shutdownCore()
{
    // Stop file change events.
    stopFileChangeNotifications();

    sfxShutdown();
}

//-----------------------------------------------------------------------------
// dumpKeybindings
// Saves of all keybindings.
//-----------------------------------------------------------------------------
function dumpKeybindings()
{
   // Loop through all the binds.
   for (%i = 0; %i < $keybindCount; %i++)
    {
       // If we haven't dealt with this map yet...
       if (isObject($keybindMap[%i]))
       {
          // Save and delete.
          $keybindMap[%i].save(getPrefsPath("bind.cs"), %i == 0 ? false : true);
          $keybindMap[%i].delete();
       }
    }
}

function handleEscape()
{

  if (isObject(EditorGui))
  {
    if (Canvas.getContent() == EditorGui.getId())
    {
       EditorGui.handleEscape();
       return;
    }
    else if ( EditorIsDirty() )
    {
       MessageBoxYesNoCancel( "Level Modified",
                        "Level has been modified in the Editor. Save?",
                        "EditorDoExitMission(1);",
                        "EditorDoExitMission();",
                        "");
     return;
     }
  }

  if (isObject(GuiEditor))
  {
    if (GuiEditor.isAwake())
    {
      GuiEditCanvas.quit();
      return;
    }
  }

  if (PlayerInterface.isAwake())
    escapeFromGame();
}

//-----------------------------------------------------------------------------
// loadMaterials - load all materials.cs files
//-----------------------------------------------------------------------------
function loadCoreMaterials()
{
  // Load any materials files for which we only have DSOs.

  for( %file = findFirstFile( "core/materials.cs.dso" );
      %file !$= "";
      %file = findNextFile( "core/materials.cs.dso" ))
  {
      // Only execute if we don't have the source file.
      %csFileName = getSubStr( %file, 0, strlen( %file ) - 4 );
      if( !isFile( %csFileName ) )
         exec( %csFileName );
  }

  // Load all source material files.

  for( %file = findFirstFile( "core/materials.cs" );
       %file !$= "";
       %file = findNextFile( "core/materials.cs" ))
  {
    exec( %file );
  }
}

function reloadCoreMaterials()
{
   reloadTextures();
   loadCoreMaterials();
   reInitMaterials();
}

//-----------------------------------------------------------------------------
// loadMaterials - load all materials.cs files
//-----------------------------------------------------------------------------
function loadMaterials()
{
  // Load any materials files for which we only have DSOs.

  for( %file = findFirstFile( "*/materials.cs.dso" );
       %file !$= "";
       %file = findNextFile( "*/materials.cs.dso" ))
  {
     // Only execute if we don't have the source file.
     %csFileName = getSubStr( %file, 0, strlen( %file ) - 4 );
     if( !isFile( %csFileName ) )
      exec( %csFileName );
  }

  // Load all source material files.

  for( %file = findFirstFile( "*/materials.cs" );
      %file !$= "";
      %file = findNextFile( "*/materials.cs" ))
  {
    exec( %file );
  }

  // Load all materials created by the material editor if
  // the folder exists
  if( IsDirectory( "materialEditor" ) )
  {
    for(%file = findFirstFile( "materialEditor/*.cs.dso" );
        %file !$= "";
        %file = findNextFile( "materialEditor/*.cs.dso" ))
    {
     // Only execute if we don't have the source file.
     %csFileName = getSubStr( %file, 0, strlen( %file ) - 4 );
     if( !isFile( %csFileName ) )
        exec( %csFileName );
    }
      for( %file = findFirstFile( "materialEditor/*.cs" );
        %file !$= "";
        %file = findNextFile( "materialEditor/*.cs" ))
     {
       exec( %file );
     }
  }
}

function reloadMaterials()
{
    reloadTextures();
    loadMaterials();
    reInitMaterials();
}

As you can see, much of the activity is a series of script loading calls. The balance of the work is divided between stuff that we’ve covered in earlier chapters, located in our control scripts, and a number of utility calls that do things like load materials, dump key bindings, and non-game logic actions like that.

Note that all of the scripts that are loaded are part of the core code base, and do not reside outside the core folder. We will look at selected key modules from these calls in the rest of this section.

SELECTED CORE SERVER MODULES

Next, we will take a look at some of the core code server modules. The modules selected are the ones that will best help illuminate how Torque operates.

Tip


Good programming practices indicate initial caps for functions. But, function names are not case-sensitive, so your code will compile regardless of your function name format preference.


The Server Module

Take a few minutes to scan over the code:

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------

function initBaseServer()
{
   // Base server functionality
   exec("./audio.cs");
   exec("./message.cs");
   exec("./commands.cs");
   exec("./levelInfo.cs");
   exec("./missionLoad.cs");
   exec("./missionDownload.cs");
   exec("./clientConnection.cs");
   exec("./kickban.cs");
   exec("./game.cs");
   exec("./spawn.cs");
   exec("./camera.cs");
   exec("./centerPrint.cs");
}

/// Attempt to find an open port to initialize the server with
function portInit(%port)
{
   %failCount = 0;
   while(%failCount < 10 && !setNetPort(%port))
   {
     echo("Port init failed on port " @ %port @ " trying next port.");
     %port++; %failCount++;
   }
}

/// Create a server of the given type, load the given level, and then
/// create a local client connection to the server.
//
/// @return true if successful.
function createAndConnectToLocalServer( %serverType, %level )
{
   if( !createServer( %serverType, %level ) )
      return false;

   %conn = new GameConnection( ServerConnection );
   RootGroup.add( ServerConnection );

   %conn.setConnectArgs( $pref::Player::Name );
   %conn.setJoinPassword( $Client::Password );

   %result = %conn.connectLocal();
   if( %result !$= "" )
   {
    %conn.delete();
    destroyServer();

    return false;
 }

    return true;
}

/// Create a server with either a "SinglePlayer" or "MultiPlayer" type
/// Specify the level to load on the server
function createServer(%serverType, %level)
{
  if (%level $= "")
  {
    error("createServer(): level name unspecified");
    return false;
  }

  // Make sure our level name is relative so that it can send
  // across the network correctly
  %level = makeRelativePath(%level, getWorkingDirectory());

  destroyServer();

  $missionSequence = 0;
  $Server::PlayerCount = 0;
  $Server::ServerType = %serverType;
  $Server::LoadFailMsg = "";
  $Physics::isSinglePlayer = true;

  // Setup for multi-player, the network must have been
  // initialized before now.
  if (%serverType $= "MultiPlayer")
  {
    $Physics::isSinglePlayer = false;

    echo("Starting multiplayer mode");

    // Make sure the network port is set to the correct pref.
    portInit($Pref::Server::Port);
    allowConnections(true);

    if ($pref::Net::DisplayOnMaster !$= "Never" )
        schedule(0,0,startHeartbeat);
  }
   // Create the ServerGroup that will persist for the lifetime of the server.
   new SimGroup(ServerGroup);

  // Load up any core datablocks
  exec("core/art/datablocks/datablockExec.cs");

  // Let the game initialize some things now that the
  // the server has been created
  onServerCreated();

  loadMission(%level, true);

  return true;
 }

/// Shut down the server
function destroyServer()
{
   $Server::ServerType = "";
   allowConnections(false);
   stopHeartbeat();
   $missionRunning = false;

   // End any running levels
   endMission();
   onServerDestroyed();

   // Delete all the server objects
   if (isObject(ServerGroup))
       ServerGroup.delete();

   // Delete all the connections:
   while (ClientGroup.getCount())
   {
     %client = ClientGroup.getObject(0);
     %client.delete();
   }

   $Server::GuidList = "";

   // Delete all the datablocks...
   deleteDataBlocks();

   // Save any server settings
   echo( "Exporting server prefs..." );
  export( "$Pref::Server::*", "~/prefs.cs", false );
}

   /// Reset the server's default prefs
   function resetServerDefaults()
   {
     echo( "Resetting server defaults..." );

     exec( "~/defaults.cs" );
     exec( "~/prefs.cs" );

     // Reload the current level
     loadMission( $Server::MissionFile );
   }


/// Guid list maintenance functions
function addToServerGuidList( %guid )
{
   %count = getFieldCount( $Server::GuidList );
   for ( %i = 0; %i < %count; %i++ )
   {
     if ( getField( $Server::GuidList, %i ) == %guid )
         return;
   }


   $Server::GuidList = $Server::GuidList $= "" ? %guid : $Server::GuidList TAB %guid;
}


function removeFromServerGuidList( %guid )
{
     %count = getFieldCount( $Server::GuidList );
     for ( %i = 0; %i < %count; %i++ )
     {
     if ( getField( $Server::GuidList, %i ) == %guid )
     {
        $Server::GuidList = removeField( $Server::GuidList, %i );
        return;
     }
   }
}

/// When the server is queried for information, the value of this function is
/// returned as the status field of the query packet. This information is
/// accessible as the ServerInfo::State variable.
function onServerInfoQuery()
{
      return "Doing Ok";
}

At the start of the core/server/server.cs module lies the definition of the function InitBaseServer, which loads a number of core server support modules. When we examine the rest of the server.cs module, we see the following functions:

   PortInit
   CreateAndConnectToLocalServer
   CreateServer
   DestroyServer
   ResetServerDefaults
   AddToServerGuidList
   RemoveFromServerGuidList
   OnServerInfoQuery

It’s not hard to get the sense from that list that this is a pretty critical module!

PortInit tries to seize control of the assigned TCP/IP port, and if it can’t it starts incrementing the port number until it finds an open one it can use.

There are two functions, CreateAndConnectToLocalServer and CreateServer that have similar functionality, but go about things in two different ways.

CreateServer does the obvious, but it also does some interesting things along the way. First, it makes a call to DestroyServer! This is not as wacky as it might seem; while DestroyServer does release and disable resources, it does so only after making sure the resources exist. So there’s no danger of referencing something that doesn’t exist, which would thus cause a crash. You need to specify the server type (single- [default] or multiplayer) and the mission name. The PortInit function is called from here, if the server will be a multiplayer server. The last, but certainly not the least, thing that CreateServer does is call LoadMission. This call kicks off a long and somewhat involved chain of events that we will cover in a later section.

CreateAndConnectToLocalServer also creates a server and connects to it, but it does it differently. You use this function when your player will be hosting his own game and connecting into it. You could still use CreateServer and do everything the long way, but this is more convenient.

DestroyServer releases and disables resources, as mentioned, and game mechanisms. It stops further connections from happening and deletes any existing ones; turns off the heartbeat processing; deletes all the server objects in ClientGroup and ServerGroup; and finally, purges all datablocks from memory.

ResetServerDefaults is merely a convenient mechanism for reloading the files in which the server default variable initializations are stored.

AddToServerGuidList and RemoveFromServerGuidList are two functions for managing the list of clients that are connected to the server.

OnServerInfoQuery is a message handler for handling queries from a master server. It merely returns the string “Doing Ok”. The master server, if there is one, will see this and know that the server is alive. It could say anything—there could even be just a single-space character in the string. The important point is that if the server is not doing okay, then the function will not even be called, so the master server would never see the message, would time out, and then would take appropriate action (such as panicking or something useful like that).

The Message Module

InitBaseServer loads the core server-side message module, message.cs.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// Server side message commands
//-----------------------------------------------------------------------------

function messageClient(%client, %msgType, %msgString,
                 %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13)
{
     commandToClient(%client, 'ServerMessage', %msgType, %msgString,
                 %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13);
}

function messageTeam(%team, %msgType, %msgString,
                 %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13)
{
  %count = ClientGroup.getCount();
  for(%cl= 0; %cl < %count; %cl++)
  {
     %recipient = ClientGroup.getObject(%cl);
        if(%recipient.team == %team)
           messageClient(%recipient, %msgType, %msgString,
                %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13);
  }
}

function messageTeamExcept(%client, %msgType, %msgString,
                %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13)
{
      %team = %client.team;
      %count = ClientGroup.getCount();
      for(%cl= 0; %cl < %count; %cl++)
      {
       %recipient = ClientGroup.getObject(%cl);
        if((%recipient.team == %team) & (%recipient != %client))
           messageClient(%recipient, %msgType, %msgString,
                   %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13);
  }
}

function messageAll(%msgType, %msgString,
             %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13)
{
  %count = ClientGroup.getCount();
  for(%cl = 0; %cl < %count; %cl++)
  {
     %client = ClientGroup.getObject(%cl);
     messageClient(%client, %msgType, %msgString,
                   %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13);
  }
}

function messageAllExcept(%client, %team, %msgtype, %msgString,
                   %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10,%a11,%a12,%a13)
{
//can exclude a client, a team or both. A -1 value in either field will ignore that
exclusion, so
     //messageAllExcept(-1, -1, $Mesblah,  'Blah!'), will message everyone (since there
shouldn't be a client -1 or client on team -1).
     %count = ClientGroup.getCount();
  for(%cl= 0; %cl < %count; %cl++)
  {
    %recipient = ClientGroup.getObject(%cl);
    if((%recipient != %client) && (%recipient.team != %team))
        messageClient(%recipient, %msgType, %msgString,
                    %a1, %a2, %a3, %a4, %a5, %a6,
                    %a7, %a8, %a9, %a10, %a11, %a12, %a13);
  }
}

//---------------------------------------------------------------------------
// Server side client chat'n
//---------------------------------------------------------------------------
// silly spam protection...
$SPAM_PROTECTION_PERIOD = 10000;
$SPAM_MESSAGE_THRESHOLD = 4;
$SPAM_PENALTY_PERIOD      = 10000;
$SPAM_MESSAGE = 'c3FLOOD PROTECTION:crYou must wait another %1 seconds.';

function GameConnection::spamMessageTimeout(%this)
{
  if(%this.spamMessageCount > 0)
    %this.spamMessageCount--;
}

function GameConnection::spamReset(%this)
{
      %this.isSpamming = false;
}

function spamAlert(%client)
{
   if($Pref::Server::FloodProtectionEnabled != true)
      return(false);

   if(!%client.isSpamming&&(%client.spamMessageCount>=$SPAM_MESSAGE_THRESHOLD))
    {
      %client.spamProtectStart = getSimTime();
      %client.isSpamming = true;
      %client.schedule($SPAM_PENALTY_PERIOD, spamReset);
    }

    if(%client.isSpamming)
    {
      %wait = mFloor(($SPAM_PENALTY_PERIOD - (getSimTime() -
                                             %client.spamProtectStart)) / 1000);
      messageClient(%client, "", $SPAM_MESSAGE, %wait);
      return(true);
    }

    %client.spamMessageCount++;
    %client.schedule($SPAM_PROTECTION_PERIOD, spamMessageTimeout);
    return(false);
}

//---------------------------------------------------------------------------

function chatMessageClient (%client,%sender,%voiceTag,%voicePitch,%msgString,
                                    %a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10 )
{
     //see if the client has muted the sender
     if ( !%client.muted[%sender] )
         commandToClient( %client, 'ChatMessage', %sender, %voiceTag,
                       %voicePitch, %msgString,
                       %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10 );
}

function chatMessageTeam
           (%sender,%team,%msgString,%a1,%a2,%a3,%a4,%a5,%a6,%a7,%a8,%a9,%a10)
{
     if ( ( %msgString $= "" ) || spamAlert( %sender ) )
        return;

     %count = ClientGroup.getCount();

     for ( %i = 0; %i < %count; %i++ )
     {
       %obj = ClientGroup.getObject( %i );
       if ( %obj.team == %sender.team )
          chatMessageClient( %obj, %sender, %sender.voiceTag,
                           %sender.voicePitch, %msgString,
                           %a1, %a2, %a3, %a4, %a5, %a6,
                           %a7, %a8, %a9, %a10 );
     }
}

function chatMessageAll
        (%sender,%msgString, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
     if ( ( %msgString $= "" ) || spamAlert( %sender ) )
     return;
     %count = ClientGroup.getCount();
     for ( %i = 0; %i < %count; %i++ )
     {
    %obj = ClientGroup.getObject( %i );
    if(%sender.team != 0)
       chatMessageClient( %obj, %sender, %sender.voiceTag,
                       %sender.voicePitch, %msgString,
                       %a1, %a2, %a3, %a4, %a5, %a6,
                       %a7, %a8, %a9, %a10 );
    else
    {
// message sender is an observer -- only send message to other observers
           if(%obj.team == %sender.team)
             chatMessageClient( %obj, %sender, %sender.voiceTag,
                              %sender.voicePitch, %msgString,
                              %a1, %a2, %a3, %a4, %a5, %a6,
                              %a7, %a8, %a9, %a10 );
    }
  }
}

Most of this module is dedicated to providing in-game chat capabilities for players.

  MessageClient
  MessageTeam
  MessageTeamExcept
  MessageAll
  MessageAllExcept
  GameConnection::SpamMessageTimeout
  GameConnection::SpamReset
  SpamAlert
  ChatMessageClient
  ChatMessageTeam
  ChatMessageAll

The first five functions in the preceding list are for sending server-type messages to individual clients, all clients on a team, and all clients in a game. There are also exception messages, where everyone is sent the message except a specified client, or team, or even both!

The three spam control functions are used in conjunction with the chat message functions. SpamAlert is called just before each outgoing chat message is processed for sending. Its purpose is to detect if a player is swamping the chat window with messages, an action called spamming the chat window. If there are too many messages in a short time frame as determined by the SpamMessageTimeout method, then the offending message is suppressed, and an alert message is sent to the client saying something like this: “Enough already! Take a break.” Well, you could say it more diplomatically than that, but you get the idea. SpamReset merely sets the client’s spam state back to normal after an appropriately silent interval.

Next are the three chat message functions. These are linked to the chat interfaces that players will use to communicate with each other.

These functions all use the CommandToServer function (see Chapter 6) internally. It is important to note that there will need to be message handlers for these functions on the client side.

The MissionLoad Module

Torque has a concept of mission that corresponds to what many other games, especially those of the first-person shooter genre, often call maps. A mission is defined in a mission file that has the extension of .mis. Mission files contain the information that specifies objects in the game world, as well as their placement in the world. Everything that appears in the game world is defined there: items, players, spawn points, triggers, water definitions, sky definitions, and so on.

Missions are downloaded from the server to the client at mission start time or when a client joins a mission already in progress. In this way, the server has total control over what the client sees and experiences in the mission.

Here are the contents of the core/server/missionLoad.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Server mission loading
//-----------------------------------------------------------------------------
// On every mission load except the first, there is a pause after
// the initial mission info is downloaded to the client.
$MissionLoadPause = 5000;

function loadMission( %missionName, %isFirstMission )
{
  endMission();
  echo("*** LOADING MISSION: " @ %missionName);
  echo("*** Stage 1 load");

  // Reset all of these
  if (isFunction("clearCenterPrintAll"))
    clearCenterPrintAll();
  if (isFunction("clearBottomPrintAll"))
    clearBottomPrintAll();

  // increment the mission sequence (used for ghost sequencing)
  $missionSequence++;
  $missionRunning = false;
  $Server::MissionFile = %missionName;
  $Server::LoadFailMsg = "";

  // Extract mission info from the mission file,
  // including the display name and stuff to send
  // to the client.
  buildLoadInfo( %missionName );

  // Download mission info to the clients
  %count = ClientGroup.getCount();
  for( %cl = 0; %cl < %count; %cl++ ) {
     %client = ClientGroup.getObject( %cl );
    if (!%client.isAIControlled())
       sendLoadInfoToClient(%client);
  }

  // Now that we've sent the LevelInfo to the clients
  // clear it so that it won't conflict with the actual
  // LevelInfo loaded in the level
  clearLoadInfo();

  // if this isn't the first mission, allow some time for the server
  // to transmit information to the clients:
  if( %isFirstMission || $Server::ServerType $= "SinglePlayer" )
    loadMissionStage2();
  else
    schedule( $MissionLoadPause, ServerGroup, loadMissionStage2 );
}

//-----------------------------------------------------------------------------

function loadMissionStage2()
{
  echo("*** Stage 2 load");
  // Create the mission group off the ServerGroup
  $instantGroup = ServerGroup;

  // Make sure the mission exists
  %file = $Server::MissionFile;
  if( !isFile( %file ) )
  {
      $Server::LoadFailMsg = "Could not find mission "" @ %file @ """;
  }
  else
  {
      // Calculate the mission CRC. The CRC is used by the clients
      // to caching mission lighting.
      $missionCRC = getFileCRC( %file );
     //Exec the mission.The MissionGroup(loaded components)is
     // added to the ServerGroup
      exec(%file);

      if( !isObject(MissionGroup) )
      {
      $Server::LoadFailMsg = "No 'MissionGroup' found in mission ""
                                                          @ %file @ "".";
      }
  }

  if( $Server::LoadFailMsg !$= "" )
  {
      // Inform clients that are already connected
      for (%clientIndex = 0; %clientIndex < ClientGroup.getCount();
                                                              %clientIndex++)
      messageClient(ClientGroup.getObject(%clientIndex), 'MsgLoadFailed',
                                                     $Server::LoadFailMsg);
      return;
  }

  // Set mission name.

  if( isObject( theLevelInfo ) )
     $Server::MissionName = theLevelInfo.levelName;

  // Mission cleanup group. This is where run time components will reside.
  // The MissionCleanup will be added to the ServerGroup.
     new SimGroup( MissionCleanup );
  // Make the MissionCleanup group the place where all new objects will automatically be
added.

     $instantGroup = MissionCleanup;

     // Construct MOD paths
     pathOnMissionLoadDone();

     // Mission loading done...
     echo("*** Mission loaded");

     // Start all the clients in the mission
     $missionRunning = true;
     for( %clientIndex=0; %clientIndex < ClientGroup.getCount(); %clientIndex++)
     ClientGroup.getObject(%clientIndex).loadMission();

     // Go ahead and launch the game
     onMissionLoaded();
}


//-----------------------------------------------------------------------------

function endMission()
{
  if (!isObject( MissionGroup ))
    return;

  echo("*** ENDING MISSION");

  // Inform the game code we're done.
  onMissionEnded();

  // Inform the clients
  for( %clientIndex=0; %clientIndex < ClientGroup.getCount(); %clientIndex++)
  {

    // clear ghosts and paths from all clients
    %cl = ClientGroup.getObject( %clientIndex );
    %cl.endMission();
    %cl.resetGhosting();
    %cl.clearPaths();
  }

  // Delete everything
  MissionGroup.delete();
  MissionCleanup.delete();

  clearServerPaths();
 }


 //-----------------------------------------------------------------------------

 function resetMission()
 {
    echo("*** MISSION RESET");

    // Remove any temporary mission objects
    MissionCleanup.delete();
    $instantGroup = ServerGroup;
    new SimGroup( MissionCleanup );
    $instantGroup = MissionCleanup;

    clearServerPaths();
    //
    onMissionReset();
 }

Here are the mission loading–oriented functions on the server contained in this module:

  LoadMission
  LoadMissionStage2
  EndMission
  ResetMission

This module is where we find the code that provides the server portion of the client/server mission loading process.

LoadMission, as we saw in an earlier section, is called in the CreateServer function. It kicks off the process of loading a mission onto the server. Mission information is assembled from the mission file and sent to all the clients for display to their users.

After the mission file loads, LoadMissionStage2 is called. In this function, the server calculates the CRC value for the mission and saves it for later use.

What’s a CRC Value, and Why Should I Care?


We use a Cyclic Redundancy Check (CRC) when transmitting data over potentially error-prone media. Networking protocols use CRCs at a low level to verify that the sent data is the same data that was received.

A CRC is a mathematical computation performed on data that arrives at a number that represents both the content of the data and how it’s arranged. The point is that the number, called a checksum, uniquely identifies the set of data, like a fingerprint.

By comparing the checksum of a set of data to another data set’s checksum, you can decide if the two data sets are identical.

Why should you care? Well, in addition to the simple goal of maintaining data integrity, CRCs are another arrow in your anticheat quiver. You can use CRCs to ensure that files stored on the clients are the same as the files on the server and, in this regard, that all the clients have the same files—the result is that the playing field is level.


Once the mission is successfully loaded onto the server, each client is sent the mission via a call to its GameConnection object’s LoadMission method.

EndMission releases resources and disables other mission-related mechanisms, clearing the server to load a new mission when tasked to do so.

ResetMission can be called from the EndGame function in the core/scripts/server/game.cs module to prepare the server for a new mission if you are using mission cycling techniques.

The MissionDownload Module

Here are the contents of the core/server/missionDownload.cs module:

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// Mission Loading
// The server portion of the client/server mission loading process
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// Loading Phases:
// Phase 1: Transmit Datablocks
//          Transmit targets
// Phase 2: Transmit Ghost Objects
// Phase 3: Start Game
//
// The server invokes the client MissionStartPhase[1-3] function to request
// permission to start each phase. When a client is ready for a phase,
// it responds with MissionStartPhase[1-3]Ack.
function GameConnection::loadMission(%this)
{
     // Send over the information that will display the server info
     // when we learn it got there, we'll send the datablocks
     %this.currentPhase = 0;
     if (%this.isAIControlled())
     {
         // Cut to the chase...
       %this.onClientEnterGame();
     }
     else
     {
     commandToClient(%this, 'MissionStartPhase1', $missionSequence,
       $Server::MissionFile, MissionGroup.musicTrack);
     echo("*** Sending mission load to client: " @ $Server::MissionFile);
     }
}

function serverCmdMissionStartPhase1Ack(%client, %seq)
{
  // Make sure to ignore calls from a previous mission load
  if (%seq != $missionSequence || !$MissionRunning)
     return;
  if (%client.currentPhase != 0)
     return;
  %client.currentPhase = 1;

  // Start with the CRC
  %client.setMissionCRC( $missionCRC );

  // Send over the datablocks...
  // OnDataBlocksDone will get called when have confirmation
  // that they've all been received.
  %client.transmitDataBlocks($missionSequence);
}

function GameConnection::onDataBlocksDone( %this, %missionSequence )
{
  // Make sure to ignore calls from a previous mission load
  if (%missionSequence != $missionSequence)
    return;
  if (%this.currentPhase != 1)
    return;
  %this.currentPhase = 1.5;

  // On to the next phase
  commandToClient(%this, 'MissionStartPhase2', $missionSequence, $Server::MissionFile);
}

function serverCmdMissionStartPhase2Ack(%client, %seq, %playerDB)
{
  // Make sure to ignore calls from a previous mission load
  if (%seq != $missionSequence || !$MissionRunning)
    return;
  if (%client.currentPhase != 1.5)
    return;
  %client.currentPhase = 2;

  // Set the player datablock choice
  %client.playerDB = %playerDB;

  // Update mod paths, this needs to get there before the objects.
  %client.transmitPaths();

  // Start ghosting objects to the client
  %client.activateGhosting();
}

function GameConnection::clientWantsGhostAlwaysRetry(%client)
{
   if($MissionRunning)
      %client.activateGhosting();
}

function GameConnection::onGhostAlwaysFailed(%client)
{
}
function GameConnection::onGhostAlwaysObjectsReceived(%client)
{
   // Ready for next phase.
   commandToClient(%client, 'MissionStartPhase3', $missionSequence, $Server::MissionFile);
}

function serverCmdMissionStartPhase3Ack(%client, %seq)
{
  // Make sure to ignore calls from a previous mission load
  if(%seq != $missionSequence || !$MissionRunning)
    return;
  if(%client.currentPhase != 2)
    return;
  %client.currentPhase = 3;

  // Server is ready to drop into the game
  %client.startMission();
  %client.onClientEnterGame();
}

The following functions and GameConnection methods are defined in the MissionDownload module:

   GameConnection::LoadMission
   GameConnection::OnDataBlocksDone
   GameConnection::ClientWantsGhostAlwaysRetry
   GameConnection::OnGhostAlwaysFailed
   GameConnection::OnGhostAlwaysObjectsReceived
   ServerCmdMissionStartPhase1Ack
   ServerCmdMissionStartPhase2Ack
   ServerCmdMissionStartPhase3Ack

The mission downloading process is designed to happen in three phases:

Phase 1: Transmit datablocks (and also transmit targets)

Phase 2: Transmit ghost objects

Phase 3: Start mission

The server invokes the client with a call to one of the MissionStartPhase1, MissionStartPhase2, or MissionStartPhase3 functions that reside on the client to request permission to start each phase. When a client is ready for a phase, it responds (acknowledges) by calling MissionStartPhase1Ack, MissionStartPhase2Ack, or MissionStartPhase3Ack on the server.

This module contains the mission download methods for each client’s GameConnection object.

The download process for the client object starts when its LoadMission method in this module is called at the end of the server’s LoadMissionStage2 function in the server’s MissionLoad module described in the previous section. It then embarks on a phased series of activities coordinated between the client and the server (see Figure 7.2). The messaging system for this process is the CommandToServer and CommandToClient pair of direct messaging functions.

Figure 7.1
Mission download phases.

image

The server invokes the client MissionStartPhasen (where n is 1, 2, or 3) function to request permission to start each phase. This is done using our old friend Command ToClient. When a client is ready for a phase, it responds with a MissionStartPhasenAck message, for which there is a handler on the server contained in this module.

The handler GameConnection::onDataBlocksDone is invoked when phase 1 has finished. This handler then initiates phase 2 by sending the MissionStartPhase2 message to the client.

The GameConnection::onGhostAlwaysObjectsReceived handler is invoked when phase 2 is completed. At the end of this phase, the client has all the data needed to replicate the server’s version of any dynamic objects in the game that are ghosted to the clients. This handler then sends the MissionStartPhase3 message to the client.

When the server receives the MissionStartPhase3Ack message, it then starts the mission for each client, inserting the client into the game.

Figure 7.2
Mission download process.

image

The ClientConnection Module

The ClientConnection module is where most of the server-side code for dealing with clients is located. Here are the contents of the core/scripts/server/client Connection.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
// This script function is called before a client connection
// is accepted. Returning "" will accept the connection,
// anything else will be sent back as an error to the client.
// All the connect args are passed also to onConnectRequest
//
function GameConnection::onConnectRequest( %client, %netAddress, %name )
{
    echo("Connect request from: " @ %netAddress);
    if($Server::PlayerCount >= $pref::Server::MaxPlayers)
    return "CR_SERVERFULL";
    return "";
}

//-----------------------------------------------------------------------------
// This script function is the first called on a client accept
//
function GameConnection::onConnect( %client, %name )
{
 // Send down the connection error info, the client is
 // responsible for displaying this message if a connection
 // error occures.
 messageClient(%client,'MsgConnectionError',"",
                                          $Pref::Server::ConnectionError);

 // Send mission information to the client
 sendLoadInfoToClient( %client );

 // Simulated client lag for testing...
 // %client.setSimulatedNetParams(0.1, 30);

 // Get the client's unique id:
 // %authInfo = %client.getAuthInfo();
 // %client.guid = getField( %authInfo, 3 );
 %client.guid = 0;
 addToServerGuidList( %client.guid );

 // Set admin status
 if (%client.getAddress() $= "local") {
     %client.isAdmin = true;
     %client.isSuperAdmin = true;
 }
 else {
    %client.isAdmin = false;
    %client.isSuperAdmin = false;
 }
// Save client preferences on the connection object for later use.
%client.gender = "Male";
%client.armor = "Light";
%client.race = "Human";
%client.skin = addTaggedString( "base" );
%client.setPlayerName(%name);
%client.score = 0;

//
echo("CADD: " @ %client @ " " @ %client.getAddress());

// Inform the client of all the other clients
%count = ClientGroup.getCount();
for (%cl = 0; %cl < %count; %cl++) {
   %other = ClientGroup.getObject(%cl);
   if ((%other != %client)) {
      // These should be "silent" versions of these messages...
      messageClient(%client, 'MsgClientJoin', "",
            %other.playerName,
            %other,
            %other.sendGuid,
            %other.score,
            %other.isAIControlled(),
            %other.isAdmin,
            %other.isSuperAdmin);
  }
}

// Inform the client we've joined up
messageClient(%client,
   'MsgClientJoin', 'Welcome to a Torque application %1.',
   %client.playerName,
   %client,
   %client.sendGuid,
   %client.score,
   %client.isAiControlled(),
   %client.isAdmin,
   %client.isSuperAdmin);

// Inform all the other clients of the new guy
messageAllExcept(%client, -1, 'MsgClientJoin', 'c1%1 joined the game.',
   %client.playerName,
   %client,
   %client.sendGuid,
   %client.score,
   %client.isAiControlled(),
   %client.isAdmin,
   %client.isSuperAdmin);

  // If the mission is running, go ahead download it to the client
  if ($missionRunning)
  {
     %client.loadMission();
  }
  else if ($Server::LoadFailMsg !$= "")
  {
     messageClient(%client, 'MsgLoadFailed', $Server::LoadFailMsg);
  }
  $Server::PlayerCount++;
}

//-----------------------------------------------------------------------------
// A player's name could be obtained from the auth server, but for
// now we use the one passed from the client.
// %realName = getField( %authInfo, 0 );
//
function GameConnection::setPlayerName(%client,%name)
{
   %client.sendGuid = 0;

  // Minimum length requirements
  %name = trim( strToPlayerName( %name ) );
  if ( strlen( %name ) < 3 )
     %name = "Poser";

  // Make sure the alias is unique, we'll hit something eventually
  if (!isNameUnique(%name))
  {
     %isUnique = false;
     for (%suffix = 1; !%isUnique; %suffix++) {
         %nameTry = %name @ "." @ %suffix;
         %isUnique = isNameUnique(%nameTry);
     }
     %name = %nameTry;
  }

  // Tag the name with the "smurf" color:
  %client.nameBase = %name;
  %client.playerName = addTaggedString("cpc8" @ %name @ "co");
}

function isNameUnique(%name)
{
  %count = ClientGroup.getCount();
  for ( %i = 0; %i < %count; %i++ )
  {
    %test = ClientGroup.getObject( %i );
    %rawName = stripChars( detag( getTaggedString( %test.playerName ) ), "cpcoc6c7c8c9" );
    if ( strcmp( %name, %rawName ) == 0 )
       return false;
  }
  return true;
}

//-----------------------------------------------------------------------------
// This function is called when a client drops for any reason
//
function GameConnection::onDrop(%client, %reason)
{
   %client.onClientLeaveGame();

   removeFromServerGuidList( %client.guid );
   messageAllExcept(%client, -1,   'MsgClientDrop',   'c1%1 has left the game.', %
client.playerName, %client);

   removeTaggedString(%client.playerName);
   echo("CDROP: " @ %client @ " " @ %client.getAddress());
   $Server::PlayerCount--;

   // Reset the server if everyone has left the game
   if( $Server::PlayerCount == 0 && $Server::Dedicated)
   schedule(0, 0, "resetServerDefaults");
}

//-----------------------------------------------------------------------------

function GameConnection::startMission(%this)
{
  // Inform the client the mission starting
  commandToClient(%this, 'MissionStart', $missionSequence);
}

function GameConnection::endMission(%this)
{
  // Inform the client the mission is done. Note that if this is
  // called as part of the server destruction routine, the client will
  // actually never see this comment since the client connection will
  // be destroyed before another round of command processing occurs.
  // In this case, the client will only see the disconnect from the server
  // and should manually trigger a mission cleanup.
  commandToClient(%this, 'MissionEnd', $missionSequence);
}


//--------------------------------------------------------------------------
// Sync the clock on the client.

function GameConnection::syncClock(%client, %time)
{
      commandToClient(%client, 'syncClock', %time);
}

//--------------------------------------------------------------------------
// Update all the clients with the new score

function GameConnection::incScore(%this,%delta)
{
   %this.score += %delta;
   messageAll('MsgClientScoreChanged', "", %this.score, %this);
}

The following functions and GameConnection methods are defined in the ClientConnection module:

   GameConnection::OnConnectRequest
   GameConnection::OnConnect
   GameConnection::SetPlayerName
   IsNameUnique
   GameConnection::OnDrop
   GameConnection::StartMission
   GameConnection::EndMission
   GameConnection::SyncClock
   GameConnection::IncScore

The method GameConnection::OnConnectRequest is the server-side destination of the client-side GameConnection::Connect method. We use this method to vet the request—for example, to examine the IP address to compare to a ban list, to make sure that the server is not full, and stuff like that. We have to make sure that if we want to allow the request, we must return a null string ( ”” ). If we don’t return a null string, then whatever we do return is treated as an error message or error code on the client.

The next method, GameConnection::OnConnect, is called after the server has approved the connection request. We get a client handle and a name string passed in as parameters. The first thing we do is ship down to the client a tagged string to indicate that a connection error has happened. We do not tell the client to use this string. It’s just a form of preloading the client.

Then we send the load information to the client. This is the mission information that the client can display to the user while the mission loading process takes place. After that, if the client also happens to be the host (entirely possible), we set the client to be a superAdmin.

Then we add the client to the user ID list that the server maintains. After that, there are a slew of game play client settings we can initialize.

Next, we start a series of notifications. First, we tell all clients that the player has joined the server. Then we tell the joining player that he is indeed welcome here, despite possible rumors to the contrary. Finally, we tell all the client-players that there is a new kid on the block, so go kill him. Or some such—whatever you feel like!

After all the glad-handing is done, we start downloading the mission data to the client starting the chain of events depicted back there in Figure 7.2.

GameConnection::SetPlayerName does some interesting name manipulation. First, it tidies up any messy names that have leading or trailing spaces. We don’t like names that are too short (trying to hide something?), so we don’t allow those names. Then we make sure that the name is not already in use. If it is, then an instance number is added to the end of the name. The name is converted to a tagged string so that the full name only gets transmitted once to each client; then the tag number is used after that, if necessary.

The function IsNameUnique searches through the server’s name list looking for a match. If it finds the name, then it isn’t unique; otherwise it is.

The method GameConnection::OnDrop is called when the decision is made to drop a client. First, the method makes a call to the client so that it knows how to act during the drop. Then it removes the client from its internal list. All clients (except the one dropped) are sent a server text message notifying them of the drop, which they can display. After the last player leaves the game, this method restarts the server. For a persistent game, this statement should probably be removed.

The next method, GameConnection::StartMission, simply notifies clients whenever the server receives a command to start another server session in order to give the clients time to prepare for the near-future availability of the server. The $missionSequence is used to manage mission ordering, if needed.

Next, GameConnection::EndMission is used to notify clients that a mission is ended, and hey! Stop playing already!

The method GameConnection::SyncClock is used to make sure that all clients’ timers are synchronized with the server. You can call this function for a client anytime after the mission is loaded but before the client’s player has spawned.

Finally, the method GameConnection::IncScore is called whenever you want to reward a player for doing well. By default, this method is called when a player gets a kill on another player. When the player’s score is incremented, all other players are notified, via their clients, of the score.

The Game Module

The server-side Game module is the logical place to put server-specific game play features. Here are the contents of the core/scripts/server/game.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// The default hooks that are most likely be overridden/implemented by a game
//-----------------------------------------------------------------------------

function onServerCreated()
{
  // Invoked by createServer(), when server is created and ready to go

  // Server::GameType is sent to the master server.
  // This variable should uniquely identify your game and/or mod.
  $Server::GameType = "Test App";

  // Server::MissionType sent to the master server. Clients can
  // filter servers based on mission type.
  $Server::MissionType = "Deathmatch";

  // Load up any objects or datablocks saved to the editor managed scripts
  if (isScriptFile("art/shapes/particles/managedParticleData.cs"))
     exec("art/shapes/particles/managedParticleData.cs");
  if (isScriptFile("art/decals/managedDecalData.cs"))
     exec("art/decals/managedDecalData.cs");
  if (isScriptFile("art/datablocks/managedDatablocks.cs"))
     exec("art/datablocks/managedDatablocks.cs");
  if (isScriptFile("art/forest/managedItemData.cs"))
     exec("art/forest/managedItemData.cs");

  // Load up user specified data and object declarations
  exec("art/datablocks/datablockExec.cs");

  // Run the other game play scripts in this folder
  exec("./scriptExec.cs");

  // For backwards compatibility...
  createGame();
}

function onServerDestroyed()
{
// Invoked by destroyServer(), right before the server is destroyed
     destroyGame();
}

function onMissionLoaded()
{
// Called by loadMission() once the mission is finished loadingstartGame();
}

function onMissionEnded()
{
// Called by endMission(), right before the mission is destroyed endGame();
}

function onMissionReset()
{
  // Called by resetMission(), after all the temporary mission objects
  // have been deleted.
}
//-----------------------------------------------------------------------------
// These methods are extensions to the GameConnection class. Extending
// GameConnection make is easier to deal with some of this functionality,
// but these could also be implemented as stand-alone functions.
//-----------------------------------------------------------------------------

function GameConnection::onClientEnterGame(%this)
{
  // Called for each client after it's finished downloading the
  // mission and is ready to start playing.
}

function GameConnection::onClientLeaveGame(%this)
{
  // Call for each client that drops
}


//-----------------------------------------------------------------------------
// Functions that implement game-play
// These are here for backwards compatibilty only, games and/or mods should
// really be overloading the server and mission functions listed above.
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------

function createGame()
{
  // This function is called by onServerCreated (above)
}

function destroyGame()
{
  // This function is called by onServerDestroyed (above)
}


//-----------------------------------------------------------------------------

function startGame()
{
  // This is where the game play should start
  // The default onMissionLoaded function starts the game.
}
function endGame()
{
  // This is where the game play should end
  // The default onMissionEnded function shuts down the game.
}

The following functions and GameConnection methods are defined in the Game module:

   OnServerCreated
   OnServerDestroyed
   OnMissionLoaded
   OnMissionEnded
   OnMissionReset
   CreateGame
   Destroy Game
   StartGame
   EndGame
   GameConnection::OnClientEnterGame
   GameConnection::OnClientLeaveGame

The first function defined, OnServerCreated, is called from CreateServer when a server is constructed. It is a useful place to load server-specific datablocks.

The variable $Server::GameType is sent to the master, if one is used. Its purpose is to uniquely identify the game and distinguish it from other games handled by the master server. The variable $Server::MissionType is also sent to the server—clients can use its value to filter servers based on mission type.

Next, you will notice a block of logic that starts with the if statement:

if (isScriptFile("art/shapes/particles/managedParticleData.cs"))

and proceeds through three more if-then pairs. Notice the use of the term managed in the file names. When you use some of Torque’s editors in the Mission Editor they need to save code in script form in a file somewhere. When Torque does this, it calls this code managedData, to distinguish this from code that is normally edited by a script programmer. It’s not always script code, although it usually is.

The next function, OnServerDestroyed, is the antithesis of OnServerCreated—anything you do there should be undone in this function.

The function OnMissionLoaded is called by LoadMission once a mission has finished loading. This is a great location to initialize mission-based game play features, like perhaps calculating weather effects based on a rotating mission scheme.

OnMissionEnded is called by EndMission just before it is destroyed; this is where you should undo anything you did in OnMissionLoaded.

OnMissionReset is a stub routine called by ResetMission, after all the temporary mission objects have been deleted. It doesn’t do anything, but it’s there for you to populate with logic if you need to.

CreateGame, Destroy Game, StartGame, and EndGame are all stub routines. The demo expects you to override these functions with your own code in your game’s control scripts.

The function GameConnection::OnClientEnterGame is called for each client after it has finished downloading the mission and is ready to start playing. This would be a good place to load client-specific persistent data from a database back end, for example.

GameConnection::OnClientLeaveGame is called for each client that is dropped. This would be a good place to do a final update of back-end database information for the client.

Although we don’t use too many of the functions in this module, it is a great location for a lot of game play features to reside.

What normally happens is all or most of these functions are overridden by our own version of this module in our game package.

SELECTED CORE CODE CLIENT MODULES

Next, we will take a close look at some of the core code client modules. The modules selected are some of the ones that will best help illuminate how Torque operates.

Keep in mind that all of these modules are designed to affect things that concern the local client, even though they might require contacting the server from time to time.

This point is important: when you add features or capabilities, you must always keep in mind whether you want the feature to affect only the local client (such as some user preference change) or you want the feature to affect all clients. In the latter case, it would be best to use modules that are server-resident when they run.

The Canvas Module

The Canvas module is another one of those simple, small, but critical modules. One of the key features of this module is that the primary function contained in here, InitCanvas, loads a number of general graphical user interface support modules. This module is loaded from the InitCore function rather than from the InitBaseClient function, which is where the rest of the key core modules get loaded. Here are the contents of the core/scripts/client/canvas.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// initializeCanvas
// Constructs and initializes the default canvas window.
//-----------------------------------------------------------------------------
$canvasCreated = false;

function configureCanvas()
{
  // Set up a good default if we don't have one already.
  if ($pref::Video::mode $= "")
      $pref::Video::mode = "800 600 false 32 60 0";

  %resX = getWord($pref::Video::mode, $WORD::RES_X);
  %resY = getWord($pref::Video::mode, $WORD::RES_Y);
  %fs = getWord($pref::Video::mode,    $WORD::FULLSCREEN);
  %bpp = getWord($pref::Video::mode, $WORD::BITDEPTH);
  %rate = getWord($pref::Video::mode, $WORD::REFRESH);
  %fsaa = getWord($pref::Video::mode, $WORD::AA);

  echo("-------------");
  echo("Attempting to set resolution to "" @ $pref::Video::mode @ """);

  %deskRes      = getDesktopResolution();
  %deskResX    = getWord(%deskRes, $WORD::RES_X);
  %deskResY    = getWord(%deskRes, $WORD::RES_Y);
  %deskResBPP = getWord(%deskRes, 2);

  // We shouldn't be getting this any more but just in case...
  if (%bpp $= "Default")
      %bpp = %deskResBPP;

  // Make sure we are running at a valid resolution
  if (%fs $= "0" || %fs $= "false")
  {
    // Windowed mode has to use the same bit depth as the desktop
    %bpp = %deskResBPP;
    // Windowed mode also has to run at a smaller resolution than the desktop
    if ((%resX >= %deskResX) || (%resY >= %deskResY))
    {
      warn("Warning: The requested windowed resolution is equal to or" SPC
        "larger than the current desktop resolution. Attempting to" SPC
        "find a better resolution");

      %resCount = Canvas.getModeCount();
      for (%i = (%resCount - 1); %i >= 0; %i--)
      {
        %testRes = Canvas.getMode(%i);
        %testResX = getWord(%testRes, $WORD::RES_X);
        %testResY = getWord(%testRes, $WORD::RES_Y);
        %testBPP = getWord(%testRes, $WORD::BITDEPTH);

       if (%testBPP != %bpp)
          continue;
       if ((%testResX < %deskResX) && (%testResY < %deskResY))
       {
         // This will work as our new resolution
         %resX = %testResX;
         %resY = %testResY;

         warn("Warning: Switching to "" @
                                       %resX SPC %resY SPC %bpp @ """);
          break;
      }
    }
  }
}

$pref::Video::mode = %resX SPC %resY SPC %fs SPC %bpp SPC %rate SPC %fsaa;

if (%fs == 1 || %fs $= "true")
    %fsLabel = "Yes";
else
    %fsLabel = "No";

echo("Accepted Mode: " NL
  "--Resolution : " @ %resX SPC %resY NL
  "--Full Screen : " @ %fsLabel NL
  "--Bits Per Pixel : " @ %bpp NL
  "--Refresh Rate : " @ %rate NL
  "--FSAA Level : " @ %fsaa NL
  "-------------");
  // Actually set the new video mode
  Canvas.setVideoMode(%resX, %resY, %fs, %bpp, %rate, %fsaa);

  // MLAAFx piggybacks on the FSAA setting in $pref::Video::mode.
  if (isObject(MLAAFx))
     MLAAFx.isEnabled = ( %fsaa > 0 ) ? true : false;
  //if ( $pref::Video::autoDetect )
  //    GraphicsQualityAutodetect();
}

function initializeCanvas()
{
  // Don't duplicate the canvas.
  if($canvasCreated)
  {
    error("Cannot instantiate more than one canvas!");
    return;
  }

  if (!createCanvas())
  {
    error("Canvas creation failed. Shutting down.");
    quit();
  }

  $canvasCreated = true;
}

//-----------------------------------------------------------------------------
// resetCanvas
// Forces the canvas to redraw itself.
//-----------------------------------------------------------------------------
function resetCanvas()
{
  if (isObject(Canvas))
    Canvas.repaint();
}

//-----------------------------------------------------------------------------
// Callbacks for window events.
//-----------------------------------------------------------------------------

function GuiCanvas::onLoseFocus(%this)
{
}

//-----------------------------------------------------------------------------
// Full screen handling
//-----------------------------------------------------------------------------

function GuiCanvas::attemptFullscreenToggle(%this)
{
  // If the Editor is running then we cannot enter full screen mode
  if ( EditorIsActive() && !%this.isFullscreen() )
  {
     MessageBoxOK("Windowed Mode Required",
                  "Please exit the Mission Editor to switch to full screen.");
     return;
  }
  // If the GUI Editor is running then we cannot enter full screen mode
  if ( GuiEditorIsActive() && !%this.isFullscreen() )
  {
      MessageBoxOK("Windowed Mode Required",
                   "Please exit the GUI Editor to switch to full screen.");
      return;
   }

   %this.toggleFullscreen();
}
//----------------------------------------------------------------------------------
// Editor Checking
// Needs to be outside of the tools directory so these work in non-tools builds
//----------------------------------------------------------------------------------
---------
function EditorIsActive()
{
   return ( isObject(EditorGui) && Canvas.getContent() == EditorGui.getId() );
}
function GuiEditorIsActive()
{
   return ( isObject(GuiEditorGui) && Canvas.getContent() ==
                                                    GuiEditorGui.getId() );
}

ConfigureCanvas is obviously the principal function in this module. When it is called, it first sets the video mode, if none has already been defined.

The video mode is set by a string of space-delimited values. The values represent, in order from left to right:

1. Horizontal screen resolution, in pixels (800)

2. Vertical screen resolution, in pixels (600)

3. Full screen boolean switch, where false means a windowed game, true means full screen (false)

4. Number of bits per pixel (bit depth) (32)

5. Video hardware refresh rate (60)

6. Antialiasing level (0=none)

After that, there are a number of lines of code that are used to run a sanity check on the game window settings, comparing them to the desktop settings, making sure that the requested settings are actually possible. Eventually, the script proceeds to build the video mode setting variable from all the discrete pieces using this line:

$pref::Video::mode = %resX SPC %resY SPC %fs SPC %bpp SPC %rate SPC %fsaa;

And a little further down, actually invoke the setting using this line:

Canvas.setVideoMode(%resX, %resY, %fs, %bpp, %rate, %fsaa);

Then, inside initializeCanvas, we attempt to create the canvas, which is essentially an indirect call to the Windows API to create a window.

If we can’t create the window (canvas), we quit because there is no point continuing without any means to display our game. CreateCanvas is mostly just an abstracted way to create a window with a graphics context. It’s abstracted due to the need to support similar capabilities on very different platforms (Windows, Xbox360, Macintosh, and others).

The ResetCanvas function checks to see if a canvas object exists, and if so, Reset-Canvas then forces it to be repainted (re-rendered).

Finally, the module contains a number of utility functions used for displaying error or progress information, managing the Mission Editor, and toggling back and forth between windowed mode and full screen mode.

The Mission Module

The Mission module doesn’t have a really heavy-duty role. It owes its existence to the need to provide the user with feedback about what is going on when a mission is loading. Here are the contents of the core/scripts/client/mission.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------

// Whether the local client is currently running a mission.
$Client::missionRunning = false;

// Sequence number for currently running mission.
$Client::missionSeq = -1;

// Called when mission is started.
function clientStartMission()
{
  // The client receives a mission start right before
  // being dropped into the game.
  physicsStartSimulation( "client" );

  // Start game audio effects channels.
  AudioChannelEffects.play();

  // Create client mission cleanup group.
  new SimGroup( ClientMissionCleanup );

  // Done.

  $Client::missionRunning = true;
}

// Called when mission is ended (either through disconnect or
// mission end client command).
function clientEndMission()
{
   // Stop physics simulation on client.
   physicsStopSimulation( "client" );

   // Stop game audio effects channels.

   AudioChannelEffects.stop();

   // Delete all the decals.
   decalManagerClear();

   // Delete client mission cleanup group.
   if( isObject( ClientMissionCleanup ) )
     ClientMissionCleanup.delete();

   clearClientPaths();

   // Done.
   $Client::missionRunning = false;
}

//----------------------------------------------------------------------------
// Mission start / end events sent from the server
//----------------------------------------------------------------------------

function clientCmdMissionStart(%seq)
{
   clientStartMission();
   $Client::missionSeq = %seq;
}

function clientCmdMissionEnd( %seq )
{
   if( $Client::missionRunning && $Client::missionSeq == %seq )
   {
     clientEndMission();
     $Client::missionSeq = -1;
   }
}
/// Expands the name of a mission into the full
/// mission path and extension.

function expandMissionFileName( %missionFile )
{
   // Expand any escapes in it.
   %missionFile = expandFilename( %missionFile );

   // If the mission file doesn't exist... try to fix up the string.
   if ( !isFile( %missionFile ) )
   {
     // Does it need a .mis?
     if ( strStr( %missionFile, ".mis" ) == -1 )
       %newMission = %missionFile @ ".mis";

     if ( !isFile( %newMission ) )
     {
       // Attach a path to it.
       %newMission = expandFilename( "levels/" @ %newMission );
     if ( !isFile( %newMission ) )
     {
        warn( "The mission file '" @ %missionFile @ "' was not found!" );
        return "";
     }
    }

    %missionFile = %newMission;
  }

  return %missionFile;
}
/// Load a single player level on the local server.
function loadLevel( %missionNameOrFile )
{
  // Expand the mission name... this allows you to enter
  // just the name and not the full path and extension.
  %missionFile = expandMissionFileName( %missionNameOrFile );
  if ( %missionFile $= "" )
    return false;

  // Show the loading screen immediately.
  if ( isObject( LoadingGui ) )
  {
    Canvas.setContent("LoadingGui");
    LoadingProgress.setValue(1);
    LoadingProgressTxt.setValue("LOADING MISSION FILE");
    Canvas.repaint();
  }

  // Prepare and launch the server.
  return createAndConnectToLocalServer( "SinglePlayer", %missionFile );
}

ClientCmdMissionStart is a message handler that gets called immediately before the client-player finds himself in the game. As soon as it is invoked, it calls Client-StartMission, which is where the real action takes place. This is a handy place for last-minute client-side code; the mission is known and loaded, and all objects (including any remote clients) are ghosted.

The physics simulation is fired up at this point, as well as the first in-game audio channels.

This might be a good place to build and display a map or to possibly fire up an Internet Relay Chat session, if you have written one for yourself in Torque Script (it is possible—a member of the GarageGames community has done just that).

ClientCmdMissionEnd resets or stops the physic simulation, halts any audio tracks that might be playing, and then proceeds to clear up any “Decals Errant” that might be wandering around in memory, looking for trouble. This is generally the place to undo anything started in the ClientCmdMissionStart function.

There are a couple of useful client-side utility functions in this module, like Expand-MissionFileName, which expands the name of a mission into the full mission path and extension, and LoadLevel, which does exactly what its name implies (in single-player mode), providing you feed it a valid mission name or file.

The MissionDownload Module

Just as the server side has a module called MissionDownload, so does the client side. It certainly can be confusing, so you have to stay on your toes when dealing with these modules, always being aware of whether you are dealing with the client or the server version. The choice of names is understandable though, when you realize that they are functionally complementary—the mission download activity requires synchronized and coordinated actions from both the client and the server. Two peas in a pod.

Here are the contents of the core/scripts/client/missiondownload.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
//----------------------------------------------------------------------------
// Mission Loading
// Server download handshaking. This produces a number of onPhaseX
// calls so the game scripts can update the game's GUI.
//
// Loading Phases:
// Phase 1: Download Datablocks
// Phase 2: Download Ghost Objects
// Phase 3: Scene Lighting
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
// Phase 1
//----------------------------------------------------------------------------
function clientCmdMissionStartPhase1(%seq, %missionName, %musicTrack)
{
// These need to come after the cls.
   echo ("*** New Mission: " @ %missionName);
   echo ("*** Phase 1: Download Datablocks & Targets");
   onMissionDownloadPhase1(%missionName, %musicTrack);
   commandToServer('MissionStartPhase1Ack', %seq);
}

function onDataBlockObjectReceived(%index, %total)
{
  onPhase1Progress(%index / %total);
}

//----------------------------------------------------------------------------
// Phase 2
//----------------------------------------------------------------------------
function clientCmdMissionStartPhase2(%seq,%missionName)
{
   onPhase1Complete();
   echo ("*** Phase 2: Download Ghost Objects");
   onMissionDownloadPhase2(%missionName);
   commandToServer('MissionStartPhase2Ack', %seq, $pref::Player:PlayerDB);
}

function onGhostAlwaysStarted(%ghostCount)
{
  $ghostCount = %ghostCount;
  $ghostsRecvd = 0;
}

function onGhostAlwaysObjectReceived()
{
  $ghostsRecvd++;
  onPhase2Progress($ghostsRecvd / $ghostCount);
}

//----------------------------------------------------------------------------
// Phase 3
//----------------------------------------------------------------------------
function clientCmdMissionStartPhase3(%seq,%missionName)
{
  onPhase2Complete();
  StartClientReplication();
  StartFoliageReplication();

  // Load the static mission decals.
  decalManagerLoad( %missionName @ ".decals" );

  echo ("*** Phase 3: Mission Lighting");
  $MSeq = %seq;
  $Client::MissionFile = %missionName;

  // Need to light the mission before we are ready.
  // The sceneLightingComplete function will complete the handshake
  // once the scene lighting is done.
  if (lightScene("sceneLightingComplete", ""))
  {
      echo("Lighting mission....");
      schedule(1, 0, "updateLightingProgress");
      onMissionDownloadPhase3(%missionName);
      $lightingMission = true;
   }
}

function updateLightingProgress()
{
  onPhase3Progress($SceneLighting::lightingProgress);
  if ($lightingMission)
     $lightingProgressThread = schedule(1, 0, "updateLightingProgress");
}

function sceneLightingComplete()
{
   echo("Mission lighting done");
   onPhase3Complete();

   // This is also the end of the mission load cycle.
   onMissionDownloadComplete();
   commandToServer('MissionStartPhase3Ack', $MSeq);
}
//----------------------------------------------------------------------------
// Helper functions
//----------------------------------------------------------------------------
function connect(%server)
{
   %conn = new GameConnection(ServerConnection);
   RootGroup.add(ServerConnection);
   %conn.setConnectArgs($pref::Player::Name);
   %conn.setJoinPassword($Client::Password);
   %conn.connect(%server);
}

When reviewing this module, you should refer back to the server-side Mission-Download module descriptions and Figures 7.1 and 7.2. Note that here we are downloading, not transmitting, and in Phase 3 we are performing the scene lighting (a client-side responsibility) while the server version is starting the game rolling.

Phase 1: Download datablocks (and also transmit targets)

Phase 2: Download ghost objects

Phase 3: Scene Lighting

The first function for phase 1, ClientCmdMissionStartPhase1, calls the function OnMissionDownloadPhase1, which is something you want to define in your control code. Its basic purpose is to set up for a progress display as the datablocks are loaded. As soon as this call returns, an acknowledgment is sent back to the server using CommandToServer to send the MissionStartPhase1Ack message back. At this time, it also reflects the sequence number (%seq) back to the server, to ensure that the client and server remain synchronized.

The next function, OnDataBlockObjectReceived, is an important one. This message handler gets called every time the Torque Engine client-side code detects that it has finished receiving a datablock. When invoked, it then calls onPhase1Progress, which needs to be defined in our control client code.

The next function, ClientCmdMissionStartPhase2, is part of the phase 2 activities. Its duties are much the same as for ClientCmdMissionStartPhase1, but this time using OnMissionDownloadPhase2 and MissionStartPhase2Ack.

The next function, OnGhostAlwaysStarted, is called by the engine after it processes the MissionStartPhase2Ack message. It is used to track ghosted object counts.

When an object has been successfully ghosted, onGhostAlwaysObjectReceived is called from the engine. We use this to call onPhase2Progress in order to update our progress display.

The ClientCmdMissionStartPhase3 function is the last in the series. When it is called we update our progress display and then turn on two client-side replication functions. These functions provide special objects (such as grass and trees) that will be computed and rendered only by the client. For example, the server sends a seed for the location of a tuft of grass. The client-side replication code calculates the locations of hundreds or even thousands of copies of this tuft of grass and distributes them appropriately.

Because these objects are deemed not to be critical for game play, we can take the risk of client-side computation without risking someone modifying the code to cheat. Someone could modify the code, but it wouldn’t gain him any online advantage.

Next, we call the function LightScene to perform the scene’s terrain and interior lighting passes. We pass the completion callback function SceneLightingComplete, which will be called when the lighting calculations are finished.

We also schedule a function (UpdateLightingProgress) to be repeatedly called while the lighting is under way, as follows:

schedule(1, 0, "updateLightingProgress");

In this case, the function is called after one millisecond.

UpdateLightingProgress is a short function. It makes a call to update the progress display and then schedules itself to be called again in another millisecond if the lighting is not finished. It can tell if the lighting is finished by checking the variable $lightingMission. If it is true, then lighting is still under way.

SceneLightingComplete is the completion callback passed to LightScene. When SceneLightingComplete is called, lighting has completed, so it sets the variable $lightingMission to false, which will, within a millisecond or so, be detected by UpdateLightingProgress. It then notifies the server that lighting is complete by sending the MissionStartPhase3Ack message. And away we go!

The insignificant little connect function, marked as a “Helper” function by the GG code comments, is nothing more than the most important function in the client/server code. Heh. Take that somebody. You can see that it creates a new GameConnection object, and then establishes the connection. Without that call, there is no way for the client to talk to the server. Trouble is, small functions just don’t get no respect.

The Message Module

The Message module processes commands sent from the server via front-end generic message handlers for two defined message types, as well as a tool for installing handlers at run time. You may or may not find this useful, but a look at how these functions work will help when it comes to creating your own sophisticated messaging system. Here are the contents of the core/scripts/client/message.cs module.

//-----------------------------------------------------------------------------
// Torque
// Copyright GarageGames, LLC 2011
//-----------------------------------------------------------------------------
// Functions that process commands sent from the server.

// This function is for chat messages only; it is invoked on the client when
// the server does a commandToClient with the tag ChatMessage. (Cf. the
// functions chatMessage* in core/scripts/server/message.cs.)
// This just invokes onChatMessage, which the mod code must define.
function clientCmdChatMessage(%sender, %voice, %pitch, %msgString, %a1, %a2, %a3,
%a4, %a5, %a6, %a7, %a8, %a9, %a10)
{
  onChatMessage(detag(%msgString), %voice, %pitch);
}

// Game event descriptions, which may or may not include text messages, can be
// sent using the message* functions in core/scripts/server/message.cs. Those
// functions do commandToClient with the tag ServerMessage, which invokes the
// function below.
// For ServerMessage messages, the client can install callbacks that will be
// run, according to the "type" of the message.

function clientCmdServerMessage(%msgType, %msgString, %a1, %a2, %a3, %a4, %a5, %a6,
%a7, %a8, %a9, %a10)
{
   // Get the message type; terminates at any whitespace.
   %tag = getWord(%msgType, 0);

   // First see if there is a callback installed that doesn't have a type;
   // if so, that callback is always executed when a message arrives.
   for (%i = 0; (%func = $MSGCB["", %i]) !$= ""; %i++) {
     call(%func, %msgType, %msgString, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10);
   }

   // Next look for a callback for this particular type of ServerMessage.
   if (%tag !$= "") {
      for (%i = 0; (%func = $MSGCB[%tag, %i]) !$= ""; %i++) {
       call(%func, %msgType, %msgString, %a1, %a2, %a3, %a4, %a5, %a6, %a7, %a8, %a9, %a10);
   }
  }
}

// Called by the client to install a callback for a particular type of
// ServerMessage.
function addMessageCallback(%msgType, %func)
{
  for (%i = 0; (%afunc = $MSGCB[%msgType, %i]) !$= ""; %i++) {
     // If it already exists as a callback for this type,
     // nothing to do.
     if (%afunc $= %func) {
        return;
    }
  }
  // Set it up.
  $MSGCB[%msgType, %i] = %func;
}

// The following is the callback that will be executed for every ServerMessage,
// because we're going to install it without a specified type. Any type-
// specific callbacks will be executed afterward.

// This just invokes onServerMessage, which can be overridden by the game
function onServerMessage(%a, %b, %c, %d, %e, %f, %g, %h, %i)
{
   echo("onServerMessage: ");
   if(%a !$= "") echo(" +- a: " @ %a);
   if(%b !$= "") echo(" +- b: " @ %b);
   if(%c !$= "") echo(" +- c: " @ %c);
   if(%d !$= "") echo(" +- d: " @ %d);
   if(%e !$= "") echo(" +- e: " @ %e);
   if(%f !$= "") echo(" +- f: " @ %f);
   if(%g !$= "") echo(" +- g: " @ %g);
   if(%h !$= "") echo(" +- h: " @ %h);
   if(%i !$= "") echo(" +- i: " @ %i);
}

function defaultMessageCallback(%msgType, %msgString, %a1, %a2, %a3, %a4, %a5, %a6, %a7,
%a8, %a9, %a10)
{
    onServerMessage(detag(%msgString));
}

// Register that default message handler now.
addMessageCallback("", defaultMessageCallback);

The first function, ClientCmdChatMessage, is for chat messages only and is invoked on the client when the server uses the CommandToClient function with the message type ChatMessage. Refer back to the server-side message module if you need to. The first parameter (%sender) is the GameConnection object handle of the player that sent the chat message. The second parameter (%voice) is an Audio Voice identifier string. Parameter three (%pitch) is rarely used, but is offered as a means for providing pitch control for an audio message. Finally, the fourth parameter (%msgString) contains the actual chat message in a tagged string. The rest of the parameters are not actually acted on so can be safely ignored for now. The parameters are passed on to the pseudo-handler OnChatMessage. It’s called a pseudo-handler because the function that calls OnChatMessage is not really calling out from the engine. However, it is useful to treat this operation as if a callback message and handler were involved for conceptual reasons.

The next function, ClientCmdServerMessage, is used to deal with game event descriptions, which may or may not include text messages. These can be sent using the message functions in the server-side Message module. Those functions use CommandToClient with the type ServerMessage, which invokes the function described next.

For ServerMessage messages, the client can install callbacks that will be run according to the type of message.

Obviously, ClientCmdServerMessage is more involved. After it uses the GetWord function to extract the message type as first text word from the string %msgType, it iterates through the message callback array ($MSGCB) looking for any untyped call-back functions and executes them all. It then goes through the array again, looking for registered callback functions with the same message type as the incoming message, executing any that it finds.

The next function, addMessageCallback, is used to register callback functions in the $MSGCB message callback array. This is not complex; addMessageCallback merely steps through the array looking for the function to be registered. If it isn’t there, addMessageCallback stores a handle to the function in the next available slot.

The last function, DefaultMessageCallback, is supplied in order to provide an untyped message to be registered. The registration takes place with the line after the function definition.

A FINAL WORD

The core code base includes a ton of functions and methods. We have only touched on about a fifth of them here. I aimed to show you the most important modules and their contents, and I think that’s been accomplished nicely. For your browsing pleasure, Appendix A contains a reference to find all the functions in all core code modules.

One last thing to remember about the core code: as chock-full of useful and important functionality as it is, you don’t need to use it to create a game with Torque. You’d be nuts to throw it away, in my humble opinion. Nonetheless, you could create your own script code base from the bottom up. One thing I hope this chapter has shown you is that a huge pile of work has already been done for you. You just need to build on it.

MOVING RIGHT ALONG

In this chapter, we took a look at the capabilities available in the core code base so that you will gain familiarity with how Torque scripts generally work. For the most part, it is probably best to leave the core code alone. There may be times, however, when you will want to tweak or adjust something in the core code or add your own set of features, and that’s certainly reasonable. You will find that the features you want to reuse are best added to the core code.

As you saw, much of the critical server-side core code is related to issues that deal with loading mission files, datablocks, and other resources from the server to each client as it connects.

In a complementary fashion, the client-side core code accepts the resources being sent by the server and uses those resources to prepare to display the new game environment to the user.

So, that’s enough programming and code for a while. In the next few chapters, we’ll get more artistic, dealing with visual things. In the next chapter, we’ll take a look at textures, how to make them, and how to use them. We’ll also learn a new tool we can use to create them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.249.174