Game Development Community

T3D 1.0.1 Barebones Dedicated Server Problems and Solutions

by Matt Kronyak · in Torque 3D Professional · 12/15/2009 (7:16 pm) · 33 replies

I finally started converting my TGEA game over to T3D and decided to start by splitting out the FPS example project into 3 projects: Client, Server and Common. This post is a quick reference for process, issues I ran into and solutions for those issues.

To get started I removed any client / gui related directories from the FPS example project:
  • tools
  • shaders
  • core\fonts
  • core\unifiedshell
  • core\scripts\client\
  • core\scripts\gui
  • core\art\gui
  • scripts\gui
  • scripts\client

One of the first issues was a problem with SFXDescriptions. The SFXDescription datablocks on the server all inherit from SFXDescriptions declared on the client. To resolve this I simply moved the SFXDescription's from the client to the top of core\art\datablocks\audioDescription.cs
//-----------------------------------------------------------------------------
//    Channel assignments (channel 0 is unused in-game).
//-----------------------------------------------------------------------------

$GuiAudioType        = 1;  // Interface.
$SimAudioType        = 2;  // Game.
$MessageAudioType    = 3;  // Notifications.
$MusicAudioType      = 4;  // Music.

//-----------------------------------------------------------------------------
//    Master SFXDescriptions.
//-----------------------------------------------------------------------------

// Master description for interface audio.
new SFXDescription( AudioGui )
{
   volume         = 1.0;
   channel        = $GuiAudioType;
};

// Master description for game effects audio.
new SFXDescription(AudioSim)
{
   volume         = 1.0;
   channel        = $SimAudioType;
};

// Master description for audio in notifications.
new SFXDescription( AudioMessage )
{
   volume         = 1.0;
   channel        = $MessageAudioType;
};

// Master description for music.
new SFXDescription( AudioMusic )
{
   volume         = 1.0;
   channel        = $MusicAudioType;
};

With that resolved I deleted all exec statements and related function calls that tried to exec files in any client/ or gui/ directory. This was a simple process with Torsion: CTRL-SHIFT-F to "Find in Files" and then searching for client/ and gui/ respectively. Going through the list in the search results and removing all of the calls was quick and easy -- most were in main.cs.

With all of the client scripts out of the way it seemed like the dedicated server should just work but there were two additional issues I ran into that required slight modification to the engine.

First, when starting the dedicated server it would immediately crash on: exec("core/art/datablocks/datablockExec.cs");

Specifically in that file when trying to:
// LightFlareData and LightAnimData(s)
exec("./lights.cs");


I tracked this down to the declaration of LightFlareData datablocks that had a flareTexture specified (starting with datablock LightFlareData( SunFlareExample )).

After some debugging it appeared that a call to (engine\data\T3D\lightFlareData.cpp):
void GFXPrimitiveBufferHandle::set(GFXDevice *theDevice, U32 indexCount, U32 primitiveCount, GFXBufferType bufferType, String desc)

in LightFlareData::_preload was the culprit. The easy solution was simply to move all of the code in this method into the if ( !server ) block resulting in the method looking like:
bool LightFlareData::_preload( bool server, String &errorStr )
{
	if ( !server )
	{
		mElementCount = 0;
		for ( U32 i = 0; i < MAX_ELEMENTS; i++ )
		{
			if ( mElementDist[i] == -1 )
				break;
			mElementCount = i + 1;
		}   

		if ( mElementCount > 0 )
			_makePrimBuffer( &mFlarePrimBuffer, mElementCount );

		if ( mFlareTextureName.isNotEmpty() )      
			mFlareTexture.set( mFlareTextureName, &GFXDefaultStaticDiffuseProfile, "FlareTexture" );  
	}

	return true;
}

Once this was resolved there was still another engine-related issue that was crashing the dedicated server. This time in Engine\sceneGraph\reflector.cpp

The very last line of the constructor was causing the problem:
ReflectorBase::ReflectorBase()
{
   mEnabled = false;
   mOccluded = false;
   mIsRendering = false;
   mDesc = NULL;
   mObject = NULL;
   mOcclusionQuery = GFX->createOcclusionQuery(); //server doesn't like this
}

To get around this I reserved creating the occlusion query until calcScore was called rather than doing it in the constructor. Since this is only ever called on the client the server won't run into this call. This resulted in the constructor and two methods being changed.

The constructor became:
ReflectorBase::ReflectorBase()
{
   mEnabled = false;
   mOccluded = false;
   mIsRendering = false;
   mDesc = NULL;
   mObject = NULL;
   mOcclusionQuery = NULL;
}

The destructor became:
ReflectorBase::~ReflectorBase()
{
	if(mOcclusionQuery != NULL)
		delete mOcclusionQuery;
}

And I added the following two lines right to the start of
F32 ReflectorBase::calcScore( const ReflectParams &params )
if(mOcclusionQuery == NULL)
		mOcclusionQuery = GFX->createOcclusionQuery();

With these changes in place the same executable/dll works without a problem on both the client and server. The dedicated server runs without crashing as does the client which appears to be handling the flares without issue.

If anyone finds this useful, great. If anyone has any suggestions or comments regarding the engine modifications (including areas that I may have missed that should be addressed for a dedicated server) even better.
Page «Previous 1 2
#1
12/15/2009 (11:34 pm)
Nice post. Would hope this stuff gets added in down the road if it hasn't already.
#2
12/17/2009 (9:18 am)
Thanks Matt, this has saved me a significant amount of time!
#3
12/18/2009 (2:51 am)
I'm glad some people found this useful. As an added bonus here's a little chunk of code I use to set the IP address to bind to as well as the port for the server to use as parameters passed to the exe. No engine changes necessary but could be useful as a reference for those unfamiliar with these.

In scripts\main.cs in function parseArgs() simply add the following to the for loop:

case "-port":
            $argUsed[%i]++;
            
            if(%hasNextArg)
               $Pref::Server::Port = %nextArg;
            else
               error("-port used without specifying port");
         
         case "-ip":
            $argUsed[%i]++;
            
            if(%hasNextArg)
               $Pref::Net::BindAddress = %nextArg;
            else
               error("-ip used without specifying IP Address");
#4
01/20/2010 (11:54 pm)
@Matt - Are you missing some brackets in the -port and -ip code?
I'm not a coder but just comparing them to the existing file.
#5
01/21/2010 (12:35 am)
@Scot: No.
#6
01/21/2010 (2:53 pm)
ok, thanks, so will this additional code allow me to start multiple servers in a batch file and specify the port and IP in the command line like so?

Start c:TorqMyProjectsmygamegamemygame.exe -dedicated -port 28007 -mission levels/emptyterrain.mis
Start c:TorqMyProjectsmygamegamemygame.exe -dedicated -port 28008 -mission levels/emptyroom.mis


If so, is there anything else I would need to change in other scripts?
#7
01/21/2010 (5:34 pm)
Thank you Mr. Kronyak, for taking the time to type this out, with such attention to documenting your research. The information you have shared is of considerable value.
#8
01/21/2010 (6:38 pm)
@Scot: Yes, that's the intention of this. You shouldn't need to change anything else in your scripts.
#9
01/21/2010 (8:15 pm)
@Matt
Hey I got this working and can bring up multiple servers with different ports so that's very cool...thanks!
Now, however, I'm trying to create triggers to go from one to another and can't find any current code to make it work. I've tried some old code I found but so far no luck. Have you attempted this yet?
#10
01/21/2010 (8:22 pm)
Using the normal torque commandToClient / commandToServer commands you can have the server send a command to the client along with the IP / port (as a single string, like "128.0.0.1:28005") and then have the client call: connect(%ipFromServer); (where %ipFromServer is the IP received from the server in the commandToClient call).

You can have whatever condition you need on the server side trigger the call to the client to instruct it to switch servers.

If you want to authenticate this in some way, you'll have to manage that yourself on the back end.
#11
01/21/2010 (8:38 pm)
Hmm, you lost me there...lol. I'm just a singer in a Rock & Roll band. I need examples or better yet exact code...lol. I'm really good at copy/paste/change the variables though!

Here's what I'm doing so far...
in datablocks/triggers.cs:
datablock TriggerData(Level1PortalTrigger)
{
tickPeriodMS = 100;
};

in scripts/server/triggers.cs:
function Level1PortalTrigger::onEnterTrigger(%data, %obj, %colObj)
{
%client = %colObj.client;
if (%client) {
commandToClient(%client, 'connectlevel1Portal');
} else {
echo ("Not a client");
}
}
Then in scripts/client/triggerclient.cs:
function clientCmdconnectserverPortal()
{
connect("192.168.0.66:1111",$Client::Password,$pref::Player::Name);
}

Close at all?
#12
01/21/2010 (8:47 pm)
@Scot:

Almost. Change: function clientCmdconnectserverPortal() to function clientCmdconnectlevel1Portal()
#13
01/21/2010 (9:38 pm)
heh, yeah, I just found that...still not working though.
Getting an error in the console saying:
Mapping string: connectlevel1Portal to index: 17
111: Cannot re-declare object [ServerConnection].
core/scripts/client/missionDownload.cs (113): Unable to find object: '0' attempting to call function 'setConnectArgs'
core/scripts/client/missionDownload.cs (114): Unable to find object: '0' attempting to call function 'setJoinPassword'
core/scripts/client/missionDownload.cs (115): Unable to find object: '0' attempting to call function 'connect'

In the server I see: Left Level1 Portal Trigger everytime I walk through.
In the game my gun retracts as I go into it but nothing else.
#14
01/21/2010 (9:48 pm)
Got it!
I changed the info in scripts/client/triggerclient.cs to:

function clientCmdconnectLevel1Portal()
{
schedule(0, 0, "Disconnect");
schedule(0, 0, "joinLevel1Portal");
}

function joinLevel1Portal()
{
%conn = new GameConnection(ServerConnection);
%conn.setConnectArgs($pref::Player::Name);
%conn.setJoinPassword($Client::Password);
%conn.connect("192.168.0.66:1111");
}

and it went!
...now to see if it works with a couple of players
#15
01/26/2010 (3:31 am)
@Matt - You seem to know the most about this whole dedicated server thing or at least seem willing to answer my noob questions...lol. I've got another question for you...

I have a log-in gui called LoginDlg.gui that allows the player to log in and verifies them against the database. It's called when you click Play on the mainMenuGui...the problem is...when I connect using -connect IP:Port in a shortcut it by passes the main menu and goes straight to the mission specified on the server start up.
How do I call up my LoginDlg.gui before it goes straight into the mission?
Also, once verified it goes to an avatar selection screen. But it's set to connect locally, so it does just that. I've tried a couple of things but csn't get it to log into the mission on the server.

It's a lot I know...should be simple enough though if only I knew the code!

Thanks!
#16
01/26/2010 (3:53 am)
@Scot - Building off of the existing torque scripts you can handle this a few ways. It seems like you want to pass in an IP address from the shortcut that the player WILL connect to once they are logged in, but you don't want them to automatically connect.

First way: Create a script file somewhere and store the IP address as a global variable (using $ at the beginning of the variable name to make it global -- for example, $IPAddress = "127.0.0.1";) and don't pass the IP via connect at all.

Second way: In scripts\main.cs find the function: function parseArgs()
Go down to the block of code that does this:
case "-connect":
            $argUsed[%i]++;
            if (%hasNextArg) {
               $JoinGameAddress = %nextArg;
               $argUsed[%i+1]++;
               %i++;
            }
            else
               error("Error: Missing Command Line argument. Usage: -connect <ip_address>");
[/cpde]
And change the line that reads
[code]
               $JoinGameAddress = %nextArg;
to use a custom variable name of your choosing. Then, when the player connects use your custom variable.

Third way: Instead of changing parseArgs in main.cs go to scripts\client\init.cs and find function initClient()

In there you will see this chunk of code right at the bottom of the function:
// Connect to server if requested.
   if ($JoinGameAddress !$= "") {
      // If we are instantly connecting to an address, load the
      // main menu then attempt the connect.
      loadMainMenu();
      connect($JoinGameAddress, "", $Pref::Player::Name);
   }
   else {
      // Otherwise go to the splash screen.
      Canvas.setCursor("DefaultCursor");
      loadStartup();
   }

Simply change this to only do a normal load to the splash screen (or whatever screen you want) by changing that entire block of code to:
Canvas.setCursor("DefaultCursor");
      loadStartup();

Then when the user logs on using the shortcut you can use the $JoinGameAddress in your connect call.
#17
01/26/2010 (4:34 am)
Well, I chose Door #3 and it brought up the main menu and I clicked Play and it brought up the Login gui and appears to have verified me and took me to my next screen but when I clicked to go onto the server it crashed the client.
I guess I'm not sure what to put there to set up the conection.
So far I have:

function BeginMission( %playerId )
{
//trace(true);
%mission = "levels/orcbog.mis";

createServer(%serverType, %mission);
DefaultSpawner.spawnDatablock = "PlayerData" @ %playerId;
%conn = new GameConnection(ServerConnection);
RootGroup.add(ServerConnection);
%conn.setConnectArgs($pref::Player::Name);
%conn.setJoinPassword($Client::Password);
%conn.connect("1.2.3.4:28000");

ServerConnection.setFirstPerson(false);
}

Must be off a little huh?

EDIT: Scratch that...it connects to the server and goes to the loading screen and starts to load datablocks and THEN it crashes...
the console.log ends here...

'ParticleData' datablock with id: 73 already existed. Clobbering it with new 'SFXProfile' datablock from server.
A 'ParticleEmitterData' datablock with id: 74 already existed. Clobbering it with new 'SFXProfile' datablock from server.
A 'ParticleData' datablock with id: 75 already existed. Clobbering it with new 'SFXProfile' datablock from server.
A 'ParticleEmitterData' datablock with id: 76 already existed. Clobbering it with new 'LightningData' datablock from server.
A 'SFXProfile' datablock with id: 77 already existed. Clobbering it with new 'TriggerData' datablock from server.
A 'PrecipitationData' datablock with id: 78 already existed. Clobbering it with new 'TriggerData' datablock from server.

I'm certain not everything in that level matches on the server at the moment...maybe I'll try an empty level and see what happens...

EDIT: No luck...stops and crashes at the same spot...any ideas?
#18
01/26/2010 (1:32 pm)
Day 2 EDIT: Just in case you're up and checking this...I see the problem now. It's all about separating the client from the server which is what this post was originally all about! (I actually asked this question a few months ago right out of the shute but no one answered me. I come from an engine where the client and server are 2 separate exes so I never had to think about it. Torque confused the hell out of me...and still does!)

So, I guess it's time to knuckle down and try to separate the two...is everything posted above all that needs to be done?
#19
01/26/2010 (1:42 pm)
I would suggest backup your entire project somewhere before doing this but otherwise its pretty straight forward. Once you have the separate dedicated server you can also remove all of the server code from the client.

I structured my own project in source control (SVN) in 3 folders: client (client scripts and client-side only art assets for GUIs and whatnot), common (3d models, sound files, the game .exe / .dll and so on) and server (all of the server side scripts).

I copy the files from my "common" folder into the client and server folders so each have their own copy.
#20
01/26/2010 (1:49 pm)
@Matt: read about "externals" in the SVN manual. It allows you to have multiple copies of a folder in your working copy that actually point to a single folder in the repository, that way when you make a change into one of the duplicated common folders it will automatically show up in the other copy when you do a SVN update.
Page «Previous 1 2